A First Look at a Connected-Car App Hardened with a Commercial Packer
About the author. I’m Colin Xiao — an independent reverse engineer, mostly working solo on contract jobs. I dig into compiled code of every kind, including but not limited to: Android and iOS apps, x86/x64/ARM native binaries, packers and obfuscators, DRM and license checks, network protocols and APIs, crypto and signature algorithms, malware, game binaries, and embedded/IoT firmware. I also handle large-scale data collection and vulnerability research. If you have any project in mind, I’d be glad to take it on — whatever it is, as long as it’s within my wheelhouse. Just reach out. You can reach me on X at @colinxiao_lab or on Telegram at https://t.me/ColinXiao666.
This research is published for educational and defensive purposes. The target sample is anonymized throughout.
A First Look at a Connected-Car App Hardened with a Commercial Packer
0x1 — Setup
Target app: a connected-car app, v6.1.0
Tools: Cydia, Frida, Objection, Storm Sniffer (packet capture)
0x2 — Packet capture
- Open Storm Sniffer (available on the App Store; Fiddler also works).
- The app has SSL pinning enabled, so capture fails.

-
Bypass SSL pinning with objection:
objection -g TargetApp explore ios sslpinning disable --quiet

- Now capture works.
The app reports a wrong username/password:
https://appserver.example.com/app/auth/login
User-Agent: targetapp/6.0.1 (iPhone; iOS 14.0; Scale/3.00)
Content-Type: application/json
Host: appserver.example.com
{"request":"FESWLgT0LjY\/GjPQmt4xYvDWajog909VYoUcLQ8TE7E\/F4U9GwHtbmfY9uZbmqPnGW2ys0NRnezIjTETA64wpvfP3aZV+caLdK8fsLovfxoacBp5hIkvlS4hTEYkl8XfIfashQDMOdYmm8W89ZBwpFF+Ipd9hFy7NGHDom4SgXQ5uE7Io8jFnoGiz5CiQ9B3K0NbCSOkSbbwJi14iFi15sy3car\/W3xMNUZuo7\/vNE7Dz\/eHwxroJ7L+DK\/6yXbNLRHMHRtXJykfnD31ShNTEqzkF+Fp2Z6jUuFFpn4B7SB4uJYWe5cJ9g3gDIOEifHEhhD3LCQfOEKppgisduO8el4rEGFaKPx1bzOAIk0W1qPXk0LbVCFPQYFpypUB8Rrn2hBBsA8cepRBBtH73jeRcvrRMBc0gK+YRw4uq8yd44bNoEpSt1L5YOJBelSxmp9WOhkGOjt5cPCHij1ptaO9gp1d7XAg4+MhpzXoluB8aPi8A28stX04l+HdU6lNU0pUGYtFVRbTvYV4KjseXQ5wlRavwFMgv3c61WUMznyLmtI8zdheCVdr3vGG1nNvGI7cqMHdw8jvdYFCe9jr3eu1nG2uHakh7PHDMyEDpEh7WSJvVod+0sleiS+vhv5xnNhXUOtBtjCUcLSZToY6YJky9O4E8WDOGg2j5kvCaHrY\/bgSGd87O7sCkmrjeYEJj5BSV95tlI2Ky1GlTRvG4jLQ800QuXAJyXQJpf9uO5lbmV2yPJ7OcKbRoaRu7rhMj4yok9dPEY5bIJeMFptv+bBTKgRh18R5ZecUdpyx8PrVY7FvTEihzH9z+ACGIMNgw7MZIVRPBqLh\/zXK1JfY3pPblvLVhmbeWQ6ETTC2eEWDmAcBovF+Mpj\/M2ybwRdFGLATHZJUQCobYS+02Jxfd0\/ODHpOQ7c2RJwZh6aijIEeh3Ohj1weh\/ZNAK7189yi3n810Bne0ld\/Vo11Jk7hSSQ9z1vCZijCzbDb4Yuw+jsnM\/CPS0OBMMQqIsuz9TPJ\/89SP103sVv01AOXWUM0OB97POJpQ8Bnfjusf8x7CKURlyj4hl6NHksSAGeOmPv96B\/X\/xEARr3Lq7K0RnVFjqFJUsXVVJavggfrhexl7xgvU7MqOPZ24ZiMmOTKTKP+WVsykcraeoP9+yuYkhdMLPEBelpnkoHOuHvpqojhOcJvN8pu\/tRclZO6KGuRD8I7M3J2+mMo8ZTNQUB7oYVR\/UfIKJ5+aIybJklZ1HTb5nV\/D1tDgWRN5JDvndJ78cEi6s8Hi9gsZjglN1JNaQ3vShP7RuLSYdRqDO7e27cSkibTH4YW6PsHPHhqX2g4ev8WTEbRIAsiVJTOuJqNaPaoX3oDMviBz5aZ0iu6XwRwbojibWBCOABktvzVNgngxxeyPmyJlVQ5H5ehn\/iNNZC4RRz0FqhetTfWdQWXnRqicq+tiujO478mQQVKTDKvhi6y0uL2g2SuR81QJ0YmhlCFxqFWX2ACpro\/4HM5J0EdJvboVwrR70CEBa6pqwsvCSCQCltwM+EDH+OYxt34oL1ngY7+fsm1oZcyEvn3\/snVZcd97klBzdl5oCyj3ANtoDIQua35u2ENXbPc8CqIIlcbPVsmF5U1ESRs5khYlDjBH6qvpZBoA4a36pgMZVu8XKpk7c+bOIB0WrZcMSaIF+r2x0Jg8ZKjrZ\/AwhoRDHAtEIQNDLNkPDvjoEcoI0CF3\/p\/AVyjko9aqtatyNZE7i0MQ4SUdamyogd428NFygGgq5JtoSe3XXL\/3hWxlbTGqESnPXKjh7jTG5FQxy6grBp7lVN2DSW2yNkvVOrtVaRjIbTzJd7\/oiUE29Yx1l\/1tPpJTq0CDSa2sTGgYHyPkppcoA3OXLiEGuQC0lKuvuahyAh956uueAi7DvMUWzPemfszzr6Bd35RucQE5tFjzQ19QIIDGhxLHMfweQHO2jEUuTddqRbnO3ZVyM8jZpcNBwMKbuqjk"}
{"response":"Fel63A3fJsBnqDNoqrf3sNRYntlgl2bMzWrA56aEQZRggnMBao9z1ds1G/TqV2TMwrRu1wovdv5AwWkDlpdqO0A=="}
0x3 — Decrypting the app
Dump the IPA with frida-ios-dump, pull out the main binary, and load it into IDA.
0x4 — Locating the encryption
4.1 The idea: hook -[NSURLRequest initWithURL:] with Frida and print the call stack.
try {
var className = "NSURLRequest";
var funcName = "- initWithURL:";
var hook = eval('ObjC.classes.' + className + '["' + funcName + '"]');
Interceptor.attach(hook.implementation, {
onEnter: function(args) {
console.log(colors.green,"NSURLRequest with URL: ",colors.resetColor + ObjC.Object(args[2]));
console.log("\nBacktrace:\n" + Thread.backtrace(this.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress).join("\n"));
},
});
} catch (error) {
console.log(colors.red,"[!] Exception: ",colors.resetColor + error.message);
}
Output:
[iPhone::TargetApp ]-> NSURLRequest with URL: https://appserver.example.com/app/auth/login
Backtrace:
0x1057d943c /private/var/containers/Bundle/Application/48133A5D-90E1-494C-8472-79C2B5DE912B/TargetApp.app/TargetApp!-[AFHTTPRequestSerializer requestWithMethod:URLString:parameters:error:]
0x1057d106c /private/var/containers/Bundle/Application/48133A5D-90E1-494C-8472-79C2B5DE912B/TargetApp.app/TargetApp!-[AFHTTPSessionManager dataTaskWithHTTPMethod:URLString:parameters:headers:uploadProgress:downloadProgress:success:failure:]
0x1057d0780 /private/var/containers/Bundle/Application/48133A5D-90E1-494C-8472-79C2B5DE912B/TargetApp.app/TargetApp!-[AFHTTPSessionManager POST:parameters:headers:progress:success:failure:]
0x1049e1e24 /private/var/containers/Bundle/Application/48133A5D-90E1-494C-8472-79C2B5DE912B/TargetApp.app/TargetApp!+[AFNRequestUtls postRequestWithInfo:successBlock:failedBlock:]
0x104b4d270 /private/var/containers/Bundle/Application/48133A5D-90E1-494C-8472-79C2B5DE912B/TargetApp.app/TargetApp!+[AppHttpsRequestUtils httpsPostRequestWithRequestRequestInfo:successBlock:failedBlock:]
0x1048f48fc /private/var/containers/Bundle/Application/48133A5D-90E1-494C-8472-79C2B5DE912B/TargetApp.app/TargetApp!-[AppUserModel doLoginSuccess:failed:]
0x104a4c4f8 /private/var/containers/Bundle/Application/48133A5D-90E1-494C-8472-79C2B5DE912B/TargetApp.app/TargetApp!-[AppLoginVC doLogin]
...
AFHTTPRequestSerializer tells us the app uses AFNetworking. Hook -[AFHTTPRequestSerializer requestWithMethod:URLString:parameters:error:] to grab the parameters — they’re already encrypted. So the encryption happens inside +[AppHttpsRequestUtils httpsPostRequestWithRequestRequestInfo:successBlock:failedBlock:].
4.2 Look at +[AppHttpsRequestUtils httpsPostRequestWithRequestRequestInfo:successBlock:failedBlock:] in IDA:

