Get a demo

Introduction

Promon’s App Threat Reports have traditionally reviewed apps against common malware attack vectors. This report explores a unique attack vector used by a new malware targeting banks in Southeast Asia.  

In early 2024, our partner i-Sprint provided a sample of a new Android banking trojan we have named Snowblind. Our analysis of Snowblind found that it uses a novel technique to attack Android apps based on the Linux kernel feature seccomp. Android uses seccomp to sandbox applications and limit the system calls they can make. This is intended as security feature that makes it harder for malicious apps to compromise the device. However, Snowblind misuses seccomp as an attack vector to be able to attack applications. We have not seen seccomp being used as an attack vector before and we were surprised how powerful and versatile it can be if used maliciously. 

This report will describe Snowblind and provide a detailed description of how the attack vector used by the malware works. Snowblind uses the attack vector to easily bypass strong anti-tampering mechanisms like repackage detection even if strong obfuscation and integrity checks are in place. While this is one use case, we also noticed that the attack vector used by Snowblind could be used to compromise apps in many different ways and make successful, scalable app attacks much easier. 

Promon has also implemented protections against Snowblind in Promon SHIELD® for Mobile version 6.5.2. Promon SHIELD for Mobile version 6.6.0 protects an even broader range of seccomp-based attacks. We encourage all customers to update to the latest version.

Asset 13

 

Snowblind – introducing a new Android attack vector 

A very common attack vector for malware to attack Android apps is (ab)using the Android accessibility service functionality to obtain user input like usernames and passwords or to remote-control applications to perform malicious actions. However, as this is a common attack, applications have started implementing protection mechanisms against it. 

Oftentimes, this is done by asking the system for all enabled accessibility services and then checking whether they are trustworthy. If an untrustworthy accessibility service is found, apps can react to that. In the simplest case, the app could display a warning message to the user. This is ideally done in a way that cannot easily be manipulated by an accessibility service. But apps could also use stronger measures like sending telemetry to a server or even terminating themselves.

This is quite effective protection and is likely to prevent many attacks based on malicious accessibility services. Because of that, attackers seem to have realized that they must step up their game. To get around this protection, attackers commonly perform a repackaging attack on the app they target, where the part of the app’s code that detects malicious accessibility services is manipulated to never detect anything. This is quite easy if the application is not detecting this tampering and if the application does not make it difficult to find out which parts of the application need to be modified (i.e. these parts are not obfuscated). However, since repackaging is a common way to attack Android applications, many applications also protect against it either with tamper detection and/or obfuscation of sensitive parts of the code. So again, attackers need to go one step further when attacking such apps. 

Last year, we analyzed the FjordPhantom malware, which attacks apps to prevent them from detecting malicious accessibility services. To do that, it injects code into the application that hooks into the communication between the app and the system when the application asks for the list of enabled accessibility services. In this case, the malware authors bypassed the tampering detection mechanisms not by directly modifying the app but by loading it into a virtualized environment and loading additional code that performs the hooking. In addition, the attackers did not need to deobfuscate the app because they did not need to find the exact place where the detection happens. They simply hooked into the interface to the system that the code uses to determine which accessibility services are enabled. 

Snowblind has exactly the same goal as FjordPhantom in that it wants to prevent the detection of malicious accessibility services so that the target app does not warn the user about their presence. The difference between FjordPhantom and Snowblind are the techniques used to achieve this while working around anti-tampering mechanisms. Snowblind does not use virtualization for this. Instead, it performs a normal repackaging attack but uses a lesser-known technique based on seccomp that is capable of bypassing many anti-tampering mechanisms.  

Interestingly, FjordPhantom and Snowblind target apps from Southeast Asia and leverage powerful new attack techniques. That seems to indicate that malware authors in that region have become extremely sophisticated, and Promon will focus more on analyzing malware coming from that region. 

From our point of view, Snowblind's seccomp-based technique is even more interesting than the malware itself. It seems to have the potential to be used for much more than what the malware uses it for.  

Q2 2024 App Threat Report Beware of Snowblind- A new Android malware - illustration2.png

 

See Snowblind in action

What is seccomp? 

seccomp (secure computing) is a functionality in the Linux kernel that can be used by userspace processes to specify policies that are checked whenever the process performs a system call. Once a policy is applied for a given process, it cannot be removed anymore. Likewise, if the process forks any child processes, they inherit the policy from their parent. seccomp is intended as a sandboxing mechanism to be used in cases where a process wants to reduce its attack surface or the kernel's attack surface. 

