Heap Overflows on iOS ARM64: Heap Spraying, Use-After-Free (Part 3)


Welcome back to part 3 of my iOS arm64 exploitation series! 

If you’ve missed the blogs in the series, check them out below ^_^
Part 1: How to Reverse Engineer and Patch an iOS Application for Beginners
Part 2: Guide to Reversing and Exploiting iOS binaries: ARM64 ROP Chains
Part 3: Heap Overflows on iOS ARM64: Heap Spraying, Use-After-Free 

If you’re more of a visual learner – I have filmed a YouTube video on this that you can check out! The YouTube video does not go into the same level of depth as this blog post will, so just keep that in mind.


INTRODUCTION
We’ve gone through iOS hooking, buffer overflows and simple ROP chains on ARM64. Now it’s time to talk about heap overflows and exploiting use-after-free (UAF) bugs. The goal of this blog is to show you how a UAF bug can be exploited and turned into something “malicious”. I will be walking you through step-by-step the following things:

  • How to identify a UAF bug
  • How to statically analyse the binary to figure out how to perform the exploitation
  • Heap overflow logic
  • Heap spraying
  • Full exploitation

As always, we will be using open-source tools to perform this and I have compiled, signed, and uploaded the exercise binary that we will use for this exercise. I have also uploaded the source code for those of you that want to read it. This is all available for you to download on my Github:

Download exercise binary “moneymachine” from GitHub here

This blog has been broken down into five parts:

  • High level walkthrough of the steps
  • Introduction to the exercise binary
  • Static analysis of the binary using Radare2
  • ARM64 heap overflow
  • Resources 

HIGH-LEVEL STEPS

These are the high-level steps we will take during this exercise, and I will explain each one in detail. If detail isn’t your kinda thing, then just peep my YouTube video:

  1. Upload the binary to your jailbroken iOS device via SFTP to the /var/mobile directory. I am running a jailbroken iOS 14 device called “honeypot”
  2. Find the UAF vulnerability by playing around with the program
  3. Statically analyse the binary using R2 to locate any meaningful functions that we can manipulate
  4. Set up a remote debug session on the iOS device using LLDB
  5. Debug the binary and set breakpoints after memory allocation to fetch the heap address where the objects will be allocated to
  6. Perform heap spraying by populating areas of the heap until all the free lists are emptied and the only “free gap” to write to is the initial freed object on the heap
  7. Perform the heap overflow 
  8. Invoke a function call to trigger the payload 


INTRODUCTION TO THE EXERCISE BINARY

This binary is called “moneymachine” and it’s a MACH-O 64-bit binary. It was inspired by the internet and all my weird friends who grew up playing Runscape! Lol. The name of the binary is a reference to the 100 gecs iconic meme song “money machine”

Anyway, when you download the binary and run the binary, there are 6 option to choose from (as pictured below). 

The goal of the exercise is to exploit a heap overflow and perform heap spraying until you can buy the legendary item! There is an embedded use-after-free vulnerability coded into this binary. Please note, no security controls need to be disabled for this exploit to work. ;)

When you select [2], the program will call ‘malloc’ and allocate memory on the heap for your quest (in this instance, Goblin diplomacy). The “quest” is a struct – an object which stores the name of the quest along with a function pointer to a function which lists all the available quests [3].  


The call to [5] will invoke the ‘free’ function which will free the object allocated on the heap. 


However, this object has not been nulled meaning the data is still residing on the heap – which is why, when you call [3] after the object is freed, it will still return all the available quests.  The goal here is to go from a broke traveller to a rich traveller. 


And then after we exploit the moneymachine, we will become rich enough to be the owner of the largest and most magnificent sword in the world. To do this, you must perform a heap overflow, heap spray, and identify a secret function we need to overwrite the function pointer on the heap with. 




STATIC ANALYSIS USING R2 AND LLDB
Let’s look at the binary and perform some static analysis UwU
 
