Android 11魔形女系列漏洞分析

背景介绍

  • 近日,京东探索研究院信息安全实验室的研究团队发现一项高危Android 11系统漏洞链,利用该漏洞链,黑客可能在用户毫无感知的情况下,获取用户手机中所有APP的隐私数据和权限,如获取任一社交软件的聊天记录,任意劫持邮箱、公司内部沟通软件、支付软件等等。
  • 听起来影响很大,但是往往最具威力的漏洞都有着很朴素的技术原理,魔形女系列漏洞正是如此。本文将在法律允许的范围内对魔形女系列漏洞进行技术解析。

故事的开始——CVE-2021-25485 of Samsung

  • 这是一个三星手机的漏洞,下面是三星官方的披露信息。
Path traversal vulnerability in FactoryAirCommnadManger prior to SMR Oct-2021 Release 1 allows attackers to write file as system UID via BT remote socket.
  • 根据这个信息,在Galaxy S21的相关组件中我们可以找到下面的代码,极有可能和该漏洞相关(其中省略了部分代码)
public void startSocket(Context context2) {
    byte[] buffer = new byte[8192];
    byte[] newBuffer = new byte[8192];
    int maxLength = 0;
    ACUtil.log_i(CLASS_NAME, "startSocket");
    this.readSocket = new BufferedReader(new InputStreamReader(this.is, Charset.forName("UTF-8")));
    ACUtil.log_i(CLASS_NAME, "readSocket", "readSocket: " + this.readSocket);
    this.isRunning = true;
    while (true) {
        try {
            int read = this.is.read(buffer);   // Read data from bluetooth socket
            //...
            ACUtil.log_i(CLASS_NAME, "Socket", "Receive " + read + " bytes: " + Support.Functions.byteArrayToHexString(buffer, read));
            if (this.isFileMode) {
                //...
            } else if (this.isMetaMode) {
                //...
            } else {
                System.arraycopy(buffer, 0, newBuffer, maxLength, read);
                maxLength += read;
                if (newBuffer[maxLength - 1] == 126) {
                    //...
                } else if (maxLength > 1 && newBuffer[maxLength - 2] == 13 && newBuffer[maxLength - 1] == 10) {
                    ACUtil.log_i(CLASS_NAME, "Socket", "string");
                    byte[] fullBuffer2 = new byte[maxLength];
                    System.arraycopy(newBuffer, 0, fullBuffer2, 0, maxLength);
                    String str3 = new String(fullBuffer2, 0, maxLength, "UTF-8");
                    if (str3.toUpperCase().contains("AT+FACMFILE")) {
                        this.isFileMode = true;
                        String subStr = str3.substring(str3.indexOf("=") + 1, str3.length() - 2);
                        this.fileName = subStr.split(",")[0];
                        this.fileLength = Integer.parseInt(subStr.split(",")[1]);
                        ACUtil.log_i(CLASS_NAME, "Socket", "name: " + this.fileName + " length: " + this.fileLength);
                        File file = new File(savePath);
                        if (!file.exists()) {
                            file.mkdirs();      // Create directory
                        }
                        File file2 = new File(savePath + this.fileName);
                        ACUtil.log_d(CLASS_NAME, "Socket", "Save: " + file2.getPath());
                        if (this.fileLength != 0) {
                            this.fos = new FileOutputStream(file2);   // Write file
                            this.mHandler.sendEmptyMessage(0);
                        }
                        maxLength = 0;
                        Arrays.fill(newBuffer, (byte) 0);
                    } else {
                        //...
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            BluetoothService.mHandler.sendEmptyMessageDelayed(1000, 3000);
        }
    }
    //...
}
  • 可以看到这里实际上是一个蓝牙的Socket接口,接收了相关的参数并进行的文件写入操作,这里面存在两个问题:
    • 该Socket接口并没有任何的权限校验
    • 在写入文件的过程中存在路径穿越问题,会导致任意文件写入
  • 同时更严重的问题是,这个应用使用的是system uid运行,同时SELinux标签是system_app,这是一个特权应用。

Double Kill——CVE-2021-25450 of Samsung

  • 在三星的相同组件中还存在第二个漏洞,下面是三星官方的披露信息。
Path traversal vulnerability in FactoryAirCommnadManger prior to SMR Sep-2021 Release 1 allows attackers to write file as system uid via remote socket.
  • 同样地我们找到了下面的代码,极有可能和该漏洞相关(其中省略了部分代码)
public Void doInBackground(Void... voids) {
    File checkFolder;
    int n;
    try {
        this.dis = new DataInputStream(this.socket.getInputStream());
    } catch (Exception e) {
        e.printStackTrace();
    }
    while (true) {
        ACUtil.log_d(CLASS_NAME, "doInBackground", "Wait for data...");
        try {
            //...
            while (true) {
                if (i >= count || isCancelled()) {
                    break;
                }
                String fileName = this.dis.readUTF();  // Read file name
                String filePath = this.dis.readUTF();  // Read file path
                long transferSize = this.dis.readLong();  // Read file size
                ACUtil.log_d(CLASS_NAME, "doInBackground", "transferSize: " + transferSize);
                String filePath2 = savePath + this.uniqueNumber + "/" + folderCount + "/" + filePath;
                File file = new File(filePath2);
                ACUtil.log_d(CLASS_NAME, "doInBackground", "filePath: " + filePath2);
                ACUtil.log_d(CLASS_NAME, "doInBackground", "getCanonicalPath: " + file.getCanonicalPath());
                if (!(file.getCanonicalPath() + '/').equals(filePath2)) {
                    cancel(true);
                    break;
                }
                if (!file.exists()) {
                    file.mkdirs();   // Create directory
                }
                File file2 = new File(filePath2 + fileName);
                if (!file2.getCanonicalFile().toString().equals(filePath2 + fileName)) {
                    cancel(true);
                    break;
                }
                ACUtil.log_d(CLASS_NAME, "doInBackground", "Save: " + file2.getCanonicalFile());
                this.fos = new FileOutputStream(file2);
                byte[] b = new byte[1024];
                long fileSize = transferSize;
                long totalSize = 0;
                while (fileSize > 0 && (n = this.dis.read(b, 0, (int) Math.min((long) b.length, fileSize))) != -1) {
                    this.fos.write(b, 0, n);   // Write file
                    fileSize -= (long) n;
                    totalSize += (long) n;
                }
                ACUtil.log_d(CLASS_NAME, "doInBackground", "totalSize: " + totalSize);
                ACUtil.log_d(CLASS_NAME, "doInBackground", "Save: " + (i + 1) + "/" + count);
                this.context.sendBroadcast(new Intent(WifiCommandService.PROGRESS_CHANGE).putExtra("value", i + 1));
                this.fos.close();
                i++;
            }
            this.context.sendBroadcast(new Intent(WifiCommandService.PROGRESS_FINISH).putExtra("path", checkFolder.getPath()));
        } catch (RuntimeException e2) {
            throw e2;
        } catch (Exception e3) {
            e3.printStackTrace();
        }
    }
    ACUtil.log_d(CLASS_NAME, "doInBackground", "Data Input Stream is null");
    try {
        if (this.fos != null) {
            this.fos.close();
            ACUtil.log_d(CLASS_NAME, "doInBackground", "fos close");
        }
        if (this.dis != null) {
            this.dis.close();
            ACUtil.log_d(CLASS_NAME, "doInBackground", "dis close");
        }
    } catch (Exception e4) {
        e4.printStackTrace();
    }
    this.context.sendBroadcast(new Intent(WifiCommandService.PROGRESS_FINISH));
    if (isCancelled()) {
        return null;
    }
    this.context.sendBroadcast(new Intent(WifiCommandService.START_FILE_RECEIVE));
    return null;
}
  • 和上面的漏洞非常类似,这里是一个网络Socket接口,和漏洞披露的信息也是对的上的,也同样是接收了相关的参数并进行的文件写入操作,这里面也存在两个问题:
    • 该Socket接口并没有任何的权限校验
    • 在写入文件的过程中存在路径穿越问题,会导致任意文件写入
  • 当然,该代码依旧是特权应用的一部分,不再赘述。

Triple kill——CVE-2021-23243 of OPPO

  • 其实魔形女系列漏洞还包括一个OPPO的漏洞,但是由于我并没有OPPO的设备,并且对OPPO也不是很感兴趣,所以不深入分析,我估计原理和上两个漏洞应该也是非常类似的,都是system_app的路径穿越导致的任意文件写入。

如何将文件写入转化为代码执行

  • 说了这么多我们只是做到了system_app标签权限的任意文件写入,即使是通过远程进行攻击,我们也无法窃取设备上的任何数据,那么如何将这类漏洞转化为RCE(远程代码执行)呢?先看一个前人的案例
  • 该案例是Google Project Zero早在2015年的时候就发现的,当时是为了写另一个CVE-2015-7889漏洞的利用,也是三星的漏洞。这个方法是通过写/data/dalvik-cache下面的dex文件实现的,这样可以把文件写转化为代码执行
flame:/data/dalvik-cache # ls -lZ
total 51
drwx--x--x 2 root root u:object_r:dalvikcache_data_file:s0   3488 2021-11-04 10:22 arm
drwx--x--x 2 root root u:object_r:dalvikcache_data_file:s0  53248 2021-11-04 10:22 arm64
  • 当然了现在已经不能用了,因为现在已经限制了不允许app标签的进程写dalvikcache_data_file
  • 其实在2021年初的时候我手里也有其他Android厂商的几个类似的路径穿越漏洞(具体暂时无法进行披露),正在思考如何利用,因为Android里面文件系统的限制,只有/data分区是可写分区,其他的都需要有root权限并且进行remount才能操作,所以我当时的关注点也是/data分区下的文件。
  • 我们先确定一般的思路,就像是上面的利用一样,我们要企图去写入覆盖一个dex或者so文件,甚至是odex和vdex文件也行,因为这些文件是Android平台上的可执行文件,只要覆盖了如果其存在自动执行的场景,就能转化为代码执行。但是说起来容易做起来难,Android有严格的SELinux限制,我当时检查了所有可能包含这类文件的路径,都没有发现可以允许system_app标签写入的目录。直到…

Google的迷之操作——CVE-2021-0691

  • 这个漏洞披露的时候我十分震惊,因为这其实就是我看了好几个月的地方
 # Settings need to access app name and icon from asec
 allow system_app asec_apk_file:file r_file_perms;
 
-# Allow system_app (adb data loader) to write data to /data/incremental
-allow system_app apk_data_file:file write;
-
-# Allow system app (adb data loader) to read logs
-allow system_app incremental_control_file:file r_file_perms;
-
 # Allow system apps (like Settings) to interact with statsd
 binder_call(system_app, statsd)
  • apk_data_file正是各种应用的apk文件的标签,Google居然允许system_app对apk_data_file执行文件写入操作。我的天哪!我那几个月都在看些什么呢?但是后来我发现了问题所在——这个问题只在Android 11中存在,而我当时攻击的目标,是基于Android 10的ROM,而这段代码是Android 11升级的时候才加入的,我完美错过了这个漏洞!
-rw-r--r-- 1 system system  u:object_r:apk_data_file:s0          114976454 2021-11-12 16:01 base.apk
drwxr-xr-x 3 system system  u:object_r:apk_data_file:s0               3488 2021-11-12 16:01 lib
  • 后面的事情就顺理成章了,我们只需要覆盖任意一个会自启动的apk文件即可,后面就可以为所欲为了,就像开头说的一样。

后记

  • 以上漏洞均在Google 2021 #9公告,三星的2021 #9和#10公告中得到了修复,并且该套利用只影响Android 11版本,虽然对于Google来说这个问题非常严重,因为是最新的版本。但是实际上对于最终用户而言,因为Android 11的手机实在是非常少,官方数据是<1%,所以实际受影响的最终用户倒不是那么的多。
  • 当然最遗憾的是,我错过了这个漏洞,不过这在安全研究中也是非常正常的。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注