seccomp can be used in two modes: When it was originally introduced in March 2005, it only included a strict mode, where only the exit() and sigreturn() system calls as well as the read() and write() system calls on already open file descriptors are allowed. This is a very locked down and not very flexible policy, so it is mostly useful for running untrusted code that does not perform any system calls but not for much more than that. For more flexibility, seccomp-bpf was introduced in July 2012. In this mode, policies can be specified using Berkeley Packet Filters (BPF), which enables applications to define fine-grained policies and that makes seccomp quite interesting for many use cases. Nowadays, seccomp is used for sandboxing in many popular projects, including Docker, OpenSSH, vsftpd, Firefox, and systemd. 

Before we think about what seccomp could be used for on Android, there is one question that needs to be answered: is seccomp (and especially seccomp-bpf) available on Android and if it is available, since when? As mentioned earlier, seccomp hasn’t always been part of the Linux kernel and in addition, it needs to be enabled when building the Linux kernel by enabling the CONFIG_SECCOMP and CONFIG_SECCOMP_FILTER configuration options. Since device manufacturers are usually building their own kernels, there could also be a strong fragmentation. 

We have found that starting with Android 8, Google has talked about using seccomp in Android. Specifically, Google started using seccomp in zygote to prevent apps from performing certain unusual system calls to reduce the attack surface of the kernel when being attacked from a malicious app. In addition, we have also found that in Android 8, Google has added tests to the Android CTS (Compatibility Test Suite) to check that seccomp-bpf is available. The CTS is a test suite that device manufacturers should use to test that the version of Android they build for their devices is compatible. This makes us confident that most devices should support seccomp-bpf from at least Android 8 and up. We have also run tests on all Android emulators from Android 5 and up and verified that seccomp-bpf is available on all of them. This leads us to believe that there is also a good chance that seccomp-bpf is available on devices running earlier versions of Android.

Q2 App Threat Report- Analysis of a new attack vector against Android - illustration 

How to use seccomp-bpf? 

When thinking about the potential capabilities and usages of seccomp-bpf, it first helps to have a good understanding of how to use it and which options a developer has when using it. 

The first thing to do when using seccomp-bpf is to define the BPF filter that should be applied to the process. BPF filters are small programs that are executed inside the kernel. These programs can do quite a lot of things, but we will not go into details of this here. What is important in the context of seccomp-bpf is that these programs have some input called struct seccomp_data that gives them information about the system call that is about to be executed. This struct is defined like this:   

struct seccomp image

The seccomp-bpf filters can load this data and then make decisions based on the values. The most common use for this is to filter system calls based on their number. Furthermore, filters can also be defined to filter based on the address where the call came from as well as the arguments to the system call. Ultimately, the filter needs to come up with a decision on what to do next. For that, there are different options: 

filter seccomp

Commonly used options are to allow the system call, to stop it and return an error, or to have the kernel kill the process or thread immediately. 

Finally, once a filter has been defined, it needs to be applied to the process. If the app is not running with CAP_SYS_ADMIN (which is usually not the case), it first needs to perform the prctl() system call with the PR_SET_NO_NEW_PRIVS option set to 1. This is a security measure that makes sure that child processes cannot obtain higher privileges than the process that installed the filter as this could be a security problem especially with setuid/setgid programs that could be prevented from dropping privileges. After that, the filter can be applied to the process using the prctl() system call and the PR_SET_SECCOMP option with the SECCOMP_MODE_FILTER argument and a pointer to the filter. 

Putting everything together, this is a simple example: 

image (6)

This defines a program that looks at the system call number of the system call being invoked and compares it to the system call number of the ptrace() system call. If they match, the filter returns SECCOMP_RET_KILL_PROCESS, which instructs the kernel to kill the process. Otherwise, it returns SECCOMP_RET_ALLOW, which tells the kernel to allow the system call. 

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. 

Q2 2024 App Threat Report Beware of Snowblind- A new Android malware - illustration-1


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. 

Conclusion

Since the technique used by Snowblind does not seem to be well-known, we do not expect many apps to protect against it. This will hopefully change because attackers now have a powerful new tool to attack an app efficiently. 

Promon customers using any version of Promon SHIELD® from version 6.5.2 are protected against Snowblind. We encourage all customers to upgrade to the latest version of SHIELD (version 6.6.0) for an even more robust defense against seccomp-based attacks.

We also hope this discovery focuses more security researchers exploring this technique to improve app security for all.