zer0pts CTF 2021
pwn
A binary compiled with golang and its source code are given.
The program is a simple AES encryption service. It's pretty easy to spot the vulnerability.
The buffer obuf
is allocated at [0] by malloc. This pointer is supposed to be freed by the effect of defer at [1]. Basically, defer
function is called when the execution goes out of the scope.
Also, obuf
is freed at [2] and causes panic. This panic is captured in the main function and the program recovers.
In golang, defer has effect even if the execution went out of the function by panic. Because of this, the program may cause double free if len(keybuf) == 0 || len(ivbuf) == 0
.
Let's see how we can use the double free vulnerability. The binary is statically linked and the libc version is (old) libc-2.27, which we all love.
At [a] allocates a buffer for the context. This is our target to overwrite because EVP_CIPHER
has some function pointers. For example, EVP_CIPHER_CTX_reset
calls the cleanup function pointer.
Also, [b] allocates a buffer for the ciphertext. Since we can double-free a buffer in advance, we can overlap the context and the ciphertext buffer. Then, the fake cleanup function pointer is used by the effect of defer
and we can get RIP.
To get the shell, we have to do ROP because we don't have the address of libc, which means we can't call onegadget.
I used the ROP gadget shown below.
rbp
points to the buffer for EVP_CIPHER_CTX
.
Since golang uses the stack for passing the arguments, our ROP looks like this:
So, how can we prepare "/bin/sh" at a known address?
Golang uses its own runtime stack.
The address of the runtime stack is fixed for each machine. It doesn't change for each execution but changes for each machine. We can guess this address by just few hundreds times of brute force. The base address of the runtime stack is always 0xc000000000 in this case.