House of Force II
April 7th, 2023
Memory Allocation Hooks
It's time to "level up" our House of Force technique to make it a more considerable force to be reckoned with. First, we must talk about some of the Hooks for Malloc which you can find on the man pages, or from the link below:
From the blog above, we can see what these hooks do and where they could be useful:
Simply put, if we use a malloc
hook, we can have this hook run whenever malloc
is called! Don't believe me? Look at what the official GNU site says about the __malloc_hook
:
"The value of this variable is a pointer to the function that
malloc
uses whenever it is called." - GNU.org, Hooks for Malloc
We'll create an implementation, but first, let's discuss why we need to use these hooks in order to get our command execution. We're still going to be exploiting the house_of_force
binary from the first blog post.
Why Hooks?
Now, you might be wondering why we're using __malloc_hook
to get command execution, especially in the context of this binary but let's consider the following. We can't use the stack since there's still ASLR and we haven't been able to leak its address, so the stack isn't viable in this case. We could try to target the binary but we've already done this when we overwrote the target in the user data. We could mess around with structures like the PLT
or .fini_array
in order to get code execution.
Both the PLT
and .fini_array
structures are just writeable arrays of function pointers.
Some super high-level overviews of these two:
Procedure Linkage Table (PLT)
: Any function that the program calls that comes from an external library, like in this example,GLIBC
, is represented in thePLT
. This just dynamically resolves symbol names to the correct address. The reason why it remains writeable during the program's runtime is because of something called "Lazy Linking." Lazy linking/loading just makes it so that a function's address is only resolved when it's first called. Moreover, we could just overwrite theprintf
PLT
entry with the address of the code that we'd like to execute so that the next time the binary tries to callprintf
, it'll be our code that ends up getting executed..fini_array
: This is just an array of function pointers that are to be run once a program exits. If we overwrite.fini_array
slots, we could hijack the program's flow of execution and have it run whatever we want if we can get the program to exit.
Unfortunately for us, as great as these methods are, we can't use them either. If we recall the checksec
output of the binary, we can see that it was compiled with Full RELRO
:
This would make it unviable for us to use PLT
or .fini_array
because Full RELRO
would make these two structures read-only
after their initialization. Sh*t so the stack and the binary are out of the equation. So what now? Well... what about the heap itself? Well, it would be a good idea except we don't really have anything on the heap aside from our own data; no function pointers, or any useful data on the heap. So the heap's out. If we look at the program again we can see that we do have a libc
leak from the puts
function.
So this looks promising, now it's just a matter of figuring out what in libc
we can target to get command execution. Turns out libc
has a PLT
as well as two other structures called: __exit_funcs
and tls_dtors
which behave similarly to the .fini_array
. So, we could target those but even though the GLIBC PLT
is writeable throughout the lifetime of the program, triggering calls to the functions within it, is going to be pretty difficult. Furthermore, __exit_funcs
and tls_dtors
are protected by a thing called Pointer Guard which makes messing around with these structures pretty difficult as well. An awesome post on abusing exit handlers can be seen here:
"So what now?" x2
There actually is something we can use, that's also heap-specific as well! We can use the malloc hooks
! Every one of the core malloc
functions; such as malloc
, free
, realloc
, etc. has its own associated hooks! They take the form of a writeable function pointer in GLIBC
's .data
section. These are typically used by developers to do neat things like implement their own memory allocators or collect malloc
statistics. For us, however, we're going to finally get some command execution with them.
Hijacking the Hook
To start, we're going to use the exploit script template and change a few lines of code, at the end I'll program my own variation of the exploit template given to us, but for right now, we need to just understand this because it can grow quite complex. Here's the code after making some alterations to the stock exploit template:
Here, we can see that the script remains the same aside from the barred section where we overwrite the top_chunk
to hold a value of 0xffffffffffffffff
, and we get rid of the delta
function we used before since that function won't be needed here as we're not going to be wrapping around the VA
space anymore. We set the distance
to be the difference between the __malloc_hook
and the top_chunk
. We do __malloc_hook - 0x20
because we're trying to make the allocation stop just before the malloc
hook. Then in the latter part, we do heap + 0x20
to account for the 0x20
we already requested. We've also commented out the last malloc
call because we're trying to see what this does to our program before we try to actually leverage this. So, let's run this script with the GDB NOASLR
arguments:
And now, inside of gdb
we can tinker with this all we want. Let's start by breaking with ^C
and inspecting the memory around the __malloc_hook
:
We're doing a -2
instead of -16
because gdb
does pointer math depending on the type we pass it. So, let's see the output of this command:
So, the highlighted QWORD
is actually the malloc
hook! Remember that we talked about this and how it's a function pointer. So, if we populate this QWORD
with the address of a function, like system
, it'll run this every time malloc
is called! Another way you can find out where the __malloc_hook
resides is by passing &__malloc_hook
to xinfo
:
When the __malloc_hook
is NULL
like this QWORD
-> 0000000000000000
, then it doesn't do anything and malloc
works normally. When it isn't NULL
however, calls to malloc
will subsequently be redirected to the address that the __malloc_hook
holds. Your hacker senses should be tingling right about now.
Furthermore, if we run the top_chunk
command, we can see the following output:
This shows us that the top_chunk
has been put right here, right before our __malloc_hook
:
This has been perfectly placed in a manner where we can now just overwrite the __malloc_hook
with the next call we make to malloc
! Let's give this a try with an address of something like 0xdeadbeef
.
Now, let's run this script again with the GDB NOASLR
arguments. If we now try to use option one (1
) in the binary menu to allocate memory, we can see we're allowed to choose a size:
However, after pressing enter, the program hangs when in normal circumstances, it would ask us for the data we'd like to put into this allotted memory. This is happening because of the fact that we have overwritten the __malloc_hook
with an address of 0xdeadbeef
. So, since that's not a valid address, it's obviously crashed on us. And surely, if we look at our gdb
output:
The __malloc_hook
made it so that whenever malloc()
was called, it would instead run our code and as we can see by the fact that the binary crashed, and more importantly, by looking at the value in the RIP
register, we can see that indeed, this was the case. So, now all we need to do is find a viable function for us to overwrite the __malloc_hook
with so that we can get a shell! We can use a libc
function like system()
!
Command Execution
So, instead of 0xdeadbeef
, let's populate the malloc()
call in our script to be libc.sym.system
:
Now, every time there's a call to malloc()
, it'll be redirected to system()
. As many of you know, we need to set up the arguments for system()
before we call it. In this case, we to pass a shell to system()
, something like /bin/bash
or /bin/sh
, or whatever. From the man pages, we can see that system()
is a very simple function, it just takes a single argument, which is just a pointer to a string that will be interpreted as a command.
But where the hell will we find the address of the string "/bin/bash
" in memory? Well... Remember this part of our script?
We could just replace the Y
with our /bin/bash
.
We make sure it's null-terminated
. Furthermore, any arguments passed to malloc()
will now also be passed to system()
. So, we can use our string /bin/bash
in place of the size
portion of the malloc()
request and it'll be passed to system()
as an argument. Now, let's run this and find the offset of where this /bin/bash
string is going to be.
From the output of the command above, we can see that the /bin/bash
string lives really really early on in the heap memory. The heap starts at 0x603000
and the shell is at an offset of 0x30
, at 0x603030
. So, in our script, we can supply this offset as heap + 0x30
:
Now, if we run this program, it should yield us a shell!
Would you look at that? We got a shell! 😄🎉 I also made it a bit more logging-intensive just so see all the steps in action more:
References
Last updated