Professional Documents
Culture Documents
Walkthrough of An iOS CTF
Walkthrough of An iOS CTF
Home (/) > Explore Optiv Insights (/insights) > Source Zero (/insights/source-zero)
> Walkthrough of an iOS CTF
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:
1 of 11 7/9/21, 8:15 PM
Walkthrough: iOS Capture the Flag | Optiv https://www.optiv.com/insights/source-zero/blog/walkthrough...
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.
+[JailbreakDetection isJailbroken] :
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.
2 of 11 7/9/21, 8:15 PM
Walkthrough: iOS Capture the Flag | Optiv https://www.optiv.com/insights/source-zero/blog/walkthrough...
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 (https://angr.io/), 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]
3 of 11 7/9/21, 8:15 PM
Walkthrough: iOS Capture the Flag | Optiv https://www.optiv.com/insights/source-zero/blog/walkthrough...
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]
which prints:
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:
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 (https://en.wikipedia.org/wiki/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;
4 of 11 7/9/21, 8:15 PM
Walkthrough: iOS Capture the Flag | Optiv https://www.optiv.com/insights/source-zero/blog/walkthrough...
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(@"7c537a22bc6e1f979ac26341125c30d2eba190d2b003aff7a89454525932d4d8617c7797a1cfe20810cc860cff
@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(@"7c537a22bc6e1f979ac26341125c30d2eba190d2b003aff7a89454525932d4d8617c7797a1cfe20810cc860cff9
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;
}
So the sha256() hash of the user's input after the first six characters needs to
match this value.
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:
5 of 11 7/9/21, 8:15 PM
Walkthrough: iOS Capture the Flag | Optiv https://www.optiv.com/insights/source-zero/blog/walkthrough...
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:
6 of 11 7/9/21, 8:15 PM
Walkthrough: iOS Capture the Flag | Optiv https://www.optiv.com/insights/source-zero/blog/walkthrough...
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), {
7 of 11 7/9/21, 8:15 PM
Walkthrough: iOS Capture the Flag | Optiv https://www.optiv.com/insights/source-zero/blog/walkthrough...
// 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 return s;
}
Interceptor.attach(cccrypt, {
8 of 11 7/9/21, 8:15 PM
Walkthrough: iOS Capture the Flag | Optiv https://www.optiv.com/insights/source-zero/blog/walkthrough...
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);
}
})
*/
9 of 11 7/9/21, 8:15 PM
Walkthrough: iOS Capture the Flag | Optiv https://www.optiv.com/insights/source-zero/blog/walkthrough...
Share:
ANGR FRIDA
(/TAXONOMY/TAGS/ANGR) (/TAXONOMY/TAGS/FRIDA)
HOPPER
(/TAXONOMY/TAGS/HOPPER)
RELATED INSIGHTS
BLOG
March 30, 2018
BLOG
July 10, 2020
10 of 11 7/9/21, 8:15 PM
Walkthrough: iOS Capture the Flag | Optiv https://www.optiv.com/insights/source-zero/blog/walkthrough...
BLOG
July 22, 2020
(/insights/source-zero/blog/anatomy-kubernetes-attack-how-
See Details (/insights/source-zero/blog/anatomy-kubernetes-
untrusted-docker-images-fail-us)
attack-how-untrusted-docker-images-fail-us)
/channel/UC5dqDQ0tLgaohPd9meSCB6g)
Home (/) | Contact (/contact-us) | Cookie Policy (/optiv-cookie-policy) | Privacy Policy (/privacy-policy) | Terms of Use (/terms-of-use) | Sitemap (/sitemap)
The content provided is for informational purposes only. Links to third party sites are provided for your convenience and do not constitute an endorsement. These sites may not
have the same privacy, security or accessibility standards.
11 of 11 7/9/21, 8:15 PM