Snowblind Android Malware - Promon

6 min read Original article ↗

Usability for attacks  

The question now is: How can this functionality be used for attacks? As a first idea, it could be used to block apps from doing certain things. This can have security implications if essential system calls are blocked. But compared to the technique that Snowblind uses, it is not very powerful. 

So, what does Snowblind do? Its goal is to prevent the detection of the app having been repackaged. This requires bypassing the anti-tampering mechanisms found in the app it attacks. A common way to detect repackaging is by checking the app’s APK file(s) on disk to see if they have been tampered with. This requires an application to open the file(s) on disk and read their contents. If an attacker can somehow hook into this process and redirect the file's opening to another unmodified version of the app, the anti-tampering mechanism would be bypassed. This is a common attack on anti-tampering mechanisms. The question is, how do you do this hooking? In applications that do not have any protection, opening the file (either from Java or native code) would end up in a call to something like open() in libc. In that case, an attacker can directly overwrite the code in libc to cause a different behavior in case the app’s files are being opened. Alternatively, the attacker can perform global offset table hooking to hook the interface between the library that calls open() and libc.  

These are obviously well-known attacks, so applications have started protecting against them. Common ways to do this are to validate that there are no hooks in libc in memory or by not relying on libc in the first place. It has become common practice to re-implement system calls like open() in native libraries to make such attacks harder. This would mean attackers must find the implementations in the target libraries and hook these implementations instead. However, developers who implement these protection mechanisms know this and use obfuscation and strong integrity checking of their code in memory to make these attacks more challenging to execute. A common assumption is that if you implement your code in a native library, implement critical system calls yourself, and apply good obfuscation and integrity checking, your code will be very difficult to attack. Attackers still have some options, like breaking obfuscation and/or integrity checking, patching the kernel, or using code tracing or code emulation frameworks. But all these options can take a considerable amount of time or do not scale well for a large-scale attack. 

Snowblind


So now we get back to Snowblind. The application it targets does implement anti-tampering mechanisms in a native library using its own system call implementations in combination with strong obfuscation and integrity checking, which can be considered best practice. However, the technique used by Snowblind breaks the assumption that this is difficult to attack. Snowblind adds an additional native library into the application that gets loaded before the anti-tampering code can run for the first time. This native library installed its own
seccomp filter in the process it gets loaded into. If we decompile this filter, we see that it allows all system calls except for open() and a few others. In the case of open(), the filter returns SECCOMP_RET_TRAP. This return value instructs the kernel to stop the system call, and instead of executing it, it generates a SIGSYS signal that the process can catch. The malware additionally installs a signal handler for SIGSYS; whenever it receives that signal, it can inspect and manipulate the thread's registers. It uses that capability to manipulate the argument to the open()call to point to a file that is the original version of the app without modifications. Finally, it executes the open system call with the manipulated argument, and the anti-tampering mechanism is simply and robustly bypassed. 

Here is a simple example of doing that (on arm64): 

image (5)...
image (4)

Hooking all open calls that the application makes will potentially generate many signals and slow down the app noticeably. Also, shouldn’t the signal handler executing the open system call itself also trigger the seccomp filter? The malware works around these issues with an additional trick, which is to have the filter check where the call to the system call came from. The filter will only instruct the kernel to generate the signal if the call came from the library that implements the anti-tampering mechanism. This greatly improves the speed of the attack while simultaneously solving the issue of having the signal handler trigger itself by executing the open() system call.  

This is what makes this attack very powerful: It allows attackers to filter, inspect, and manipulate any system call and additionally make the filter very narrow by being able to filter on the location that the call came from and potentially on the arguments to the system call. This means that it can obviously be useful to do much more than bypassing anti-tampering mechanisms. It can be used to manipulate and trace any code that relies on system calls, even if it implements the system calls and makes them hard to find and patch.  

We have been investigating whether this approach has been publicly described or used in any public tools. We have found a few repositories on GitHub that implement something in this direction, as well as some Chinese blog posts describing similar methods. None of them seem to be as refined as the methods that Snowblind uses. They do not seem to have attracted much attention, and it is interesting to note that all of these sources seem to be in Chinese. 

It should be mentioned that there is one popular tool that uses seccomp in a similar way. The strace tool can monitor any system call that a process makes. It works by attaching to the process to be monitored using the ptrace() system call. With ptrace(), it has the possibility to stop the process whenever it executes any system call. This is obviously very slow if the process makes a lot of system calls. Version 5.3 of strace adds a new option to only have it trace specific system calls. This speeds up the tracing considerably. It works by applying a seccomp filter to the process that is being monitored to trigger a SIGSEGV whenever the system calls that should be monitored are being executed. The difference to the methods used by Snowblind is that strace monitors the signals from another process using ptrace() and not from a signal handler inside the process that is being traced.