VolgaCTF had only three pwnable challenges that were base on the same binary.
Their idea was to increase the difficulty little by little by adding security
features at each phase:
The first one had neither ALSR nor NX activated
The second one had no ASLR, but NX was activated
The third one had ASLR and NX activated
However all of them had stack canaries.
Here is the write-up of the first one, the other two will follow shortly.
Basic information
From the organizers:
The binary is a stripped ELF 64bit with only canaries activated:
Operation
The binary starts by asking a name and then propose ten additions to ensure
that the client is used by a human:
If the ten additions are correctly answered, the service proposes to create
papers with various attributes. That part is not important for the first two
binaries because the exploitation is done beforehand.
Vulnerabilities
There are at least two vulnerabilities:
string format on the name;
stack buffer overflow on the responses to the ten additions.
The string format vulnerability could be used to overwrite the saved rip
pointer, but a more simple approach is to use the string format vulnerability
to leak a stack address and the canary and then use the stack buffer overflow
to send the shellcode, rewrite the canary and overwrite the saved rip pointer
to the address of the beginning of the shellcode.
Exploitation
String format
When asked for a name, formats can be injected into it. When the name is
printed using printf, it will be passed as the first argument format,
the formats injected will be interpreted and information on the stack will be
leaked. More information
For example to leak data as pointers:
To leak the canary, 43 %p are needed and to leak a stack address, 46 are
needed:
Stack buffer overflow
The gets function reads a line (i.e a string terminated by a carriage return)
into a buffer passed as argument. The problem with that function is that no
size is given to limit the amount of data to be copied, therefore it is
possible to write passed the end of the buffer to the saved rip pointer. When
the execution returns from the current function, it will continue execution at
the address that has been written over saved rip and the control flow can
therefore be hijacked.
The calling convention for Linux on x86-64 architecture shows that
the first argument passed to a function is stored in the register rdi. The
call to gets is done at 0x40092b. If we break execution in a debugger, we
would be able to read the address of the buffer.
Here is a quick look of the function that asks for the response to the
addition:
Instead of injecting 46 %p, I set the argument number in the format:
The address of the buffer here is 0x7fffffffdc20 (it might differ on your
system but this is not important). Note that the canary is equal to
0x9ce2bb4ec57b1300 and the leaked stack address 0x7fffffffdce0
Let’s have look at the layout of the stack:
Saved ripis at 0x7fffffffdcc8. If we go up the stack, we can see the leaked
stack address at 0x7fffffffdcc0 and the canary at 0x7fffffffdca8, which give us:
distance between the leaked stack address and the beginning of the buffer:
0x7fffffffdce0 - 0x7fffffffdc20 = 192 bytes
distance between the beginning of the buffer and the canary:
0x7fffffffdca8 - 0x7fffffffdc20 = 136 bytes
Payload creation
Here is a summary of all the steps:
inject formats into the name to leak data on the stack
get the canary and leaked stack address
answer the 9 additions (no need to answer them correctly)
construct the payload and send it as the last addition response
the binary will return on the address of the shellcode
The payload is built as follow:
136 bytes containing the shellcode and some padding
canary
24 bytes of padding
leaked stack address minus 192
Here is the full python script that uses pwntools library:
And the exploitation:
Well ASLR could have been activated, it would not have changed anything…