1. Examine all the functions within the program
To do this you can use Hopper, Radare2 (https://github.com/radareorg/radare2) – whatever diassembler you want to use. I am going to use radare2 to perform this step. All you need to do is to run the following commands:
  • r2 <binaryname>
  • aaa – to trigger r2 to analyse the flags, function calls, bytes etc
  • afl – lists all the functions 
From the screenshot below you can see some standard C functions and three non-standard functions highlighted in purple. 


2. Look at buyItems function to see what it does
The goal of this exercise is to figure out how you can afford to buy the legendary item – so based on the name of this function, we should probably prioritise it for analysis. Hopefully it might help us understand the logic behind how to buy items within this binary. To examine this function type in:
  • s sym._buyItems
  • pdf

Straight away, in the code we can see comparison happening leading to two jumps. Let me break down the assembly into plain English:
  • First, we notice there is a variable called “sym._money”, we can guess variable may be storing how much gold we have 
  • The value of “sym._money” is stored in the w8 register
  • The function moves the value of 999999 into w9 register (as highlighted in purple). This is represented as 0XF423F in hex.
  • There is a comparison being done between w9 and w8 (comparing our current money with the value of 999999)
  • The program branches to two different locations depending on if the amount of money we have is greater than or equal to 999999 (b.ge), otherwise it jumps somewhere else
We don’t really need to analyse the rest of the code to understand logically why this is useful. It’s likely at this point that because we are broke with 0 gold, we will not be able to buy the legendary item. It’s also likely that the legendary item costs 999999. 
 
I pulled the binary into Hopper (as it does a better job of showing strings than r2 visually) to show you where the function branches to based on the comparison. The first location is if the comparison fails and we have less than 999999 gold – it will print “you do not have enough money”. 

The second location shows you what happens if you do have 999999 or more gold. It prints “item acquired”.  


Now with this knowledge, it’s immediately clear we need to find a way to get more money to exploit this program.

3. Analyse the makeItRain function
Next, let’s look at the makeItRain function to figure out what it does. As you can see in the picture below – once again after the function prologue, it interacts with the “money” variable (sym._money). 
  • Stores the value of sym._money inside the w8 register
  • Stores the value of 0xF4240 in w10 register which corresponds to the value “1000,000”
  • Adds the value from w10 register to the w8 register
  • Prints something 
From this we can deduce that this function increases the value of “money” by 1,000,000.  


4. Analyse other functions
Looking at the other functions called within the program we can see references to these functions:
  • malloc – something is allocating memory on the heap
  • free – something is being freed on the heap 
  • fread/fopen – something is being read from a file

5. Find the use-after-free bug
When we run the program, we notice something interesting. When we call [3] for available quests before allocating a new quest [2], a segmentation fault happens. A segmentation fault doesn’t occur after we get a new quest [2]. 


However, after we free the quest with option [5], and then we call [3] available quests, the data is still referenced. This is the premise of a use-after-free bug where the data is still resident on the heap and that memory address is still being referenced by another function.  

If we look at the source code, we can see below that the freeing of the quest object on the heap does not include nulling the data out! It’s this bug which causes the use-after-free bug. 



HEAP OVERFLOW WITH HEAP SPRAYING
Now that we have statically analysed the binary, we have an idea of how we should proceed with the exploitation:
  • Allocate a new quest on the heap with option [2]
  • Free the new object on the heap [5] 
  • Read data from a file by calling [4] to import items from a file
  • Use the data from the file to overwrite the original quest heap object
  • Overwrite the function call to listQuests() on the heap with the address of makeItRain()
  • Trigger the use-after-free by calling [3] available quests 
  • See if the exploit was successful by calling [1] to buy legendary items
To perform these steps, we need to gather some information and ensure a few things:
  • Get the heap address where the quest is allocated to
  • Get the heap address where option [4] is writing the file contents to
  • Ensure that the two heap addresses are the same, otherwise perform heap spraying
1. Remotely connect to your iOS where the binary is running with LLDB
On your iOS device, make sure to have two terminal sessions open, one which is running the binary, and another to start a debugserver session attaching it to the running process. Pass into debugserver the IP address of the workstation you’ll use to perform the remote analysis


On your workstation, open LLDB and connect to the running process. 


2. Disassemble the main function 
The first thing we are going to do is to analyse the main function. The purpose of analysing the main function is so we can get an idea of what it is doing and work out where we should set some breakpoints.  To do this you just type the command:
  • disass -n main
Please note all your addresses will be different to mine :)


