Reverse Engineering Starling Bank (Part II): Jailbreak & Debugger Detection, Weaknesses & Mitigations
There are three layers of protection applied before main starts doing its intended work. Frida detection, jailbreak detection, and debugger detection.
When Frida runs in injected mode1, there’s a daemon,
frida-server, that listens for connections on port
27042 and exposes
frida-core. And as mentioned in the OWASP guide2, you could detect Frida in this mode of operation using this port. Starling uses this method.
First it gets all TCP interfaces using
getifaddrs3 and checks for interfaces with the address family
AF_INET4, which is for Internet connections.
After getting Internet interfaces, which in my experiments have been the
lo0 (loopback/localhost) and
en2 (USB ethernet), a socket is created, and
bind() is called to try and bind that socket to the interface’s address at port
27024. All that does is basically check if that port is already open, in all Internet interfaces iteratively.
If Frida is listening there,
bind() will return an error
EADDRINUSE5. But what if it’s another legit process that has nothing to do with Frida but for some reason chose to listen on that port? Frida uses the D-Bus protocol6, so Starling double checks that this is Frida by sending an
AUTH command7, if it receives a
REJECTED8 response, then this is D-Bus and this is most likely is Frida.
access(), and sometimes
stat64(), is a canonical method for checking for the existence of jailbreak artifacts (Cydia, SafeMode, themes, etc.). Starling takes it three steps further:
First, before checking for those files using their absolute paths, .e.g
"/Applications/Cydia.app", it creates a symlink from the root directory
"/" to a file in
tmp inside the binary’s sandbox, and uses that symlink to check for the existence of said artifacts, so it would check for the existence for
"<sandbox>/tmp/<somefile>/Applications/Cydia.app" instead. Why? Porbably because most jailbreak detection bypass tweaks are expecting absolute addresses, so this bypasses the bypasses.
Second, it checks for non-JB files/directories, e.g.
/etc/hosts, and expects
access() to return
0, or success. Otherwise, it’ll crash. This is probably to prevent you from trivially hooking
access() to always return
ENOENT (file doesn’t exist); because you expect it to only check for jailbreak artifacts.
Third, it checks for a quite sizable amount of files. Most jailbreak detectors will check for maybe 10 files and that’s it. So all in all it’ll look something like this:
There’s a call to
kill(getpid(), 0). Regarding the second argument, the
man page for
A value of 0, however, will cause error checking to be performed (with no signal being sent). This can be used to check the validity of pid.
Hmm, so it checks if the process actually exists. My guess then is that this is for anti-emulation purposes; because a process wouldn’t normally exist in an emulated enviornment. If anyone has a better explanation, feel free to hit me up.
After confirming that the device isn’t jailbroken using the methods above, the binary will check if a debugger is attached to it via the standard way,
sysctl, even Apple has a page9 on it. It’s trivial to bypass it by just flipping the
P_TRACED flag if it’s on in
info.kp_proc.p_flag10. The twist here is that it does this very same check not once, not twice, but thrice. You would think that is just redundant, but it’s not. Remember, with this binary, you can’t single-step your way out of it11. The original code should look something like this:
Weaknesses & mitigations
The Starling team did a great job, kudos to them. But like everything else humans make, there’s room for improvement.
I was quite surprised when I was able to re-sign the binary myself, run it on a non-jailbroken device, and see what the execution trace for a normal device (save for the debugger) should look like, it saved me a lot of time actually because I was able to do confirm things quickly, e.g.
bind()11 should return success. For an app that cares about its security, it’s not a good idea to let the binary run its critical parts when it’s been re-signed by a third-party.
Mitigation: verify that the binary isn’t signed by a third-party.
Better debugger detection
sysctls are better than one, a non-standard debugger detection would be a good advantage to a binary like this. Although these are kind of trade secrets.
Currently nothing stops someone from injecting a dylib that hooks12 ObjC/Swift methods in this binary and changes its behavior. That’s true even on a jailed device due to the lack of the signature check above.
Mitigation: verify that no suspicious dylibs (dynamic libraries) are loaded. This could be done using
dyld (dynamic linker) functions such as
After having reverse engineered the detections, which requires a good amount of skill, nothing stops someone from trivially patching all the checks, and re-packaging the binary, then use it even on a jailed device. Possible mitigation: an obfuscated checksum function that verifies the integrity of the checks.
As long as it doesn’t come at a huge performance cost, more obfuscation techniques would help make breaking jailbreak/debugger detection an even harder task than it already is, and give Starling more advantage in the cat-and-mouse that is reverse engineering.
PS: I’m available for hire