Debugging Stripped Binaries and Bypassing ptrace

Introduction

Hello and welcome back! Today we are going to talk about debugging stripped binaries. Additionally, I'm going to show you how we can bypass ptrace with a debugger. To demonstrate this, we are going to use the secret flag binary from an earlier challenge. This binary was stripped of it's symbols and also uses ptrace as a means to stop us from debugging the program. In this blog post, I'll show you how we can find the main function in gdb and bypass this anti-debugging measure.

Finding Main

The first thing we need to do is find the main function. If we try to set a breakpoint at main when we initially load the binary we'll get an error.

Function "main" not defined error
Function "main" not defined error

This is obvious because the main function and other function names have been stripped from the binary. What we need to do first is find the entry point. This can be done by running info file. This will print all the symbols in the binary as well as the entry point. The entry point isn't main necessarily. This is just where code begins which isn't actually main. The main function is actually called as an argument to another function! But, we won't talk about that today. Let's move on.

Finding the entry point of the binary
Finding the entry point of the binary

GDB tells us the entry point for this binary is 0x400810. Let's examine 15 instructions from this address. That is x/15i 0x400810.

Examining 15 instructions from the entry point
Examining 15 instructions from the entry point

This isn't exactly a function prolog that we are used to seeing. But, we do see a function being called, __libc_start_main@plt. This function performs all of the necessary initialization for the main function. To put it simply, this calls the main function. In fact the address of the main function is it's first parameter. We know from experience that the first parameter in a 64-bit binary is stored in the RDI register. Well, we see another address being MOV'd into the RDI register. This is the address of the main function! We've successfully found the main function! If this were a 32-bit program, we'd see PUSH instructions before __libc_start_main@plt was called. Recall on 32-bit programs, arguments are passed on the stack. Now that we are armed with the address of main, let's set a breakpoint there and run the program!

Breaking at the main function
Breaking at the main function

Alright we did it! We are at the main function. To confirm, you can open this in IDA, navigate to this address and you'll notice the instructions are the same! We can see the code in the code section of this image but let's examine, say, 50 instructions from the main function.

50 Instructions from main
50 Instructions from main

Alright great! If you read the blog for the secret flag challenge or watched the YouTube video, the disassembly should be very familiar. We can step through this program without an issue.

Bypassing ptrace

Now, we know eventually this program will call ptrace, which will inhibit us from debugging any further. Let's set a breakpoint when ptrace is called. Simply type, b ptrace and type continue or the shortcut c.

Breakpoint in the ptrace function
Breakpoint in the ptrace function

We successfully set a breakpoint and we are now inside the ptrace function. We can step through this function but it won't do us much good. Let's allow the function to finish by typing finish.

ptrace finished execution
ptrace finished execution

After ptrace finishes, it returns with a value of -1 (0xffffffffffffffff in hexadecimal). The very next instruction compares the return value (RAX) to -1. If they are equal you see that the program exits with a code of 1.

Program exits with a code of 1
Program exits with a code of 1

We also see some random output which is what we saw during normal program execution but we don't see the "Are you sure it's the right one." Alright so how can we bypass this so we can debug this? Well, it's quite simple! We are in a debugger and we can set the registers as we see fit! So, after ptrace gets called we can set the RAX register to 0. That way when it gets compared to -1, we'll be able to perform the jump to 0x400AE8.

Restart the program and allow the ptrace function to run. After it finishes set RAX to 0. I'll demonstrate below. You can verify the change by running info registers.

Setting RAX to 0
Setting RAX to 0

Now, we can continue debugging the program. Perform two step instruction, si, and you'll notice that we jump to address 0x400AE8.

Performing a single step instruction
Performing a single step instruction

Notice this time we are going to jump to the address because RAX is not equal to -1. Performing another step instruction will land us firmly at the address we wanted to reach.

Performing another step instruction lands us at the instruction we want to reach!
Performing another step instruction lands us at the instruction we want to reach!

There you go! We have bypassed this simple anti-debugging technique! Another option is to change the call to ptrace to NOPs. This is probably a better option since you don't have to manually set registers. Let's try it out!

Nopping out the ptrace Function Call

Let's start by opening the binary up in IDA and navigating to a place where ptrace is called. For this binary that's here:

ptrace function call
ptrace function call

So, just like we did in the blog, let's find out what bytes are responsible for calling ptrace. Highlight the call _ptrace and switch to the Hex View.

Bytes responsible for ptrace function call
Bytes responsible for ptrace function call

Now, let's open the binary up in a hex editor, find the bytes and change them to NOPs (\x90).

Searching for the bytes
Searching for the bytes

The bytes we want to modify are highlighted below.

Bytes to modify
Bytes to modify

Now, change all of the highlighted bytes to 90 and save it as secret_flag.bin.debug.

Modified bytes
Modified bytes
Saving changes
Saving changes

To confirm these changes, let's open up our modified binary in IDA. If we were successful, the call ptrace would be changed to 5 NOPs.

Successfully NOP'd out call to ptrace
Successfully NOP'd out call to ptrace

Now, let's try and debug this again. We should see that we have no problems debugging! Let's set a breakpoint just before ptrace was originally called, 0x400ACE.

Breakpoint before NOP'd ptrace function call
Breakpoint before NOP'd ptrace function call

We can step through these instructions just fine and we will be able to continue debugging the program! Now, I know what you might be thinking. This is just one function call. If you recall, ptrace was called a few times. Wouldn't it be easier to just NOP out ptrace entirely? Unfortunately, it's not that simple. The ptrace function is inserted into the binary at runtime with the help of dynamic linking. So, we cannot simply NOP out the entire function globally. We have to do it on a case by case basis.

Conclusion

That's all I have for this post! I hope you enjoyed reading and that you learned something new! If you have any questions feel free to hit me up on Twitter, Instagram, or Discord: jaybailey216#6540. If there is a challenge you want me to try or a tutorial you would like, feel free to shoot me suggestions! I'll see you all next time!

Peace out! ✌🏾

Show Comments