3. Set breakpoints for the purpose of working out where we are writing to on the heap
The heap address where malloc allocates to will be pushed into the x0 register in ARM64 right after the function call to malloc. Therefore, by breaking right after the call to malloc, and by inspecting the registers, you will be able to pull the heap address that we are writing to ^_^

Therefore, at this stage, we will scroll through the main function and copy down the addresses where we will set breakpoints at. The first breakpoint happens early in the program (likely where the call to [2] New quest is done). 


The second malloc is later in the main function right after the contents of a file ‘items.txt’ is read and then stored onto the heap. 


We are also going to set a breakpoint right after the call to the “free” function. This is so we can double check that this UAF exists. If there is a UAF bug then the data on the heap will still remain after the call to free as it won’t be nulled. 


To set the breakpoints, just run the following commands:


4. Hit the first breakpoint and gather the address of the heap from the first malloc
As the program execution is currently paused, we need to continue the process and then select [2] to allocate a new quest. This should immediately then hit our first breakpoint at the first malloc:


Now, we need to examine the register and pull the address of the heap. This can be done by typing “register read”. As you can see below the heap address where our quest “Goblin Diplomacy” will be allocated to will reside at the address “0x0000000104b06ce0”. 


Looking at the heap address right now, you will see there’s not much sitting there on the heap. This will be important later as we compare what happens when we get data written to the heap. 


5. Free the heap allocation by calling [5] 
Continue the process execution by typing “process continue” and then call option [5] to free the allocated data on the heap. 


6. Check the data on the heap
Pause execution by typing “process interrupt” and then observe what is written to the heap. As you can see below there has been data populated in the highlighted purple region. As expected, the data written there corresponds to “Goblin Diplomacy” in hexademical followed by the address of the listQuests() function at 0x104b06d00. The important thing to note here is, even though the heap has been freed, the data is still resident here instead of being nulled! This is the premise of the bug. 


Just to really demonstrate my point, the values on the heap correlate exactly to “Goblin Diplomacy” in hex but in little endian format :)



7. Prepare payload to overwrite the heap
In order for us to prepare the payload, we will need to put data into a “items.txt” file before we invoke a call to option [4] to read items from a file. Let’s use this as an opportunity to calculate the offset where we need to overwrite the function pointer address with the makeItRain() address. 


After piping all the data into a “items.txt” file. I then SFTP’d it into the same directory as the binary on the iOS device. 


8. Get the payload heap address and check it’s the same as the first heap address
Now let’s trigger the payload and see where this new data will be written to on the heap. We do this by calling option [4]. 

This will hit our second malloc breakpoint. We will do the same step of calling “register read” to pull the new heap address off the x0 register. This heap address is 0x0000000102906080 which is different from the first heap address of 0x0000000104b06ce0. 


