A Single Partner for Everything You Need Optiv works with more than 450 world-class security technology partners. By putting you at the center of our unmatched ecosystem of people, products, partners and programs, we accelerate business progress like no other company can.
We Are Optiv Greatness is every team working toward a common goal. Winning in spite of cyber threats and overcoming challenges in spite of them. It’s building for a future that only you can create or simply coming home in time for dinner. However you define greatness, Optiv is in your corner. We manage cyber risk so you can secure your full potential.
Breadcrumb Home Insights Source Zero Walkthrough of an iOS CTF August 25, 2020 Walkthrough of an iOS CTF Capture the Flags (CTFs) and crackmes for iOS applications aren’t as common as they are for Android, so here’s a helpful (and fun and quick) review of the general steps I took to solve one. A quick walkthrough of the general steps I took to solve the challenge and obtain the flag follows. (The walkthrough assumes the reader is already familiar with using Frida as well as a basic understanding of disassembly.) After downloading the IPA, resigning and installing it to your device, a single screen will display when running: The app takes the flag as input and will display a message to let you know if the value was correct after tapping "Submit." Incorrect attempts or running on a jailbroken device result in an error message. Jailbreak Detection Bypass This was the first step and was not too difficult. After decompressing the ipa and loading the main binary in Hopper, we can quickly see some references to jailbreak detection routines such as the start of the one shown here: +[JailbreakDetection isJailbroken] : 0000000100005fdc sub sp, sp, #0x50 ; Objective C Implementation defined at 0x100009220 (class method), DATA XREF=0x100009220 0000000100005fe0 stp x24, x23, [sp, #0x10] 0000000100005fe4 stp x22, x21, [sp, #0x20] 0000000100005fe8 stp x20, x19, [sp, #0x30] 0000000100005fec stp x29, x30, [sp, #0x40] 0000000100005ff0 add x29, sp, #0x40 0000000100005ff4 adrp x23, #0x100009000 0000000100005ff8 ldr x0, [x23, #0x478] ; objc_cls_ref_NSFileManager,_OBJC_CLASS_$_NSFileManager 0000000100005ffc nop 0000000100006000 ldr x19, =aDefaultmanager ; "defaultManager",@selector(defaultManager) 0000000100006004 mov x1, x19 0000000100006008 bl imp___stubs__objc_msgSend ; objc_msgSend 000000010000600c mov x29, x29 0000000100006010 bl imp___stubs__objc_retainAutoreleasedReturnValue ; objc_retainAutoreleasedReturnValue 0000000100006014 mov x21, x0 0000000100006018 nop 000000010000601c ldr x20, =aFileexistsatpa ; "fileExistsAtPath:",@selector(fileExistsAtPath:) 0000000100006020 adr x2, #0x100008238 ; @"/Applications/Cydia.app" However, the app also performs a couple of other checks that I needed to work around as well; notably, the app makes a call to the [CriticalLogic bat] method. I did not fully understand the purpose of this but did manage to hook the return value. There are many tutorials on using tools such as Frida to bypass jailbreak and root detection and I will not go into too much detail on the methodology here. The required hooked methods are shown in the Frida script provided at the end of this article and include comments. Reversing Engineering the Flag When I use the Frida script to start the app, it no longer complains after I tap "Submit" so we can now see how the submitted data is handled. A little bit of reverse engineering and we can see that the fun starts at a branch call to the function at 0x100005e34 from within the [CriticalLogic b:] method: 0000000100005ad4 mov x22, x0 0000000100005ad8 bl sub_100005e34 ; sub_100005e34 After examining the function at 1005e34(), we can see that the user-supplied guess is passed to it, but also the value after the first six chars is used later on for hashing (more on this later). If we look at the control flow graph for 10005e34() it gets a bit complex as shown in the following image (don't worry about the exact instructions for now, just note the overall structure of the graph): I have done a few CTFs previously and have come to recognize this "waterfall" shape as being characteristic of a function that makes a series of checks and modifications to a given input – usually involving custom XOR or mathematical routines. Should any of the values fail a check, the function leaves the "waterfall" and exits. While it is possible to manually try and reverse-engineer what input value is required to arrive at a given address in the function, it is also a good candidate for concolic analysis using something like angr, which is much faster. After using lipo to extract the 64 bit macho from the main executable, we can use angr to solve for the required input to the function at 0x10005e34 using the following angr Python script: import angr function_start = 0x100005e60 function_target = 0x100005fac function_avoid = [0x100005fb0] proj = angr.Project('iOweA**.arm64') state = proj.factory.blank_state(addr=function_start) arg = state.solver.BVS("input_bytes", 8 * 6) x0 = state.solver.BVS('x0', 64) x8 = state.solver.BVS('x8', 64) state.regs.x0 = x0 state.regs.x8 = x8 # pick an arb memory location bind_addr = 0x1000 state.memory.store(bind_addr, arg) state.add_constraints(state.regs.x0 == bind_addr) for byte in arg.chop(8): state.add_constraints(byte >= '\x20') # ' ' state.add_constraints(byte <= '\x7e') # '~' sm = proj.factory.simulation_manager(state) sm.explore(find=function_target, avoid=function_avoid) found_state = sm.found[0] print('First 6 chars: {}'.format(found_state.solver.eval(arg, cast_to=bytes))) print('Min len (x8): {}'.format(found_state.solver.eval(x8))) which prints: First 6 chars: b'winni ' Min len (x8): 14 So now we know the first six characters of the flag as well as the total flag length. Note that angr is telling us the input length including the null char so we have determined: Flag[0..5] = 'winni ' Flag[6..12] = unknown (7 chars) At this point, because of the donkey displayed by the app and the reference to winni at the start of the flag, I was fairly certain we would be dealing with a reference to Milne's Winnie-the-Pooh. Also note that angr is telling us just one possible solution using the providing constraints (which included a space character) and I suspected that 'winnie' could also be a valid start to the flag. Before trying my guess for the remaining seven characters, I first wanted to see what the app does with them. During my testing I hooked the calls to CC_SHA256 and CCCrypt but hooking the ObjC [CriticalLogic AES128Operation:data:key:iv:] method works as well. Observing the values passed to and returned by these functions revealed that the user input after the first six characters is first sha256 hashed and then compared with the (AES) decrypted string that is hardcoded in the app. Hopper pseudo code of the decompiled [CriticalLogic b:] method shows this process nicely. When called, arg2 points to the sha256 hash of the user provided input (less the first six chars) and is assigned to x19. Near the end of the function we see the hash of the user's input being compared to the decrypted hardcoded string: /* @class CriticalLogic */ -(bool)b:(void *)arg2 { r31 = r31 - 0x70; var_50 = r28; stack[-88] = r27; var_40 = r26; stack[-72] = r25; var_30 = r24; stack[-56] = r23; var_20 = r22; stack[-40] = r21; var_10 = r20; stack[-24] = r19; saved_fp = r29; stack[-8] = r30; r29 = &saved_fp; r20 = self; r19 = [arg2 retain]; if (sub_100005c5c() != 0x0) { r20 = 0x0; } else { r21 = [[NSMutableData alloc] init]; if (objc_msgSend(@"7c537a22bc6e1f979ac26341125c30d2eba190d2b003aff7a89454525932d4d8617c7797a1cfe20810cc860cff996373", @selector(length)) >= 0x2) { r27 = 0x0; r25 = 0x1; do { r24 = @selector(length); [@"7c537a22bc6e1f979ac26341125c30d2eba190d2b003aff7a89454525932d4d8617c7797a1cfe20810cc860cff996373" characterAtIndex:r25 - 0x1]; [@"7c537a22bc6e1f979ac26341125c30d2eba190d2b003aff7a89454525932d4d8617c7797a1cfe20810cc860cff996373" characterAtIndex:r25]; strtol(&var_54, 0x0, 0x10); [r21 appendBytes:&var_51 length:0x1]; r27 = r27 + 0x1; r0 = objc_msgSend(@"7c537a22bc6e1f979ac26341125c30d2eba190d2b003aff7a89454525932d4d8617c7797a1cfe20810cc860cff996373", r24); r25 = r25 + 0x2; } while (r0 >> 0x1 > r27); } r22 = [[r20 AES128Operation:0x1 data:r21 key:@"Bmrb5WBcWgLXRyjJ" iv:0x0] retain]; r20 = [r19 isEqualToData:r22]; [r22 release]; [r21 release]; } [r19 release]; r0 = r20; return r0; } Hooking either the [CriticalLogic AES128Operation:data:key:iv:] method or CCCrypt allows us to obtain the value of the decrypted string: be1f35b7a9353a2aa509eb719fd8ecd054c0a90e891253b0f4fc661699c68911. So the sha256() hash of the user's input after the first six characters needs to match this value. It was on my first try in Python I confirmed my suspicions with a hash match: In [5]: hashlib.sha256(b"thepooh").hexdigest() Out[5]: 'be1f35b7a9353a2aa509eb719fd8ecd054c0a90e891253b0f4fc661699c68911' I then started the app using the Frida script and provided a flag value of "winniethepooh." After I tap "Submit" the Frida script generates the following output showing the jailbreak check bypasses and the two hash values being used for comparison: [-] isJailbroken called [-] isJailbroken returning: 0x0 [-] coreLogic retval: 0x0 [-] Current bat NSArray retval: naah [+] changing to "yeah" [-] 5e34() called with: winniethepooh [+] 5e34() returning 0x1: 0x1 message: {'type': 'send', 'payload': '[-] b: (sha256 hash) input bytes:'} data: b'\xbe\x1f5\xb7\xa95:*\xa5\t\xebq\x9f\xd8\xec\xd0T\xc0\xa9\x0e\x89\x12S\xb0\xf4\xfcf\x16\x99\xc6\x89\x11' [+] 5c5c() returning 0x0: 0x0 [-] AES128Operation called [-] AES128Operation returned message: {'type': 'send', 'payload': 'decrypted data bytes:'} data: b'\xbe\x1f5\xb7\xa95:*\xa5\t\xebq\x9f\xd8\xec\xd0T\xc0\xa9\x0e\x89\x12S\xb0\xf4\xfcf\x16\x99\xc6\x89\x11' [-] b: retval: 0x1 [-] a: retval: 0x1 And from the message in the UI we can see that the flag is correct ("winni thepooh" also worked): Solving CTFs and crackmes is a great way to learn new techniques and tools to assist with reverse engineering. Knowing how to perform fundamental binary analysis can be especially helpful during security assessments when evaluating custom security controls or suspected malware. In addition to the angr script included above, the Frida script for bypassing the jailbreak checks is included below: var baseAddress = Process.enumerateModules()[0].base var JailbreakDetection = ObjC.classes.JailbreakDetection; var CriticalLogic = ObjC.classes.CriticalLogic; var NSArray = ObjC.classes.NSArray var NSString = ObjC.classes.NSString Interceptor.attach(JailbreakDetection['isJailbroken'].implementation, { onEnter: function (args) { console.log('[-] isJailbroken called'); }, onLeave: function (retval) { retval.replace(0x0); console.log('[-] isJailbroken returning: ' + retval); } }) /* Interceptor.attach(baseAddress.add(0x50d8), { // This is not required if entering the correct length pass. Helpful during reversing onEnter: function (args) { this.context.x0 = 0xd; console.log('[-] Changed x0 to: ' + this.context.x0 + ' for length check'); } }) */ // I don't think this is doing anything in our case, some ipad check? Interceptor.attach(CriticalLogic["- coreLogic"].implementation, { onEnter: function (args) { }, onLeave: function (retval) { console.log('[-] coreLogic retval: ' + retval); } }) Interceptor.attach(CriticalLogic["- bat"].implementation, { onEnter: function (args) { }, onLeave: function (retval) { var yeah = NSString["stringWithString:"]("yeah") var yeahArray = NSArray["arrayWithObject:"](yeah) var array = new ObjC.Object(retval); var count = array.count().valueOf(); var current = ''; for (var i = 0; i !== count; i++) { current = current + array.objectAtIndex_(i); } console.log('[-] Current bat NSArray retval: ' + current); // I have no idea what the yeah vs naah was, I guess a certain battery level? console.log('[+] changing to "yeah"') retval.replace(yeahArray); } }) Interceptor.attach(baseAddress.add(0x5c5c), { onEnter: function (args) { // this is called from inside CriticalLogic b: just before AES128Operation }, onLeave: function (retval) { // This needs to be 0 or the self reference gets cleared retval.replace(0) console.log('[+] 5c5c() returning 0x0: ' + retval); } }) Interceptor.attach(baseAddress.add(0x5e34), { onEnter: function (args) { console.log('[-] 5e34() called with: ' + Memory.readUtf8String(args[0])); }, onLeave: function (retval) { retval.replace(0x1); // we need to return a 1 here console.log('[+] 5e34() returning 0x1: ' + retval); } }) // showing the ObjC hooks for decrypt as well as CCCrypt below (commented out) Interceptor.attach(CriticalLogic["- AES128Operation:data:key:iv:"].implementation, { onEnter: function (args) { console.log('[-] AES128Operation called') }, onLeave: function (retval) { console.log('[-] AES128Operation returned'); //console.log('Type of retval -> ' + new ObjC.Object(retval).$className) var data = new ObjC.Object(retval); send('decrypted data bytes:', data.bytes().readByteArray(data.length())); } }) Interceptor.attach(CriticalLogic["- a:"].implementation, { onEnter: function (args) { }, onLeave: function (retval) { // this should return a 1 console.log('[-] a: retval: ' + retval); } }) Interceptor.attach(CriticalLogic["- b:"].implementation, { // should be the hash bytes onEnter: function (args) { //console.log('Type of args[2] -> ' + new ObjC.Object(args[2]).$className) var data = new ObjC.Object(args[2]); send('[-] b: (sha256 hash) input bytes:', data.bytes().readByteArray(data.length())); }, onLeave: function (retval) { // this should return a 1 console.log('[-] b: retval: ' + retval); } }) /* // CCCrypt const cccrypt = Module.findExportByName(null, 'CCCrypt'); const ccsha256 = Module.findExportByName(null, 'CC_SHA256'); var algs = { 0: 'AES', 1: 'DES', 2: '3DES', 3: 'CAST', 4: 'RC4', 5: 'RC2' } function pad(num, size) { var s = num + ""; while (s.length size) s = "0" + s; return s; } Interceptor.attach(cccrypt, { onEnter: function (args) { console.log('[*] CCCrypt called:'); args[0] == 0 ? console.log(" [+] Mode: Encrypt") : console.log(" [+] Mode: Decrypt"); this.alg = parseInt(args[1]); var keyLength = parseInt(args[4]); var dataInLength = parseInt(args[7]); this.dataMovedLength = parseInt(args[10]); this.keyBytes = Memory.readByteArray(args[3], keyLength); this.ivBytes = Memory.readByteArray(args[5], 16); this.dataInBytes = Memory.readByteArray(args[6], dataInLength); this.dataOut = args[8]; this.dataOutAvail = parseInt(args[9]); this.dataOutMoved = args[10]; }, onLeave: function (retval) { var b = new Uint8Array(this.keyBytes); var keyData = ""; for (var i = 0; i b.length; i++) { keyData += pad(b[i].toString(16), 2); } var b = new Uint8Array(this.ivBytes); var ivData = ""; for (var i = 0; i b.length; i++) { ivData += pad(b[i].toString(16), 2); } var b = new Uint8Array(this.dataInBytes); var dataInData = ""; for (var i = 0; i b.length; i++) { dataInData += pad(b[i].toString(16), 2); } var dataOutBytes = Memory.readByteArray(this.dataOut, this.dataOutMoved.readUInt()); var b = new Uint8Array(dataOutBytes); var dataOutData = ""; for (var i = 0; i b.length; i++) { dataOutData += pad(b[i].toString(16), 2); } console.log(' [+] Alg: ' + algs[this.alg] + '\n [+] Key: ' + keyData + '\n [+] IV: ' + ivData + '\n [+] DataIn: ' + dataInData + '\n [+] DataOut: ' + dataOutData); } }); Interceptor.attach(ccsha256, { onEnter: function (args) { console.log('[+] CC_SHA256 called'); var length = parseInt(args[1]); this.input = Memory.readByteArray(args[0], length); this.md = args[2]; }, onLeave: function (retval) { send('cc_sha256 input', this.input); var hash = Memory.readByteArray(this.md, 32); send('cc_sha256 hash', hash); } }) */ By: Optiv AppSec Team Share: Angr Frida Red Team Source Zero® Hopper Copyright © 2024 Optiv Security Inc. All rights reserved. No license, express or implied, to any intellectual property or other content is granted or intended hereby. This blog is provided to you for information purposes only. While the information contained in this site has been obtained from sources believed to be reliable, Optiv disclaims all warranties as to the accuracy, completeness or adequacy of such information. Links to third party sites are provided for your convenience and do not constitute an endorsement by Optiv. These sites may not have the same privacy, security or accessibility standards. Complaints / questions should be directed to Legal@optiv.com
Copyright © 2024 Optiv Security Inc. All rights reserved. No license, express or implied, to any intellectual property or other content is granted or intended hereby. This blog is provided to you for information purposes only. While the information contained in this site has been obtained from sources believed to be reliable, Optiv disclaims all warranties as to the accuracy, completeness or adequacy of such information. Links to third party sites are provided for your convenience and do not constitute an endorsement by Optiv. These sites may not have the same privacy, security or accessibility standards. Complaints / questions should be directed to Legal@optiv.com
Would you like to speak to an advisor? How can we help you today? Image E-Book Cybersecurity Field Guide #13: A Practical Approach to Securing Your Cloud Transformation Download Now Image Events Register for an Upcoming OptivCon Learn More Ready to speak to an Optiv expert to discuss your security needs?