Guide to Reversing and Exploiting iOS binaries Part 2: ARM64 ROP Chains
Welcome back my masochistic kings and queens... This is PART 2 of how to reverse engineer and exploit iOS binaries.
By the end of this blog post you will be able to reverse engineer an arm64 iOS binary and exploit it in two ways – first through a buffer overflow, and then through a ROP chain. Once again, I will walk you step-by-step through the following:
- Building and compiling your own iOS binary
- Reverse engineering the binary
- Calculating the runtime function addresses without disabling ASLR
- Buffer overflow attack
- ROP chain exploitation
We will only be using FREE tools because I don’t like to spend money on nerd things. Therefore, for this blog post/tutorial, I have compiled and built you an iOS binary that you can use and abuse. I have also included the source code on GitHub for all my evil cheaters out there!!! Don’t think for a second that I don’t know you exist :P.
This blog post builds on Part 1’s knowledge of how to perform basic reversing on iOS apps. The difference is this blog post will focus on exploiting iOS arm64 binaries and we will take what we learn from reversing the binary to perform two attacks. As such, I will be introducing you to buffer overflows and ROP chain attacks.
This blog post is broken up into five sections, if you are familiar with my blog posts then this shouldn’t be a surprise. Anyway, these are the sections:
- High level walk through of the steps we will take
- Environment set up
- Introduction to the binary we are exploiting
- Arm64 buffer overflow
- Arm64 ROP Chain
- A selfie of me uwu
Below are the high-level step-through of what this blog post will cover. We will go through each little step below in detail.
- Upload the iOS binary onto your jailbroken iOS via SFTP. If you don’t know how to jailbreak your phone, please refer to unC0ver guide.
- Run the binary to test it works
- Find the buffer overflow vulnerability
- Check the iOS crash logs to get the position of the link register (LR) so we can craft the payload
- Debug the binary to find interesting functions that we will jump to in the payload
- Calculate the function addresses at runtime (ASLR enabled on iOS)
- Execute the buffer overflow attack
- Find the next function we want to use for ROP chain
- Execute a ROP chain
These are the tools we will use to perform the analysis:
- LLDB - https://lldb.llvm.org/
- Radare 2 - https://rada.re/n/
- Hopper Disassembler (DEMO Version) - https://www.hopperapp.com/
- Your jailbroken iPhone (I am using an old phone with iOS 14.1)
- Your capable and massive brain
In order for you to follow this walkthrough step-by-step, make sure you have your jailbroken phone ready with SSH / SFTP set up and connected on the same LAN.
Step 1: Download the vulnerable binary from my GitHub page
Please only download the dontpopme binary and NOT the source code. We are going to perform this entire exercise as a black box attack.
Step 2: SFTP the binary onto your jailbroken iOS device to the /var/mobile directory
Use chmod 777 dontpopme :)
How do you know the system is little endian or big endian? And how do you know what other controls are enabled? Let me show you a command you can run on your iOS device :) As depicted in the screenshot below, you can see "endian" is "little".
- aaa – to analyse the binary
- afl – lists all the functions that exist in the binary
- s sym._runCode
- runCode() – executes the command “/bin/ls -la”
- change() – does not execute code but changes global variables to “/bin/uname -a”
First get the leaked address of main from the program. Don't exit the program because the address will change each time. Your address will probably be different to mine :)
To calculate the slide, perform this calculation:
To calculate the real address position of the runCode() function at runtime do this:
- change() – Changes two global variables to “/bin/uname” and “-a”
- runCode() – Executes execl() passing in the two global variables
- Leaked Main Address
- Change() entry address
- runCode() entry address
- Debugger Main Address
- Debugger Change() address
- Debugger runCode() address
The static address for change() is shown below as 0x100007c50. We don’t want to gather the address of the entry but rather the second instruction (highlighted). I did this using radare2 using the instructions I provided in the Buffer Overflow section:
- change() entry point: 0x102527C50
- runCode() entry point: 0x102527C88
- Address of change() - \x50\x7c\x52\x02\x01\x00\x00\x00
- 8 bytes of whatever - \xff\xff\xff\xff\xff\xff\xff\xff
- Address of runCode() – \x88\x7c\x52\x02\x01\x00\x00\x00