Reverse Engineering Starling Bank (Part II): Jailbreak & Debugger Detection, Weaknesses & Mitigations

2020-08-02

Three layers

There are three layers of protection applied before main starts doing its intended work. Frida detection, jailbreak detection, and debugger detection.

Frida listens

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.

ifa->ifa_addr->sa_family == AF_INET

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.

int status = bind(sfd, addr, sizeof(addr));

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.

char *cmd = "\0AUTH"; 				// null-beginning for some reason
write(sfd, cmd, sizeof(cmd));			// communicate with Frida

char reply[REPLYMAX];
recvfrom(sfd, reply, sizeof(reply));		// Frida replies
if (strncmp(reply, "REJECTED", 8) == 0) { 	// strncmp or something along those lines
	// This is defintely Frida, crash
}

Jailbreak detection

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. /dev/null, /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:

char slnk = "<sandbox>/tmp/<somefile>";	// replace <sandbox> with that of the app
symlink("/", slnk);
int status_lookup[];	// hardcoded, expected status for each file (JB or non-JB)
char artifacts[];	// hardcoded, but strings are obfuscated

for (int i = 0; i < LEN_ARTIFACTS; i++) {
	char artifactp[400];
	sprintf(artifactp, "%s%s", slnk, artifacts[i]);
	int status = access(artifactp, F_OK); 	// just check for its existence
	if (status != status_lookup[i]) {
		// File access isn't what's expected, crash 
	} 
}

kill -0?

There’s a call to kill(getpid(), 0). Regarding the second argument, the man page for kill states:

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.

Debugger detection

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:

bool is_debugged = amIBeingDebugged();
is_debugged |= amIBeingDebugged();
is_debugged |= amIBeingDebugged();

Weaknesses & mitigations

The Starling team did a great job, kudos to them. But like everything else humans make, there’s room for improvement.

Code signature

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

Although three 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.

Code injection

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 dlsym()13 and _dyld_get_image_count()14.

Anti-tampering

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.

More obfuscation?

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