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)