This post is an improvement to [ir0nstone's note on challenges in containers](https://ir0nstone.gitbook.io/notes/misc/challenges-in-containers). ## Why? > Sometimes you get challenges provided with a Dockerfile. In most cases, it's best to use it, as you can be sure it acts the same locally and remotely. I completely agree with him. Sometimes, the `chall_patched` file from running `pwninit` may act differently from the remote server. Especially in `ret2dlresolve` challenges. Through this whole post, I assume the original file is named `chall`. ## Building container ### Modifying Dockerfile Install extra tools inside the Dockerfile [0]: ```dockerfile # ubuntu image RUN apt-get install -y gdbserver -- OR -- # alpine image RUN apk add gdb ``` #### _Note for `pwn.red/jail` images_: If the author uses redpwn images, it may look like this (Dockerfile from UOFTCTF 2025): ```dockerfile FROM ubuntu@sha256:80dd3c3b9c6cecb9f1667e9290b3bc61b78c2678c02cbdae5f0fea92cc6734ab AS app RUN mkdir -p /challenge WORKDIR /challenge COPY chall . COPY flag.txt . FROM pwn.red/jail COPY --from=app / /srv RUN mkdir -p /srv/app COPY --chmod=555 ./run /srv/app/run ENV JAIL_PIDS=40 JAIL_MEM=10M JAIL_TIME=120 ``` We should rename it to `Dockerfile_bak`, and write our new `Dockerfile`, use only the ubuntu image [2], install `gdbserver` in it, and use `socat` to serve the file: ```dockerfile FROM ubuntu@sha256:80dd3c3b9c6cecb9f1667e9290b3bc61b78c2678c02cbdae5f0fea92cc6734ab RUN mkdir -p /challenge RUN apt update && apt install -y gdbserver socat WORKDIR /challenge COPY chall . COPY flag.txt . CMD socat TCP-LISTEN:5000,reuseaddr,fork EXEC:"./chall" ``` #### _Note for xinetd_ Remember to increase the timeout to one day ;)) The process is the same, install gdb, gdbserver. ### Adding docker-compose.yml I don't want to deal with all the parameters everytime I re-run the container, so I add a `docker-compose.yml`, here I name the container `debug_container` for later convinience: ```dockerfile services: challenge: build: context: . dockerfile: Dockerfile container_name: debug_container ports: - "5000:5000" # port for socat - "9090:9090" # port for gdbserver stdin_open: true tty: true cap_add: - SYS_PTRACE ``` Then, to start: ```bash docker compose up -d --build ``` To shutdown the container: ```bash docker compose down ``` ## Attaching and debugging manually There should be 2 terminal for this: one for starting gdbserver, one for attaching gdb. * On the first: ```bash docker exec -u root -it debug_container bash ``` Everytime you want to start GDB server: ```bash gdbserver :9090 --attach $(pidof chall) ``` Or, if you don't want to keep that bash inside container: ```bash docker exec -u root -it debug_container bash -c "gdbserver :9090 --attach \$(pidof chal) &" ``` Remove the `&` at the end if you want to see the logs. * On the second: ```bash gdb ./chall ``` Inside `gdb`, when we want to attach: ``` target remote :9090 ``` Notice here, we start gdb with the **ORIGINAL** file, not the patched one. ## Automating the debugging process with pwntools ```pyt= remote_connection = "nc addr 5000".split() local_port = 5000 localscript = f''' file {context.binary.path} define rerun !docker exec -u root -i debug_container bash -c "kill -9 \\$(pidof gdbserver) &" !docker exec -u root -i debug_container bash -c "gdbserver :9090 --attach \\$(pidof chall) &" end define con target remote :9090 end ''' gdbscript = ''' # put your gdb script here ''' def start(): if args.REMOTE: return remote(remote_connection[1], int(remote_connection[2])) elif args.LOCAL: return remote("localhost", local_port) elif args.GDB: return gdb.debug({proc_args}, gdbscript=gdbscript) else: return process({proc_args}) def GDB(): if not args.LOCAL and not args.REMOTE: gdb.attach(p, gdbscript=gdbscript) pause() if args.LOCAL: gdbserver = process("docker exec -u root -i debug_container bash -c".split()+ [f"gdbserver :9090 --attach $(pidof chall) &"]) pid = gdb.attach(('0.0.0.0', 9090), exe=f'{{context.binary.path}}', gdbscript=localscript+gdbscript) pause() p = start() GDB() p.interactive() ``` Get my full template [here](https://gist.github.com/khoatran107/b0969b216b489e2e4817fc2d41fbd5cb). Put `GDB()` when you want to start debugging. Note that when debug in Docker, you should remove `_patched` in `exe = ELF("./chall_patched")`. To run and attach gdb to the file running in Docker: ```bash python3 solve.py LOCAL ``` In your `pwndbg` instance, if somehow the `gdbserver` stops, just type `rerun`, then `con`. ## ASLR Run the following command on the **HOST** machine to disable ASLR in the whole system, hence also disable ASLR in the docker container [1]: ```bash echo 0 | sudo tee /proc/sys/kernel/randomize_va_space ``` To re-enable: ```bash echo 2 | sudo tee /proc/sys/kernel/randomize_va_space ``` ## References [0]: [ir0nstone's note: Challenges in Containers](https://ir0nstone.gitbook.io/notes/misc/challenges-in-containers) [1]: [Disable ASLR inside Docker container](https://security.stackexchange.com/questions/214923/disable-aslr-inside-docker-container#:~:text=privileges%20with%20--privileged-,solution,-The%20only%20way) [2]: [Redpwn jail docs for competitors](https://github.com/redpwn/jail/blob/main/docs/competitors.md)