DreamHack 시스템 해킹 문제 Docker 구성 방법

Dreamhack 은 티오리에서 운영하는 사이트로, 해킹 및 사이버 보안과 관련된 내용을 공부하고 워게임, CTF 콘텐츠를 이용할 수 있다.
Dreamhack 에서 이용할 수 있는 워게임 콘텐츠 중 시스템 해킹 분야는 바이너리 파일을 실행하는 환경에 따라 exploit 의 성공 여부가 좌우되는 경우가 많기 때문에 해당 바이너리를 실행하는 도커 환경을 구성하는 DockerFile 이 함께 첨부된다.
이 글에서는 문제에 첨부된 DockerFile 을 이용해 바이너리 파일 실행 환경을 구성하고, pwndbg 로 동적 디버깅을 하는 방법에 대해 설명한다.
1. 테스트 환경
테스트 용 문제는 struct person_t
을 사용했다. 해당 문제의 첨부 파일을 다운로드하고 zip 압축 해제하면 아래와 같은 폴더 구조를 가진다.
.
├── deploy
│ ├── chall
│ ├── chall.c
│ └── flag
└── Dockerfile
deploy
내 파일은 다음과 같은 역할을 가진다.
chall
:chall.c
로 빌드한 바이너리 실행 파일.chall.c
: 취약점을 가지는 소스 코드.flag
: flag 값을 가지는 텍스트 파일. 첨부 파일은 더미 값이 적혀 있다.
Docker 이미지를 구성하는 내용은 Dockerfile
에 아래와 같이 적혀 있다.
FROM ubuntu:22.04@sha256:340d9b015b194dc6e2a13938944e0d016e57b9679963fdeb9ce021daac430221
ENV user chall
ENV chall_port 31337
RUN apt-get update
RUN apt-get -y install socat
RUN adduser $user -u 1337
ADD ./deploy/flag /home/$user/flag
ADD ./deploy/$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 socat -T 90 TCP-LISTEN:$chall_port,reuseaddr,fork EXEC:/home/$user/$user
OS 환경은 Linux kali 6.12.13-amd64 이며 pwndbg 버전은 아래와 같다.
- Pwndbg: 2025.02.19 build: a82e152f
- Python: 3.13.2 (main, Mar 13 2025, 14:29:07) GCC 14.2.0
- GDB: 16.2
- Capstone: 5.0.5
- Unicorn: 2.1.3
- Pwnlib: 4.14.0
2. Docker 환경 구성
2.1 이미지 빌드
첨부 파일을 압축 해제한 디렉토리로 working directory 를 이동한 후, 아래와 같은 docker 명령어를 실행하여 현재 경로의 Dockerfile 의 정보를 참조한 이미지를 빌드해야 한다.
$ sudo docker build -t {임의의 이미지 이름} .
docker images
명령어를 실행하면 현재 로컬 시스템 내 docker 이미지 목록을 확인할 수 있다.
$ sudo docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
... 중략 ...
struct latest 4ee4a8e5667f 3 weeks ago 140MB
... 후략 ...
2.2 컨테이너 생성
이제 앞선 과정에서 빌드한 이미지를 기반으로 컨테이너를 생성해야 한다. 컨테이너 생성 옵션 중 중요한 것은 -p
옵션이다. 이 옵션은 컨테이너 외부-내부 포트를 매칭시키는 값을 지정한다. 이 문제의 Dockerfile
은 컨테이너 내부에서 31337번 포트를 통해 socat
을 실행해 통신하므로, 컨테이너 외부 포트를 31337번 포트와 매칭시켜야 한다.
$ sudo docker run -d -p {임의의 포트}:{Dockerfile 에서 expose 한 포트 번호} --name {임의의 컨테이너 이름} {앞서 빌드한 이미지 이름}
예제에서는 대충 1234번 포트를 31337 포트와 매칭시키고 컨테이너 이름은 struct_container1
로 설정했다.
$ sudo docker run -d -p 1234:31337 --name struct_container1 struct
c79a35372970ec7104694891ff40723c8565a50135f7aa58cf67e48fd4bcc22b
docker ps
명령어로 현재 실행 중인 도커 컨테이너 정보를 확인할 수 있다.
$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c79a35372970 struct "/bin/sh -c 'socat -…" 45 seconds ago Up 44 seconds 0.0.0.0:1234->31337/tcp, :::1234->31337/tcp struct_container1
3. pwndbg 로 원격 디버깅
디버깅에 앞서 우리가 생성한 도커 컨테이너가 잘 작동하는지 확인해보자. dreamhack 에서 VM 에 접속하듯이 nc
명령어를 실행해 도커 컨테이너에 접속하여 바이너리를 실행할 수 있다. 현재 이 컨테이너는 localhost
에서 실행 중이며, 위 예제에서 외부 포트를 1234로 설정했으므로 아래와 같은 명령어를 실행하여 접속한다.
$ nc localhost 1234
접속에 성공하면 컨테이너 내부의 바이너리와 정상적으로 사용자 입력, 출력을 주고 받을 수 있는 것을 확인할 수 있다.
컨테이너가 정상 실행 중인 것을 확인했다면 이제 pwndbg 로 원격 디버깅을 시도할 차례다. nc localhost {port 번호}
명령어로 컨테이너 내 바이너리 파일을 실행 후, ps -ef
명령어를 실행하면 컨테이너 내에서 실행 중인 바이너리 프로세스를 확인할 수 있다. 아래 예시에서는 /home/chall/chall
경로를 실행 중인 48274
pid 프로세스가 컨테이너 내에서 실행 중인 바이너리다.
$ sudo ps -ef | grep chall
1337 44498 44469 0 11:58 ? 00:00:00 /bin/sh -c socat -T 90 TCP-LISTEN:$chall_port,reuseaddr,fork EXEC:/home/$user/$user
1337 44524 44498 0 11:58 ? 00:00:00 socat -T 90 TCP-LISTEN:31337,reuseaddr,fork EXEC:/home/chall/chall
1337 48273 44524 0 12:05 ? 00:00:00 socat -T 90 TCP-LISTEN:31337,reuseaddr,fork EXEC:/home/chall/chall
1337 48274 48273 0 12:05 ? 00:00:00 /home/chall/chall
kali 48715 48233 0 12:06 pts/5 00:00:00 grep --color=auto chall
sudo gdb -p {원격 디버깅할 프로세스 pid}
명령어를 실행하면 해당 프로세스에 attach 할 수 있다. 앞서 ps
명령어로 찾은 컨테이너 내 바이너리 프로세스를 찾아 attach 하자.
사용자 권한으로 gdb
를 실행하면 ~/.gdbinit
파일 내 명시된 gdbinit.py
파일 경로를 찾아 자동으로 pwndbg
로 변경되지만, sudo
명령어로 gdb
를 실행하면 자동으로 pwndbg
로 변경되지 않을 수 있다. 이 경우엔 gdb
로 원격 접속 후, 직접 source
명령어로 gdbinit.py
경로를 지정하여 pwndbg
로 변경하자.
(gdb) source {pwndbg 경로}/gdbinit.py
pwndbg
로 변경 후 procinfo
명령어를 실행하면 /home/chall/chall
프로세스에 제대로 attach 한 것을 확인할 수 있다.
pwndbg> procinfo
exe '/home/chall/chall'
cmdline /home/chall/chall ''
cwd '/home/chall'
pid 53397
tid 53397
selinux docker-default (enforce)
ppid 44498
uid [1337, 1337, 1337, 1337]
gid [1337, 1337, 1337, 1337]
groups [1337]
fd[0] socket:[113898]
fd[1] socket:[113898]
fd[2] pipe:[95364]
fd[3] socket:[113078]
fd[4] socket:[113079]
이제 원하는대로 브레이크 포인트를 설정하거나, instruction 을 하나하나 실행하면서 디버깅하면 된다. 참고로 attach 한 직후는 아마 사용자 입력을 받기 위해 blocking된 상태라서 step into 명령어를 내려도 instruction 이 실행되지 않을 거다. nc
명령어를 실행한 창으로 이동해 적절히 사용자 입력 값을 입력하면 blocking 상태가 풀린다.