sandboxgrind
- Event: hxp CTF 2021
- Category: pwn
- Solves: 32
- Points: 244
Overview
The challenge files can be downloaded from here. This was a sandbox written with the help of valgrind - it’s written as a valgrind tool and run with
/sandboxgrind/bin/valgrind --vgdb=no --quiet --tool=sandboxgrind ${BINARY} 2>&${OUT}
The file Makefile.am
was totally not important and will be skipped here.
sandboxgrind.c
was a little bit important, it says that all syscalls
are blocked. What’s the most important - valgrind is not a sandbox tool, it’s purpose is
dynamic analysis of a binary.
Understanding valgrind
I was reading something that valgrind uses JIT to do the emulation, the
first step was to check out more closely how it works. My first idea was to attach gdb to check if there are some rwx
mappings.
To attach gdb to a process inside a docker add to Dockerfile following lines for root user:
RUN apt -y install gdb gdbserver
RUN apt -y install procps
Inside the docker container do gdbserver localhost:2001 --attach PID
.
On host do target remote localhost:2001
. Also add --privileged
and -p 2001:2001
flags to docker run
command.
Here is a script that is responsible just for compiling and sending our
code in sh.asm
file:
For the beginning I sent just an infinite loop, sh.asm file looks this way:
It turned out that there are rwx
regions and almost every mapping
is loaded onto the same address at every run:
gef➤ vmmap
[ Legend: Code | Heap | Stack ]
Start End Offset Perm Path
0x0000000000400000 0x0000000000401000 0x0000000000000000 r-- /tmp/tmp.JMDzf9HfCqe68kuGPvYTZ66alZkCtEUusV (deleted)
0x0000000000401000 0x0000000000402000 0x0000000000001000 r-x /tmp/tmp.JMDzf9HfCqe68kuGPvYTZ66alZkCtEUusV (deleted)
0x0000000004000000 0x0000000004001000 0x0000000000000000 rwx
0x0000000058000000 0x0000000058001000 0x0000000000000000 r-- /sandboxgrind/libexec/valgrind/sandboxgrind-amd64-linux
0x0000000058001000 0x000000005819b000 0x0000000000001000 r-x /sandboxgrind/libexec/valgrind/sandboxgrind-amd64-linux
0x000000005819b000 0x0000000058230000 0x000000000019b000 r-- /sandboxgrind/libexec/valgrind/sandboxgrind-amd64-linux
0x0000000058231000 0x0000000058236000 0x0000000000230000 rw- /sandboxgrind/libexec/valgrind/sandboxgrind-amd64-linux
0x0000000058236000 0x0000000058c29000 0x0000000000000000 rw- [heap]
0x0000001002001000 0x00000010026bc000 0x0000000000000000 rwx
0x000000100277c000 0x000000100297c000 0x0000000000000000 rwx
0x000000100297c000 0x000000100297e000 0x0000000000000000 ---
0x000000100297e000 0x0000001002a7e000 0x0000000000000000 rwx
0x0000001002a7e000 0x0000001002a80000 0x0000000000000000 ---
0x0000001002b71000 0x0000001003af1000 0x0000000000000000 rwx
0x0000001ffefff000 0x0000001fff001000 0x0000000000000000 rwx
0x00007ffff7ff9000 0x00007ffff7ffd000 0x0000000000000000 r-- [vvar]
0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rw- [stack]
Also our JITed code is loaded always in exactly the same addres. This is just our infinite loop:
Exploitation
At this point I started trying random things like overwriting
memory of the process.
I didn’t find any interesting pointers in valgrind that could give easy win
but I discovered that it is possible to write to various regions, for
example to the place where our JITed code is located, and to others rwx
regions.
Unfortunately it’s not possible to simply jmp to the address we wish because
this is blocked.
So the idea was to copy our shellcode to the place where JITed code starts - 0x1002d9d000
, and do jmp _start
.
The shellcode was copied succesfully but this exploit didn’t work because jmp _start
was not JITed to
jmp 0x1002d9d000
like in the first example, but to 0x1002d9d34b: jmp 0x1002d9d098
:(.
Maybe some optimalization or some other strange stuff.
But when I tried to copy the shellcode to 0x1002d9d098
, it also didn’t
work, I got such an error:
==13==
==13== Process terminating with default action of signal 4 (SIGILL): dumping core
==13== Illegal operand at address 0x1002D9D098
==13== at 0x401019: ??? (in /tmp/tmp.r00rA6FJaXTj83WFngvPpffs5SeWTaQDfo)
I was doing some tests with different addresses,
and sometimes I was getting the same error and sometimes my buff that I copied
was overwritten again by JIT to a proper code. Only copying
to 0x1002d9d000
worked.
I started to look how JITed code looks like. Basically copying code after JITing is huuuuge. And this is how it looks after my simplifications:
And every part of asm code has a call, or several calls to the place in a different mapping
Probably there are some checks that destroy my plans.
So finally, my last idea that actually worked, was to copy the shellcode
to the beginning of our mapping (0x1002d9d000
) and also
to overwrite only 5 bytes somewhere to jmp to 0x1002d9d000
.
But this place that we overwrite must be right after JITed code that
overwrites it - so that a function throwing errors won’t be executed. But
how to find that address? For now, let’s add following code (after the code copying shellcode) and attach gdb:
In gdb we see:
So we can now change mov rax, 0x1002d9d000
to mov rax, 0x1002d9d306
and 0xDEADDEAD
to the:
This is a change of only 2 numbers so our code will compile the same way and addresses will stay the same.
Here is the final code:
Finally I got the flag and I wonder how other people solved it because probably there are a lot of ways of doing it.