9. Heap spraying
On later versions of iOS, even if two objects being allocated on the heap are the same size, the second object may not be allocated to the same location as the first freed object. This is due to how the heap manager manages free space on the heap. A great explanation on how this works is on Azeria Labs blog (https://azeria-labs.com/grooming-the-ios-kernel-heap/). 

How this works is, on the heap, there might be several gaps where the new malloc’d object will be written to. Our goal here is to keep filling in these gaps with data to increase the chance that our data will be written to the attacker freed area on the heap. To do this, you just keep spraying the heap and fill all the free gaps with objects until our object is written into the desired heap section. 

In the picture below, this is a *VERY* rough representation of our current heap state. I simplified this so it’s easier to understand. Basically, we have free gaps and our freed quest object is sitting on the heap. However, our items.txt was written to another freed gap on the heap, at a different address. 

Therefore, to perform the heap spraying, we need to fill out the heap until only freed area in the zone which can accommodate the size of the malloc’d items.txt object is the original freed quest object address.  


Therefore, in our program example, we will just keep calling the [4] import items call to malloc until we get our items.txt allocated to the only gap in the zone left – the same address as the freed quest object. This program is a more simplistic example of this as I coded it so that the two objects are the same size. 

10. Call [4] import items again
The number of times you may have to call [4] will vary, but you need to continue the process and keep calling it and checking the x0 register until we hit the same address on the heap as the original object at 0x0000000104b06ce0. As you can see on the second iteration of the program, we have had our second malloc set to the same address - 0x0000000104b06ce0.  


At this point, we will hit ‘process continue’ to allow the heap to populate. As you can see from the screenshot below, we have overwritten the heap with the hex representation of “AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDDEEEEEEEEFFFFFFFF”. 


Highlighted in red, is the hex of “EEEEEEEE”. This is the location of where “listQuests()” resided on the original heap when we allocated the quest data. This means when we edit our payload, we need to replace this with the address of the “makeItRain()” function.


FULL EXPLOITATION
Now that we know we need to replace the EEEEEEEE in our payload with the address of the makeItRain() function. We are ready to perform the full exploit. Let’s get it!

Step 1: Rerun the binary
We are going to start a fresh debug of the running binary. So just load up the binary again and connect to it with your remote workstation where you will be performing the exploitation.

Step 2: Repeat steps 2-4 in the heap overflow section
Once again, we need to set breakpoints at the address right after the two calls to malloc so we can ensure that we are writing to the same heap address. For the actual exploitation you can ignore the third breakpoint set after the “free” function call. If the heap address is different, just remember you may need to heap spray, I have rarely ever seen it hit the same address straight away.

Step 3:  Get the runtime address of the makeItRain() function
We need to change the payload to replace EEEEEEEE with the address of the makeItRain function at runtime. Therefore we need to disassemble this function and get the entry address which is 0x1028637bc. 

To set the breakpoint you just need to run:
  • br s -a 0x1028637bc

Step 4:  Replace the payload
Currently our payload looks like this:
\x41\x41\x41\x41\x41\x41\x41\x41\x42\x42\x42\x42\x42\x42\x42\x42\x43\x43\x43\x43\x43\x43\x43\x43\x44\x44\x44\x44\x44\x44\x44\x44\x45\x45\x45\x45\x45\x45\x45\x45\x46\x46\x46\x46\x46\x46\x46\x46

We need to replace the \x45\x45\x45\x45\x45\x45\x45\x45 section with the little endian version of the address of makeItRain 0x1028637bc.

This turns the payload into this:
\x41\x41\x41\x41\x41\x41\x41\x41\x42\x42\x42\x42\x42\x42\x42\x42\x43\x43\x43\x43\x43\x43\x43\x43\x44\x44\x44\x44\x44\x44\x44\x44\xbc\x37\x86\x02\x01\x00\x00\x00\x46\x46\x46\x46\x46\x46\x46\x46



Step 5: Read from items.txt 
Before you read from the file make sure you have allocated a new quest [2] and freed it [5]. Then do a call to [4] import items checking that the heap address is the same. If not, then you need to heap spray until it is. 



Step 6: Check the heap and the payload on the heap
I like to just interrupt the process and ensure that the address is written properly to the heap. You do this by running:
  • process interrupt
  • x/64 0x0000000102a04510
As you can see from the screenshot below we have successfully overwritten the function call area of the heap with our makeItRain() function. 


Step 7: Make it rain baby!
Now it’s time to trigger the exploit. To do this you just need to call the [3] available quests function and it will jump to the makeItRain() address. As you can see below, this is successful!


Now all there is to do is to try and get our legendary item UwU. As you can see below, this is a success! Heap overflow, UAF abuse with heap spray complete!



Other Resources
I noticed there are not many resources specifically dedicated to arm64. There are a few on arm32, but most of the exploitation write-ups are focused on x86. I recommend for further study you take a look at the following resources below T_T



Comments

Popular posts from this blog

How to Reverse Engineer and Patch an iOS Application for Beginners: Part I

Forensic Analysis of AnyDesk Logs

How to Investigate Insider Threats (Forensic Methodology)