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
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:
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
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!
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.