Hopper’s decompiler misbehaved here — I couldn’t figure out how to change the function range in Hopper; if anyone knows, leave a comment.

Take a look at the CFG. Yikes — control-flow obfuscation. Keep your cool, no big deal; check the pseudocode first.

It creates a network-request requestInfo instance. Find the call that sets the parameters:

There it is — +[AppDataManager entryRequestInfo:] returns the encrypted content. The v6 passed in is the AES-encrypted plaintext message dictionary, with the key being $key + $value concatenated and SHA-1 hashed (leaving a TODO here — that part gets complicated). Let’s follow it.

What the… this is getting absurd. Keep reading the pseudocode:

There’s the request string. BangSafeSDK confirms this is the encryption of a commercial hardening SDK. Digging further into +[BangSafeSDK checkcode:dataStyle:]: it UTF-8-encodes the content, grabs the current time, and finally calls sub_1023BE0D4.
I wanted to check sub_1023BE0D4’s CFG, but IDA errored out — too many nodes.

Now what? Blind guess: control-flow obfuscation again (used to it by now). Time to trace it — thanks to misskings for the Stalker trace idea.
/// <reference path="frida-gum.d.ts" />
//+[AppHttpsRequestUtils httpsPostRequestWithRequestRequestInfo:successBlock:failedBlock:]
var target = ObjC.classes['AppHttpsRequestUtils']['+ httpsPostRequestWithRequestRequestInfo:successBlock:failedBlock:']
console.log(target.implementation)
var m = Process.getModuleByAddress(target.implementation)
var vm = m.base.add(0x23BE0D4)// note the base address IDA uses for the binary
var baseAddr = m.base
console.log(vm);
Interceptor.attach(vm, {
onEnter: function (args) {
console.log("enter")
this.tid = Process.getCurrentThreadId()
console.log(this.tid);
Stalker.follow(this.tid, {
events: {
// not needed for now
call: false,
ret: false,
exec: false,
block: false,
compile: false
},
transform: function (iterator) {
var instruction = iterator.next();
const startAddress = instruction.address;
// get sub_1023BE0D4's address range from IDA (alt+p)
var is = startAddress.compare(baseAddr.add(0x23CBB6C))<0 && startAddress.compare(baseAddr.add(0x23BE0D4))>=0;
do{
if(is){
console.log(instruction.address.sub(baseAddr).add(0x100000000) + ":" + instruction);
}
iterator.keep();
} while ((instruction = iterator.next()) !== null);
}
})
},
onLeave: function (ret) {
Stalker.unfollow(this.tid)
}
})
Traced instructions:
0x1023be0e4:stp x20, x19, [sp, #0x40]
0x1023be0e8:stp x29, x30, [sp, #0x50]
0x1023be0ec:add x29, sp, #0x50
0x1023be0f0:sub sp, sp, #0x220
0x1023be0f4:sub x9, x29, #0x98
0x1023be0f8:adrp x8, #0x106f3d000
0x1023be0fc:ldr x8, [x8, #0xd70]
0x1023be100:ldr x8, [x8]
0x1023be104:str x8, [x9, #0x40]
0x1023be108:mov w8, #0x12c
0x1023be10c:cmp x0, #0
0x1023be110:str w8, [sp, #0x130]
0x1023be114:mov w8, #0x2e
0x1023be118:mov w9, #0x4c
0x1023be11c:csel w8, w9, w8, eq
0x1023be120:stp w8, w2, [sp, #0xb0]
0x1023be124:cmp w2, #4
0x1023be128:mov w8, #0x53
0x1023be12c:csel w8, w9, w8, eq
0x1023be130:str w8, [sp, #0x34]
0x1023be134:mov w8, #0x12
0x1023be138:mov w9, #0x34
0x1023be13c:csel w8, w9, w8, eq
......
With an IDA Python script, strip the unused branches:
import sark
import idc
import sys
def patch_code(addr,code):
for i in range(len(code)):
idc.patch_byte(addr+i,code[i])
# patch the bytes at the address to nop
def nop(addr):
nop_code=[0x1f,0x20,0x03,0xd5]
patch_code(addr,nop_code)
def main():
logfilename="./log.txt"
# the function's start/end range
start=0x1023BE0D4
end=0x1023CBB6C
addrs=[]
# read all executed instruction addresses
with open(logfilename,"r") as logfile:
lines=logfile.readlines()
for line in lines:
addrs.append(int(line.replace("\n","").split(':')[0],16))
# recolor executed addresses for visibility
for addr in addrs:
line=sark.line.Line(addr)
line.color=0x00ffff
# all instruction addresses in the target function
funcLines=sark.lines(start,end)
for line in funcLines:
# if the line is code, check whether it's marked as executed
if line.type=="code":
if line.color!=0x00ffff:
nop(line.ea)
if __name__=="__main__":
main()
Check the CFG again:

Still nothing — so I called it here.
Giving up for now. Leaving this as a TODO: it’s still control-flow obfuscation, so I’ll stop the analysis here.
Colin Xiao — I take on contract reverse-engineering work, and writeups like this are how I show what I can do. If you need help with anything in this space — including but not limited to the areas I cover above — I’m available. No project is too big or too small — if it’s reverse engineering and I can do it, I’ll take it. DM me on X (@colinxiao_lab) or Telegram (@ColinXiao666).
Get in touch