背景介绍
- 近日,京东探索研究院信息安全实验室的研究团队发现一项高危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的路径穿越导致的任意文件写入。
如何将文件写入转化为代码执行
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的手机占比约为24.3%,大概1/4的Android客户受影响(经评论区提示,已经根据Android 开发者信息分发中心的新数据进行更新)。