华为 P9 Lite TrustedCore TA 解密

议题 Unearthing the TrustedCore: A Critical Review on Huawei’s Trusted Execution Environment 的子部分,其分析的TEE方案是华为自研的TrustedCore,解密TA的大概流程主要有三步:(1)模拟运行TEE中的白盒密码算法解出RSA私钥prikeyx。(2)使用RSA的私钥prikeyx解密TA头部的manifest。(3)使用解密后manifest中的AES key 解密 TA 正文。但作者没有给出解密过程中的一些细节,例如RSA和AES密钥的组织方法。所以我尝试复现了这个解密,并给出解密过程中的所有细节。
image

固件解包

目标固件VNS-L31C432B160是2016年的版本,也是作者开源在github上的相关工具tckit的示例固件。固件解压后的UPDATE.APP可以使用在之前CVE-2021-39994:HUAWEI SMC SE Factory Check OOB Access中的提到过的Android Image Tools的emui_extractor进行提取,此工具在我本机的mac环境下编译此工具需要添加指定C++11的编译选项-std=c++11:

emui_extractor: image.h image.cc emui_extractor.cc
        g++ -std=c++11 -o emui_extractor image.cc emui_extractor.cc

.PHONY: clean
clean:
        rm emui_extractor

然后即可使用其list子功能查看UPDATE.APP中的不同镜像:

  ./emui_extractor ./UPDATE.APP list
=====================================================================
Sequence            File.img            Size            Type   Device
=====================================================================
fe000000       SHA256RSA.img       256.00  B       SHA256RSA   HW7x27
fe000000             CRC.img       197.10 KB             CRC   HW7x27
fffffff0          CURVER.img        15.00  B          CURVER   HW7x27
fffffff1         VERLIST.img         3.14 KB         VERLIST   HW7x27
00000000             EFI.img        17.00 KB             EFI   HW7x27
00000018         XLOADER.img        69.25 KB         XLOADER   HW7x27
00000017         FW_LPM3.img       164.44 KB         FW_LPM3   HW7x27
00000013        FASTBOOT.img         2.53 MB        FASTBOOT   HW7x27
00000016 MODEMNVM_UPDATE.img        19.97 MB MODEMNVM_UPDATE   HW7x27
0000001a           TEEOS.img         2.30 MB           TEEOS   HW7x27
00000012   TRUSTFIRMWARE.img        94.56 KB   TRUSTFIRMWARE   HW7x27
00000019       SENSORHUB.img       416.44 KB       SENSORHUB   HW7x27
00000014         FW_HIFI.img         2.51 MB         FW_HIFI   HW7x27
0000000c            BOOT.img        13.67 MB            BOOT   HW7x27
0000000a        RECOVERY.img        32.34 MB        RECOVERY   HW7x27
0000000a       RECOVERY2.img        32.34 MB       RECOVERY2   HW7x27
00000008             DTS.img        18.12 MB             DTS   HW7x27
00000011        MODEM_FW.img        96.00 MB        MODEM_FW   HW7x27
0000000e           CACHE.img         6.10 MB           CACHE   HW7x27
0000000d          SYSTEM.img         2.38 GB          SYSTEM   HW7x27
0000000f            CUST.img       418.13 MB            CUST   HW7x27
00000010        USERDATA.img        67.33 MB        USERDATA   HW7x27
=====================================================================

主要关注两个镜像:

  • TEEOS.img:TEEOS和静态TA,其中包含对动态TA解密的关键代码和数据
  • SYSTEM.img:Android侧文件系统,其中包含加密后的动态TA

将二者提取出来:

  ./emui_extractor ./UPDATE.APP dump TEEOS.img TEEOS.img
  ./emui_extractor ./UPDATE.APP dump SYSTEM.img SYSTEM.img

在TEEOS.img中可以找到关键字符串TrustedCore,这就是华为TEE方案的名字:

  strings ./TEEOS.img | grep -i core
Copy All Tasks in TrustedCore
****************** All Tasks in TrustedCore *******************
can not reloc this symbol, symbol is not defined by trustedcore
TrustedCore do not support FingerPrint in this platform, initCapture return error. ret =0x%x
Core ID
TrustedCore Release Version %s, %s.%s

SYSTEM.img是android文件系统的主要部分,加密后的TA就在其中,但其格式是 Android sparse image,还需要再次进行处理:

➜  file SYSTEM.img
SYSTEM.img: Android sparse image, version: 1.0, Total of 655320 4096-byte output blocks in 4845 input chunks.

使用https://github.com/anestisb/android-simg2img,将其转化为正常的ext4文件系统格式:

  ./simg2img ./SYSTEM.img ./system.ext4
  file ./system.ext4
./system.ext4: Linux rev 1.0 ext4 filesystem data, volume name "system" (extents) (large files)

然后在linux中挂载此文件系统即可:

  mkdir rootfs
  sudo mount ./system.ext4 ./rootfs
  ls ./rootfs
app  build.prop  emui    fonts      global  lib64       phone.prop  themes  vendor
asr  cdrom       etc     fpgaice40  hw_oem  lost+found  priv-app    tts     watermark
bin  delapp      extras  framework  lib     media       screenlock  usr     xbin

加密的TA就在bin目录下,是以UUID格式和.sec后缀为名字的文件:

  ls -al ./rootfs/bin | grep sec
-rw-r-----.  1 root xuan   12756 11  9  2016 6c8cf255-ca98-439e-a98e-ade64022ecb6.sec
-rw-r-----.  1 root xuan   26696 11  9  2016 79b77788-9789-4a7a-a2be-b60155eef5f4.sec
-rw-r-----.  1 root xuan    9012 11  9  2016 868ccafb-794b-46c6-b5c4-9f1462de4e02.sec
-rw-r-----.  1 root xuan  417224 11  9  2016 883890ba-3ef8-4f0b-9c02-f5874acbf2ff.sec
-rw-r-----.  1 root xuan   23940 11  9  2016 9b17660b-8968-4eed-917e-dd32379bd548.sec
-rw-r-----.  1 root xuan   41500 11  9  2016 b4b71581-add2-e89f-d536-f35436dc7973.sec
-rw-r-----.  1 root 1004   12776 11  9  2016 fd1bbfb2-9a62-4b27-8fdb-a503529076af.sec
-rw-r-----.  1 root 1004 1326040 11  9  2016 fpc_1021_ta.sec
-rw-r-----.  1 root 1004 1329000 11  9  2016 fpc_1021_ta_venus.sec
-rw-r-----.  1 root 1004 1208040 11  9  2016 fpc_1022_ta.sec
-rwxr-x---.  1 root root   10096 11  9  2016 secure_storage
-rw-r-----.  1 root 1004 1711896 11  9  2016 syna_109A0_ta.sec

给出示例,之后以868ccafb-794b-46c6-b5c4-9f1462de4e02.sec为例,目标就是解密这个TA,使用binwalk可以看出这个目标确实是加密的:

  binwalk -E ./868ccafb-794b-46c6-b5c4-9f1462de4e02.sec 

DECIMAL       HEXADECIMAL     ENTROPY
--------------------------------------------------------------------------------
0             0x0             Rising entropy edge (0.968520)

image

接下来就用TEEOS.img来解密868ccafb-794b-46c6-b5c4-9f1462de4e02.sec:

拆分TEEOS.img

使用https://github.com/teesec-research/tckit(python2),即可从TEEOS.img拆出RSA的私钥,首先使用splitteeos.py从TEEOS.img拆分出globaltask这个PTA:

  python2 tckit/splitteeos/splitteeos.py ./TEEOS.img
  ls -al ./tas_out
drwxrwxr-x 2 xuan xuan    4096 6   4 15:12 .
drwxrwxr-x 4 xuan xuan    4096 6   4 15:12 ..
-rw-rw-r-- 1 xuan xuan 1642624 6   4 15:12 globaltask
-rw-rw-r-- 1 xuan xuan   18212 6   4 15:12 task_gatekeeper
-rw-rw-r-- 1 xuan xuan   75216 6   4 15:12 task_keyboard
-rw-rw-r-- 1 xuan xuan  107132 6   4 15:12 task_keymaster
-rw-rw-r-- 1 xuan xuan   14792 6   4 15:12 task_reet
-rw-rw-r-- 1 xuan xuan   10232 6   4 15:12 task_secboot
-rw-rw-r-- 1 xuan xuan   15540 6   4 15:12 task_storage

然后使用globaltaskgencode.py从拆分出的globaltask中,提取白盒密码算法代码,并封装为C代码:

  python2 tckit/globaltaskgencode.py ./tas_out/globaltask
  file tas_out/globaltask_extract_keys.c 
tas_out/globaltask_extract_keys.c: ASCII text

编译并使用QEMU模拟运行此白盒密码算法:

  arm-linux-gnueabihf-gcc tas_out/globaltask_extract_keys.c -o test 
  qemu-arm -L /usr/arm-linux-gnueabihf ./test 

运行后即可打印RSA私钥:

private_key = '\xcc\x29\xd6\x21\xb9\x86\xab\xa7\x13\xa7\xa2\x61\x06\x32\x1b\x33\x8d\xd1\x12\xd8\x6f\x36\x14\xaa\x39\xcd\x1c\xd5\x9b\x1d\xf1\xfd\x5a\x17\x58\xea\x64\xc5\x3d\x76\xcb\xce\x2a\x12\x04\x23\xf7\x78\x89\xbe\x63\x5b\xa1\xd4\x0b\x22\xb8\x78\x2a\x9c\xc3\xdd\xbf\xeb\xc2\xd1\x59\x53\x2b\x07\xaf\x45\x54\x90\x37\xae\xe9\x7b\x24\x57\x42\x68\x44\x59\xce\x72\xe7\x68\xfc\x07\xae\xa7\xcd\xdb\x87\x9b\x4f\x3b\x8c\x49\xfe\xe2\x66\xbd\xc8\x77\x89\x0d\xc6\xba\x07\xac\x7a\x9f\xc0\x84\x25\xa8\x62\x66\x55\xf7\xae\x43\x68\x15\xe1\xcd\x66\x7f\x62\x77\x8f\xf2\xe2\x5e\x80\xe9\x9a\x05\xe7\xdc\x63\xf7\x9f\xed\x24\xee\xef\xf6\x50\xad\x9d\x53\x32\x74\xb2\xe9\x77\xc1\xdf\xe6\xf4\xc6\xc8\x4c\x95\xac\xfc\x68\xc6\x8a\x40\xf5\xe5\x99\xe8\x5d\x62\xf8\x6f\xe8\x4a\xa6\xe5\xc1\xbe\x72\xf1\x8a\x74\x7d\x76\x3b\xd3\xb8\x53\xdf\x20\x12\x35\x96\x29\x15\x30\x82\x19\xb6\x13\x89\x70\x22\x08\xd7\x57\x76\x31\xae\xff\xe2\xbb\x5e\xc6\x58\x0d\xa8\x18\x26\x38\x58\x72\xfe\x2f\x11\xcc\xcd\xdd\x93\xbd\x60\x82\x33\x3e\x05\x75\x4d\x52\x1a\xc5\x85\xc1\xef\x0a\xd6\x6c\xe9\x22\x41\x21\xbc\xa3\x79\xea\x2e\xd1\x40\xd3\xcc\xd2\x75\xbb\xb4\x05\x86\x91\x7a\x17\xf9\xc2\xd5\x40\x63\xbb\xe0\x60\xb8\xaa\x85\xc9\x3e\x83\x19\xca\xfe\x1c\xd9\x17\x3c\x4c\x51\xc1\xa0\xa0\xd3\xbd\x7f\xa5\xd1\x91\xec\x6d\x03\x8c\x80\x8d\xe6\x7f\xf5\x7f\xba'
public_key = '\xc4\x11\xcc\x98\xce\x92\x0d\x78\x7e\xbb\x7a\xa4\xff\x5f\x60\x82\xa9\x4d\xb5\xe7\x75\x9d\xfd\x7d\xa7\xcf\xa3\xbb\x25\x83\xf5\x24\xf9\x31\x65\x5a\x5c\xea\xab\x88\xb9\x1b\x94\xc8\x5a\x75\x44\x4e\x17\x50\x76\xa5\xa4\x39\xdd\x79\x5b\xf4\xcc\xd0\x11\xb8\x52\xe0\x8d\x4f\x49\xd4\x6b\xb8\x5b\x4a\xdf\x51\x53\xef\x61\x75\xc4\x43\xe1\xfd\x8c\x18\x9a\x3f\x45\x11\x69\x31\xb4\x9c\x2c\x2f\xdb\x5a\xe9\x09\x4d\x99\xf5\xdc\x95\x34\xde\x1a\xc6\x34\xd7\xbf\x86\x27\xce\x94\x4f\xc8\x03\xd1\x47\x24\x02\x8e\x49\x0b\x22\xe6\x82\x42\xf9\xa7\x1b\x85\x29\xb1\x90\x4e\x22\xd0\x48\x4a\x56\x63\xee\x93\x75\x9d\x25\xbc\x02\xa0\x23\x55\xe6\xd4\x67\xa0\x76\x22\x23\x8b\x5a\x7b\x4d\x24\x7a\x28\x71\x83\x4c\xc0\xa1\x28\x9c\x14\x45\x47\x75\xdb\x12\x42\xfd\x94\x05\xc6\xa3\xb9\xcc\xf7\x48\x8c\xe9\x55\xac\x1f\x01\xca\x6b\xc5\xe5\x1c\xa8\xf4\xc7\x09\x7d\x5b\x4b\x2c\x1d\xcb\xc5\x4e\x12\xfb\x46\x76\x23\xb2\x58\x94\x4b\x7b\x66\xbb\xbb\x18\x8b\x7e\x10\x3c\xbc\x2d\xd8\x1d\x0d\xa2\x7b\x60\xa6\xb9\x1a\x20\x40\x96\xa2\x01\xc9\x09\x06\xe6\xb6\xb0\x7f\x44\xa3\xa9\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'

私钥的解析以及后续的解密TA,需要逆向globaltask以及TEEOS本身,可以使用globaltask2elf.py恢复globaltask的ELF结构:

  python2 tckit/globaltask2elf.py ./tas_out/globaltask
  file ./tas_out/globaltask.elf 
globaltask.elf: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, not stripped

以及使用tos2elf.py恢复TEEOS的ELF结构:

  python2 tckit/tos2elf.py ./TEEOS.img
  file Rtosck
Rtosck: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), no program header, not stripped

接下来就对这两个ELF进行逆向,主要是globaltask.elf:

RSA私钥解析

根据作者PPT,可以确定QEMU运行打印出的两个密钥中第一个私钥(x)为解密TA所需要的关键密钥:

image

但对于这个RSA的私钥解析,可以看出来这里的打印的公私钥没有什么openssl的格式,也就不是标准格式公私钥数据,所以猜测其打印的数据就是p、q、n、e、d等RSA相关数据,因此需要逆向分析解析私钥的代码。

猜测格式

但这里的密钥数据其实也直接可以通过公私钥的特性猜出来(就当CTF做),首先可以将私钥数据保存成二进制文件方便查看:

private_key  = b'\xcc\x29\xd6\x21\xb9\x86\xab\xa7\x13\xa7\xa2\x61\x06\x32\x1b\x33\x8d\xd1\x12\xd8'
private_key += b'\x6f\x36\x14\xaa\x39\xcd\x1c\xd5\x9b\x1d\xf1\xfd\x5a\x17\x58\xea\x64\xc5\x3d\x76'
private_key += b'\xcb\xce\x2a\x12\x04\x23\xf7\x78\x89\xbe\x63\x5b\xa1\xd4\x0b\x22\xb8\x78\x2a\x9c'
private_key += b'\xc3\xdd\xbf\xeb\xc2\xd1\x59\x53\x2b\x07\xaf\x45\x54\x90\x37\xae\xe9\x7b\x24\x57'
private_key += b'\x42\x68\x44\x59\xce\x72\xe7\x68\xfc\x07\xae\xa7\xcd\xdb\x87\x9b\x4f\x3b\x8c\x49'
private_key += b'\xfe\xe2\x66\xbd\xc8\x77\x89\x0d\xc6\xba\x07\xac\x7a\x9f\xc0\x84\x25\xa8\x62\x66'
private_key += b'\x55\xf7\xae\x43\x68\x15\xe1\xcd\x66\x7f\x62\x77\x8f\xf2\xe2\x5e\x80\xe9\x9a\x05'
private_key += b'\xe7\xdc\x63\xf7\x9f\xed\x24\xee\xef\xf6\x50\xad\x9d\x53\x32\x74\xb2\xe9\x77\xc1'
private_key += b'\xdf\xe6\xf4\xc6\xc8\x4c\x95\xac\xfc\x68\xc6\x8a\x40\xf5\xe5\x99\xe8\x5d\x62\xf8'
private_key += b'\x6f\xe8\x4a\xa6\xe5\xc1\xbe\x72\xf1\x8a\x74\x7d\x76\x3b\xd3\xb8\x53\xdf\x20\x12'
private_key += b'\x35\x96\x29\x15\x30\x82\x19\xb6\x13\x89\x70\x22\x08\xd7\x57\x76\x31\xae\xff\xe2'
private_key += b'\xbb\x5e\xc6\x58\x0d\xa8\x18\x26\x38\x58\x72\xfe\x2f\x11\xcc\xcd\xdd\x93\xbd\x60'
private_key += b'\x82\x33\x3e\x05\x75\x4d\x52\x1a\xc5\x85\xc1\xef\x0a\xd6\x6c\xe9\x22\x41\x21\xbc'
private_key += b'\xa3\x79\xea\x2e\xd1\x40\xd3\xcc\xd2\x75\xbb\xb4\x05\x86\x91\x7a\x17\xf9\xc2\xd5'
private_key += b'\x40\x63\xbb\xe0\x60\xb8\xaa\x85\xc9\x3e\x83\x19\xca\xfe\x1c\xd9\x17\x3c\x4c\x51'
private_key += b'\xc1\xa0\xa0\xd3\xbd\x7f\xa5\xd1\x91\xec\x6d\x03\x8c\x80\x8d\xe6\x7f\xf5\x7f\xba'
open('private_key.bin','wb').write(private_key)

使用010editor对私钥数据相面,可以看出来其长度总共0x140字节:

image

对于RSA的私钥,在理论上一般表达为以下两种:

  • n、d
  • p、q、dp、dq、qinv (中国剩余定理优化的RSA计算 RSA-CRT)

关于RSA的中国剩余定理:

但在实际中,RSA私钥一般以PKCS#1和PKCS#8格式存储(ASN.1),其中不仅包括理论上的私钥数据,还包含公钥(n、e),例如使用openssl生成私钥包括(n、e、d、p、q、dp、dq、qinv),对应关系如下:

ASN.1 key structures in DER and PEM

RSAPrivateKey ::= SEQUENCE {
    version Version,
    modulus INTEGER, -- n
    publicExponent INTEGER, -- e
    privateExponent INTEGER, -- d
    prime1 INTEGER, -- p
    prime2 INTEGER, -- q
    exponent1 INTEGER, -- dp: d mod (p-1)
    exponent2 INTEGER, -- dq: d mod (q-1)
    coefficient INTEGER, -- qinv: (inverse of q) mod p
    otherPrimeInfos OtherPrimeInfos OPTIONAL
}

可以自己使用openssl生成一个1024bit的RSA私钥并查看:

  openssl genrsa -out rsa_private_key.pem 1024
Generating RSA private key, 1024 bit long modulus
.........++++++
......................++++++
e is 65537 (0x10001)

  openssl rsa -in ./rsa_private_key.pem -text 
Private-Key: (1024 bit)
modulus:
    00:d7:1f:f9:0f:d4:f8:00:91:fd:9e:d9:66:b1:12:
    d4:73:20:ee:50:06:8a:e9:f7:28:d6:e0:76:78:60:
    e6:96:cc:a0:4b:db:78:bb:56:8d:ae:0f:d9:33:d2:
    82:92:11:49:25:83:67:58:77:93:b5:68:4c:ec:7c:
    d6:4c:b1:9f:18:ce:93:c7:3e:d4:b4:cc:26:ec:59:
    1f:8c:aa:52:b8:9b:9f:e0:86:8a:53:65:6c:47:0f:
    de:40:bb:8a:28:53:df:81:da:61:97:32:56:3a:80:
    40:8c:27:38:58:3a:97:09:f7:8b:d7:59:37:c2:2c:
    39:8e:2d:c5:cd:30:b8:35:01
publicExponent: 65537 (0x10001)
privateExponent:
    26:b7:8f:68:c5:08:99:79:ac:ee:b0:eb:e5:84:a1:
    0d:d3:68:70:a8:ac:c9:ac:fd:01:a7:46:4b:26:0d:
    7a:28:7b:d5:0b:3b:f0:63:84:7e:46:45:ee:28:bd:
    ed:32:05:3b:26:2a:2c:66:e1:03:ae:30:e2:03:19:
    c2:95:d9:2f:16:2e:1e:b1:34:11:8b:22:02:e5:1e:
    ef:0e:21:59:fc:c1:6b:9a:da:24:eb:98:2d:fd:a0:
    19:2a:e1:1d:3e:a6:2b:af:03:48:14:f9:69:b0:73:
    ba:f7:c6:05:db:f7:ad:5a:b2:3e:80:3e:9f:2d:54:
    5a:2c:33:81:20:58:a5:11
prime1:
    00:f7:c1:40:a3:b5:20:a0:7d:f9:e7:38:df:ec:4d:
    2d:f1:17:00:23:d9:fa:ff:03:c1:e8:78:01:36:2c:
    a9:55:3f:67:d6:89:93:ca:b7:bf:b1:a1:2b:aa:90:
    1a:1c:a6:5a:df:2b:b4:4f:6d:60:04:45:30:b4:cc:
    3b:3f:07:5c:95
prime2:
    00:de:48:ba:aa:c6:f7:65:ba:65:84:6b:1c:04:92:
    a8:c4:76:55:57:d2:0e:86:60:53:31:fd:34:68:b0:
    1e:f5:85:b9:51:b8:0a:84:a5:ff:23:d8:73:19:1f:
    51:c2:66:fc:70:c8:fc:32:f0:5b:41:56:3d:61:c2:
    2c:91:43:af:bd
exponent1:
    1b:9c:a6:1f:98:a8:32:3a:d8:07:35:07:7f:c6:7a:
    40:4c:57:ef:a6:f3:9a:48:48:ec:27:b3:ba:dd:ef:
    61:58:d7:b1:c9:53:77:5c:53:38:f0:c5:75:14:ea:
    54:17:16:39:99:1d:57:5c:d1:3e:a8:97:6d:0e:f5:
    eb:68:5e:a1
exponent2:
    00:93:de:93:f6:f9:87:28:70:38:0a:3f:ea:92:8c:
    31:a3:08:09:3b:f3:ab:df:ee:82:49:b5:e4:40:64:
    31:24:29:82:1f:7f:ab:d7:94:49:c7:41:bd:47:90:
    13:26:9c:b6:00:1d:63:d0:4b:1e:99:b7:51:fc:0f:
    5c:f0:81:b3:8d
coefficient:
    00:e8:5b:63:26:f9:a2:e7:d2:08:c4:36:25:8c:64:
    8f:69:e6:6c:94:54:dd:8f:ff:d3:e4:56:cb:dd:b1:
    f5:4b:d1:a4:6e:44:9c:7f:77:ae:61:4f:49:e2:a0:
    96:ec:72:05:6b:3d:1a:22:13:a9:49:a1:f1:4b:dc:
    fe:5c:90:66:f0
writing RSA key
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDXH/kP1PgAkf2e2WaxEtRzIO5QBorp9yjW4HZ4YOaWzKBL23i7
Vo2uD9kz0oKSEUklg2dYd5O1aEzsfNZMsZ8YzpPHPtS0zCbsWR+MqlK4m5/ghopT
ZWxHD95Au4ooU9+B2mGXMlY6gECMJzhYOpcJ94vXWTfCLDmOLcXNMLg1AQIDAQAB
AoGAJrePaMUImXms7rDr5YShDdNocKisyaz9AadGSyYNeih71Qs78GOEfkZF7ii9
7TIFOyYqLGbhA64w4gMZwpXZLxYuHrE0EYsiAuUe7w4hWfzBa5raJOuYLf2gGSrh
HT6mK68DSBT5abBzuvfGBdv3rVqyPoA+ny1UWiwzgSBYpRECQQD3wUCjtSCgffnn
ON/sTS3xFwAj2fr/A8HoeAE2LKlVP2fWiZPKt7+xoSuqkBocplrfK7RPbWAERTC0
zDs/B1yVAkEA3ki6qsb3ZbplhGscBJKoxHZVV9IOhmBTMf00aLAe9YW5UbgKhKX/
I9hzGR9Rwmb8cMj8MvBbQVY9YcIskUOvvQJAG5ymH5ioMjrYBzUHf8Z6QExX76bz
mkhI7Cezut3vYVjXsclTd1xTOPDFdRTqVBcWOZkdV1zRPqiXbQ7162heoQJBAJPe
k/b5hyhwOAo/6pKMMaMICTvzq9/ugkm15EBkMSQpgh9/q9eUScdBvUeQEyactgAd
Y9BLHpm3UfwPXPCBs40CQQDoW2Mm+aLn0gjENiWMZI9p5myUVN2P/9PkVsvdsfVL
0aRuRJx/d65hT0nioJbscgVrPRoiE6lJofFL3P5ckGbw
-----END RSA PRIVATE KEY-----

那以上导出的这0x140字节的RSA私钥应该包含什么了呢?

  • 如果是n、d,那么n、d各0xa0字节,换算为bit为1280,并非512、1024,不太合理
  • 如果是p、q、dp、dq、qinv,则其各64字节,换算为bit为512,可以为RSA 1024,较为合理

那是不是我们猜的p、q、dp、dq、qinv呢?如果是,这里为什么不包含e?因为按照RSA原始的计算方法:

e * d  1 (mod (p-1)*(q-1))

没有e也就不知道d,也就不能按照原始的办法解密。但其实中国剩余定理优化出的RSA计算方法中多出来的3个数据,即通过dp、dq、qinv,可以和p、q结合直接解密密文,而不需要e的直接参与。并且dp、dq、qinv是和e相关的,可以通过p和dp计算出e。其中dp 不是d*p,而是定义为 d mod(p-1),并且dp、e、p满足一个关系,dp与e在mod(p-1)下互为逆元,即:

dp * e  1 (mod (p-1))

自己证明了一下(我也不知道证的对不对):

image

那我们就可以假设0x140字节的私钥数据是p、q、dp、dq、qinv,然后计算一下e:

from Crypto.Util.number import bytes_to_long
import gmpy2

private_key  = b'\xcc\x29\xd6\x21\xb9\x86\xab\xa7\x13\xa7\xa2\x61\x06\x32\x1b\x33\x8d\xd1\x12\xd8'
private_key += b'\x6f\x36\x14\xaa\x39\xcd\x1c\xd5\x9b\x1d\xf1\xfd\x5a\x17\x58\xea\x64\xc5\x3d\x76'
private_key += b'\xcb\xce\x2a\x12\x04\x23\xf7\x78\x89\xbe\x63\x5b\xa1\xd4\x0b\x22\xb8\x78\x2a\x9c'
private_key += b'\xc3\xdd\xbf\xeb\xc2\xd1\x59\x53\x2b\x07\xaf\x45\x54\x90\x37\xae\xe9\x7b\x24\x57'
private_key += b'\x42\x68\x44\x59\xce\x72\xe7\x68\xfc\x07\xae\xa7\xcd\xdb\x87\x9b\x4f\x3b\x8c\x49'
private_key += b'\xfe\xe2\x66\xbd\xc8\x77\x89\x0d\xc6\xba\x07\xac\x7a\x9f\xc0\x84\x25\xa8\x62\x66'
private_key += b'\x55\xf7\xae\x43\x68\x15\xe1\xcd\x66\x7f\x62\x77\x8f\xf2\xe2\x5e\x80\xe9\x9a\x05'
private_key += b'\xe7\xdc\x63\xf7\x9f\xed\x24\xee\xef\xf6\x50\xad\x9d\x53\x32\x74\xb2\xe9\x77\xc1'
private_key += b'\xdf\xe6\xf4\xc6\xc8\x4c\x95\xac\xfc\x68\xc6\x8a\x40\xf5\xe5\x99\xe8\x5d\x62\xf8'
private_key += b'\x6f\xe8\x4a\xa6\xe5\xc1\xbe\x72\xf1\x8a\x74\x7d\x76\x3b\xd3\xb8\x53\xdf\x20\x12'
private_key += b'\x35\x96\x29\x15\x30\x82\x19\xb6\x13\x89\x70\x22\x08\xd7\x57\x76\x31\xae\xff\xe2'
private_key += b'\xbb\x5e\xc6\x58\x0d\xa8\x18\x26\x38\x58\x72\xfe\x2f\x11\xcc\xcd\xdd\x93\xbd\x60'
private_key += b'\x82\x33\x3e\x05\x75\x4d\x52\x1a\xc5\x85\xc1\xef\x0a\xd6\x6c\xe9\x22\x41\x21\xbc'
private_key += b'\xa3\x79\xea\x2e\xd1\x40\xd3\xcc\xd2\x75\xbb\xb4\x05\x86\x91\x7a\x17\xf9\xc2\xd5'
private_key += b'\x40\x63\xbb\xe0\x60\xb8\xaa\x85\xc9\x3e\x83\x19\xca\xfe\x1c\xd9\x17\x3c\x4c\x51'
private_key += b'\xc1\xa0\xa0\xd3\xbd\x7f\xa5\xd1\x91\xec\x6d\x03\x8c\x80\x8d\xe6\x7f\xf5\x7f\xba'

p  = bytes_to_long(private_key[:64])
dp = bytes_to_long(private_key[128:192])
e  = gmpy2.invert(dp,(p-1))

print(hex(e))

结果为0x10001,就是常用的e,因此:

  • 以上导出的这0x140字节的RSA私钥,确实是p、q、dp、dq、qinv各64字节
  • dp与e在mod(p-1)下确实互为逆元

在CTF中也有在不给p、q的情况下,仅通过n、e、dp来破解RSA的题目,大概原理就是利用dp与e在mod(p-1)下互为逆元的性质,将其变化为等式:

 dp * e  1 (mod (p-1))  ->  dp * e = 1 + k * (p-1) 】(k为整数)

因为dp定义为d mod (p-1),所以dp小于p-1,因此k = [dp/(p-1)] * e - 1的取值范围为[-1,e-1)。题目一般给出e为0x10001,因此可以爆破k,计算对应的p,当p可以被n整除时,成功拆分n为p和q,即可破解RSA:

但现在我们这里不用这么麻烦,因为已经有p、q、e了,所以我们继续可以算出d,然后就可以对密文进行解密了(当然纯用p、q、dp、dq、qinv也可以解密):

from Crypto.Util.number import bytes_to_long
from Crypto.Util.number import long_to_bytes
import gmpy2

private_key  = b'\xcc\x29\xd6\x21\xb9\x86\xab\xa7\x13\xa7\xa2\x61\x06\x32\x1b\x33\x8d\xd1\x12\xd8'
private_key += b'\x6f\x36\x14\xaa\x39\xcd\x1c\xd5\x9b\x1d\xf1\xfd\x5a\x17\x58\xea\x64\xc5\x3d\x76'
private_key += b'\xcb\xce\x2a\x12\x04\x23\xf7\x78\x89\xbe\x63\x5b\xa1\xd4\x0b\x22\xb8\x78\x2a\x9c'
private_key += b'\xc3\xdd\xbf\xeb\xc2\xd1\x59\x53\x2b\x07\xaf\x45\x54\x90\x37\xae\xe9\x7b\x24\x57'
private_key += b'\x42\x68\x44\x59\xce\x72\xe7\x68\xfc\x07\xae\xa7\xcd\xdb\x87\x9b\x4f\x3b\x8c\x49'
private_key += b'\xfe\xe2\x66\xbd\xc8\x77\x89\x0d\xc6\xba\x07\xac\x7a\x9f\xc0\x84\x25\xa8\x62\x66'
private_key += b'\x55\xf7\xae\x43\x68\x15\xe1\xcd\x66\x7f\x62\x77\x8f\xf2\xe2\x5e\x80\xe9\x9a\x05'
private_key += b'\xe7\xdc\x63\xf7\x9f\xed\x24\xee\xef\xf6\x50\xad\x9d\x53\x32\x74\xb2\xe9\x77\xc1'
private_key += b'\xdf\xe6\xf4\xc6\xc8\x4c\x95\xac\xfc\x68\xc6\x8a\x40\xf5\xe5\x99\xe8\x5d\x62\xf8'
private_key += b'\x6f\xe8\x4a\xa6\xe5\xc1\xbe\x72\xf1\x8a\x74\x7d\x76\x3b\xd3\xb8\x53\xdf\x20\x12'
private_key += b'\x35\x96\x29\x15\x30\x82\x19\xb6\x13\x89\x70\x22\x08\xd7\x57\x76\x31\xae\xff\xe2'
private_key += b'\xbb\x5e\xc6\x58\x0d\xa8\x18\x26\x38\x58\x72\xfe\x2f\x11\xcc\xcd\xdd\x93\xbd\x60'
private_key += b'\x82\x33\x3e\x05\x75\x4d\x52\x1a\xc5\x85\xc1\xef\x0a\xd6\x6c\xe9\x22\x41\x21\xbc'
private_key += b'\xa3\x79\xea\x2e\xd1\x40\xd3\xcc\xd2\x75\xbb\xb4\x05\x86\x91\x7a\x17\xf9\xc2\xd5'
private_key += b'\x40\x63\xbb\xe0\x60\xb8\xaa\x85\xc9\x3e\x83\x19\xca\xfe\x1c\xd9\x17\x3c\x4c\x51'
private_key += b'\xc1\xa0\xa0\xd3\xbd\x7f\xa5\xd1\x91\xec\x6d\x03\x8c\x80\x8d\xe6\x7f\xf5\x7f\xba'

p  = bytes_to_long(private_key[:64])
q  = bytes_to_long(private_key[64:128])
dp = bytes_to_long(private_key[128:192])
e  = gmpy2.invert(dp,(p-1))
d  = gmpy2.invert(e,(p-1)*(q-1))

print(len(long_to_bytes(d)))

其他参考:

逆向确定

当然也可以通过逆向找到这个RSA私钥的格式,从作者给出的封装代码中可以看出这个RSA私钥是通过白盒密码算法对ciphertext1(还是带符号的变量)解密得到的:

https://github.com/teesec-research/tckit/blob/master/globaltaskgencode.py

char *ciphertext1 = data + """ + hex(sym_map['ciphertext1']) + """;
char *ciphertext2 = data + """ + hex(sym_map['ciphertext2']) + """;

所以可以从globaltask.elf中对ciphertext1变量的使用出发进行逆向:

  strings globaltask.elf | grep ciphertext1
ciphertext1

可以通过IDA的Names或者Exports窗口搜索此符号,找到ciphertext1变量:

image

然后交叉引用可以跟到get_dx_private_key函数,通过对其中的_CC_CRYS_RSA_Build_PrivKeyCRT函数名字(CRT:中国剩余定理)以及参数,也可以确定RSA私钥的划分,其中的65字节是因为解密后的第一组数据是拷贝的起始地址是buf(v17)第二个字节,原因可能是:Leading 00 in RSA public/private key file

image

所以通过这个参数也能看出来私钥的0x140字节应该是被划成了五份,所以应该是p、q、dp、dq、qinv。如果想继续跟进_CC_CRYS_RSA_Build_PrivKeyCRT分析会发现进了系统调用,所以需要继续逆向TEEOS,即转换成名为Rtosck的ELF文件,在其中可以找到CRYS_RSA_Build_PrivKeyCRT函数,虽然逆向这个函数一眼看不太出来什么:

image

但CRYS_RSA_Build_PrivKeyCRT这个函数可搜到,是mbed-os的库函数,通过函数定义中的参数可以彻底确定私钥私钥的组成确实和猜测一致:

https://os.mbed.com/docs/mbed-os/v6.15/mbed-os-api-doxy/group__crys__rsa__build.html

CRYSError_t CRYS_RSA_Build_PrivKeyCRT	(	CRYS_RSAUserPrivKey_t * 	UserPrivKey_ptr,
uint8_t * 	P_ptr,
uint16_t 	PSize,
uint8_t * 	Q_ptr,
uint16_t 	QSize,
uint8_t * 	dP_ptr,
uint16_t 	dPSize,
uint8_t * 	dQ_ptr,
uint16_t 	dQSize,
uint8_t * 	qInv_ptr,
uint16_t 	qInvSize 
)	

RSA解密manifest

现在即可使用RSA来解密TA头部的manifest,不过在解密之前我们还是通过010editor看一看TA,可以发现开头的0x18字节比较干净,应该不是加密的数据。从0x98字节开始是个字符串,所以也肯定没加密。因此 0x18 到 0x98偏移这0x80字节的数据比较乱,应该就是加密的manifest,也正好是128字节,满足RSA 1024的密文情况:

image

通过之前逆向globaltask.elf中的get_dx_private_key函数往上交叉引用 ,可以找到load_secure_app_image函数,其中调用parse_manifest函数的参数中有一个立即数0x18,这个偏移和我们推测的TA中加密的manifest的偏移一致:

image

跟入parse_manifest函数,可以看到立即数128,和我们推测的manifest密文长度也一致:

image

所以直接尝试使用RSA解密TA开头偏移0x18,长度为0x80字节的数据:

from Crypto.Util.number import bytes_to_long
from Crypto.Util.number import long_to_bytes
import gmpy2

private_key  = b'\xcc\x29\xd6\x21\xb9\x86\xab\xa7\x13\xa7\xa2\x61\x06\x32\x1b\x33\x8d\xd1\x12\xd8'
private_key += b'\x6f\x36\x14\xaa\x39\xcd\x1c\xd5\x9b\x1d\xf1\xfd\x5a\x17\x58\xea\x64\xc5\x3d\x76'
private_key += b'\xcb\xce\x2a\x12\x04\x23\xf7\x78\x89\xbe\x63\x5b\xa1\xd4\x0b\x22\xb8\x78\x2a\x9c'
private_key += b'\xc3\xdd\xbf\xeb\xc2\xd1\x59\x53\x2b\x07\xaf\x45\x54\x90\x37\xae\xe9\x7b\x24\x57'
private_key += b'\x42\x68\x44\x59\xce\x72\xe7\x68\xfc\x07\xae\xa7\xcd\xdb\x87\x9b\x4f\x3b\x8c\x49'
private_key += b'\xfe\xe2\x66\xbd\xc8\x77\x89\x0d\xc6\xba\x07\xac\x7a\x9f\xc0\x84\x25\xa8\x62\x66'
private_key += b'\x55\xf7\xae\x43\x68\x15\xe1\xcd\x66\x7f\x62\x77\x8f\xf2\xe2\x5e\x80\xe9\x9a\x05'
private_key += b'\xe7\xdc\x63\xf7\x9f\xed\x24\xee\xef\xf6\x50\xad\x9d\x53\x32\x74\xb2\xe9\x77\xc1'
private_key += b'\xdf\xe6\xf4\xc6\xc8\x4c\x95\xac\xfc\x68\xc6\x8a\x40\xf5\xe5\x99\xe8\x5d\x62\xf8'
private_key += b'\x6f\xe8\x4a\xa6\xe5\xc1\xbe\x72\xf1\x8a\x74\x7d\x76\x3b\xd3\xb8\x53\xdf\x20\x12'
private_key += b'\x35\x96\x29\x15\x30\x82\x19\xb6\x13\x89\x70\x22\x08\xd7\x57\x76\x31\xae\xff\xe2'
private_key += b'\xbb\x5e\xc6\x58\x0d\xa8\x18\x26\x38\x58\x72\xfe\x2f\x11\xcc\xcd\xdd\x93\xbd\x60'
private_key += b'\x82\x33\x3e\x05\x75\x4d\x52\x1a\xc5\x85\xc1\xef\x0a\xd6\x6c\xe9\x22\x41\x21\xbc'
private_key += b'\xa3\x79\xea\x2e\xd1\x40\xd3\xcc\xd2\x75\xbb\xb4\x05\x86\x91\x7a\x17\xf9\xc2\xd5'
private_key += b'\x40\x63\xbb\xe0\x60\xb8\xaa\x85\xc9\x3e\x83\x19\xca\xfe\x1c\xd9\x17\x3c\x4c\x51'
private_key += b'\xc1\xa0\xa0\xd3\xbd\x7f\xa5\xd1\x91\xec\x6d\x03\x8c\x80\x8d\xe6\x7f\xf5\x7f\xba'

p  = bytes_to_long(private_key[:64])
q  = bytes_to_long(private_key[64:128])
dp = bytes_to_long(private_key[128:192])
e  = gmpy2.invert(dp,(p-1))
d  = gmpy2.invert(e,(p-1)*(q-1))
n  = p * q

c = bytes_to_long(open('868ccafb-794b-46c6-b5c4-9f1462de4e02.sec','rb').read()[0x18:0x18+0x80])
m = long_to_bytes(pow(c,d,n))

print(len(m))
print(m.hex())

解密后的数据如下,通过其中连片的00字节,就可以判断出解密成功了:

127

02f7c9f892ef5dbb32fe00fbca8c864b79c646b5c49f1462de4e0201000000000000000000000000400100002000000000000020000000200000001900000031a4679966df8337fb391a9b4bcf1695dbba3037ce89b876165298ac9ab783c813b20f304fb0d2d0e61e37a2fdd6835756803d7ae2367b27ff17dab70f422daf

但需要注意,解密后的数据的开头是02,这是PKCS#1 v1.5 的 padding,也叫 RSA_PKCS1_PADDING,是在数据头部填充padding:

image

所以需要把解密数据开头的0x02 到 0x00 的数据去掉:

from Crypto.Util.number import bytes_to_long
from Crypto.Util.number import long_to_bytes
import gmpy2

private_key  = b'\xcc\x29\xd6\x21\xb9\x86\xab\xa7\x13\xa7\xa2\x61\x06\x32\x1b\x33\x8d\xd1\x12\xd8'
private_key += b'\x6f\x36\x14\xaa\x39\xcd\x1c\xd5\x9b\x1d\xf1\xfd\x5a\x17\x58\xea\x64\xc5\x3d\x76'
private_key += b'\xcb\xce\x2a\x12\x04\x23\xf7\x78\x89\xbe\x63\x5b\xa1\xd4\x0b\x22\xb8\x78\x2a\x9c'
private_key += b'\xc3\xdd\xbf\xeb\xc2\xd1\x59\x53\x2b\x07\xaf\x45\x54\x90\x37\xae\xe9\x7b\x24\x57'
private_key += b'\x42\x68\x44\x59\xce\x72\xe7\x68\xfc\x07\xae\xa7\xcd\xdb\x87\x9b\x4f\x3b\x8c\x49'
private_key += b'\xfe\xe2\x66\xbd\xc8\x77\x89\x0d\xc6\xba\x07\xac\x7a\x9f\xc0\x84\x25\xa8\x62\x66'
private_key += b'\x55\xf7\xae\x43\x68\x15\xe1\xcd\x66\x7f\x62\x77\x8f\xf2\xe2\x5e\x80\xe9\x9a\x05'
private_key += b'\xe7\xdc\x63\xf7\x9f\xed\x24\xee\xef\xf6\x50\xad\x9d\x53\x32\x74\xb2\xe9\x77\xc1'
private_key += b'\xdf\xe6\xf4\xc6\xc8\x4c\x95\xac\xfc\x68\xc6\x8a\x40\xf5\xe5\x99\xe8\x5d\x62\xf8'
private_key += b'\x6f\xe8\x4a\xa6\xe5\xc1\xbe\x72\xf1\x8a\x74\x7d\x76\x3b\xd3\xb8\x53\xdf\x20\x12'
private_key += b'\x35\x96\x29\x15\x30\x82\x19\xb6\x13\x89\x70\x22\x08\xd7\x57\x76\x31\xae\xff\xe2'
private_key += b'\xbb\x5e\xc6\x58\x0d\xa8\x18\x26\x38\x58\x72\xfe\x2f\x11\xcc\xcd\xdd\x93\xbd\x60'
private_key += b'\x82\x33\x3e\x05\x75\x4d\x52\x1a\xc5\x85\xc1\xef\x0a\xd6\x6c\xe9\x22\x41\x21\xbc'
private_key += b'\xa3\x79\xea\x2e\xd1\x40\xd3\xcc\xd2\x75\xbb\xb4\x05\x86\x91\x7a\x17\xf9\xc2\xd5'
private_key += b'\x40\x63\xbb\xe0\x60\xb8\xaa\x85\xc9\x3e\x83\x19\xca\xfe\x1c\xd9\x17\x3c\x4c\x51'
private_key += b'\xc1\xa0\xa0\xd3\xbd\x7f\xa5\xd1\x91\xec\x6d\x03\x8c\x80\x8d\xe6\x7f\xf5\x7f\xba'

p  = bytes_to_long(private_key[:64])
q  = bytes_to_long(private_key[64:128])
dp = bytes_to_long(private_key[128:192])
e  = gmpy2.invert(dp,(p-1))
d  = gmpy2.invert(e,(p-1)*(q-1))
n  = p * q

c = bytes_to_long(open('868ccafb-794b-46c6-b5c4-9f1462de4e02.sec','rb').read()[0x18:0x18+128])
m = long_to_bytes(pow(c,d,n))
i = m.find(b'\x00') + 1

open('manifest.bin','wb').write(m[i:])

最终得到的明文manifest,长度0x74字节,具体如下:

manifest.bin

image

接下来就是从这0x74字节的明文manifest找出AES加密的KEY,并且其中也许还包含AES加密的IV。另外还需要确定868ccafb-794b-46c6-b5c4-9f1462de4e02.sec中加密的TA_ELF本体的位置。确定这些的事的方法就是逆向了,具体来说就是逆向globaltask.elf中的load_secure_app_image函数,以及其调用的两个关键函数:

  • parse_manifest:解密manifest并解析
  • elf_decry:对加密后的TA进行解密

image

加密TA ELF 本体的偏移

首先结合868ccafb-794b-46c6-b5c4-9f1462de4e02.sec分析load_secure_app_image,可以确定其调用elf_decry的第一个参数,就是加密TA文件中AES加密后TA_ELF的偏移,本例中偏移为0x1b4具体计算如下:

v15 = en_TA + 0x18;                  // 0x18
v20 = v15 + (0x99 & 0xFFFFFFFC) + 4; // 0x9c
v19 = *(_DWORD *)(en_TA + 0x14);     // 0x100

hex(0x18 + 0x9c + 0x100) == 0x1b4

因此load_secure_app_image函数与868ccafb-794b-46c6-b5c4-9f1462de4e02.sec的对应分析的方法如下:

  • *.sec文件头的0x18字节对应load_secure_app_image中解析出的一些全局变量
  • MANIFEST部分交给parse_manifest函数处理,并在其中也会解析出一些全局变量
  • 解密TA_ELF交给elf_decry函数,但其会使用parse_manifest解析出的一些全局变量

image

AES: GP标准的TEE加密API

跟入elf_decry函数,可以看到其使用的密码函数是符合GP标准的TEE密码函数接口:

GlobalPlatform Technology TEE Internal Core API Specification Version 1.3.1

  • TEE_AllocateOperation
  • TEE_SetOperationKey
  • TEE_CipherInit

image

elf_decry调用TEE_AllocateOperation的第二个参数为0x10000110,通过TEE_AllocateOperation函数的定义:

TEE_Result TEE_AllocateOperation(TEE_OperationHandle* operation, 
                                 uint32_t algorithm, 
                                 uint32_t mode,
                                 uint32_t maxKeySize );

可以识别出其使用的加密算法为AES CBC (0x10000110):

optee_os/lib/libutee/include/tee_api_defines.h

#define TEE_ALG_AES_CBC_NOPAD               0x10000110

但如果对比OP-TEE的AES示例,会发现elf_decry这里少用了如下几个函数:

https://github.com/linaro-swg/optee_examples/blob/master/aes/ta/aes_ta.c

  • TEE_AllocateTransientObject
  • TEE_InitRefAttribute
  • TEE_PopulateTransientObject

可以在GP规范中找到以上涉及的函数定义,另外也可以在OP-TEE中找到如下定义:

https://elixir.bootlin.com/op-tee/latest/source/lib/libutee/include/tee_internal_api.h

TEE_Result TEE_AllocateTransientObject(TEE_ObjectType objectType, 
                                       uint32_t maxObjectSize, 
                                       TEE_ObjectHandle *object);

void TEE_InitRefAttribute(TEE_Attribute *attr,
                          uint32_t attributeID, 
                          const void *buffer, 
                          size_t length);

TEE_Result TEE_PopulateTransientObject(TEE_ObjectHandle object, 
                                       const TEE_Attribute *attrs, 
                                       uint32_t attrCount);

TEE_Result TEE_SetOperationKey(TEE_OperationHandle operation, 
                               TEE_ObjectHandle key);

void TEE_CipherInit(TEE_OperationHandle operation, 
                    const void *IV, 
                    size_t IVLen);

TEE_Result TEE_CipherUpdate(TEE_OperationHandle operation, 
                            const void *srcData,
                            size_t srcLen, 
                            void *destData, 
                            size_t *destLen);

在OP-TEE的AES示例中,这几个函数主要是设置AES加密密钥前序步骤。通过这几个函数,AES密钥即变量key,会通过变量attr,最终被设置到类型为 TEE_ObjectHandle 的 sess->key_handle,并传递给TEE_SetOperationKey函数:

TEE_AllocateTransientObject(TEE_TYPE_AES, sess->key_size * 8, &sess->key_handle);
TEE_InitRefAttribute(&attr, TEE_ATTR_SECRET_VALUE, key, sess->key_size);
TEE_PopulateTransientObject(sess->key_handle, &attr, 1);

TEE_SetOperationKey(sess->op_handle, sess->key_handle);

TEE_CipherInit(sess->op_handle, iv, iv_sz);
TEE_CipherUpdate(sess->op_handle, params[0].memref.buffer,  params[0].memref.size,
                                  params[1].memref.buffer, &params[1].memref.size);

但这种绕一大圈设置密钥的方法着实令人费解,TEE_SetOperationKey第二个参直接把AES密钥怼进去看着多清楚呀。但这种操作其实是GP标准中规定的TEE加密函数的使用方法,因此使用这种方法设置密钥肯定有他的道理,不理解肯定是我们的问题。关于TEE加密API的使用资料不多,除了这个OP-TEE的AES的示例代码以外,在黄书《手机安全和可信应用开发指南:TrustZone与OP-TEE技术详解》OP-TEE中的密码学算法(269-273页) 一章中,对OP-TEE中的AES算法有如下解释:

image

不过这里还是看不出来为什么要这么设计,但是如果再往下看一点,看到OP-TEE中的RSA算法的说明,就可以发现其前序设置密钥的函数调用过程和AES基本一致:

image

所以这么设计API的目的就是:让TEE中的加密函数有比较统一的接口封装。主要做法是:把不同加密方法的差异抽象为属性Attribute,通过TEE_PopulateTransientObject函数把属性传递给类型为 TEE_ObjectHandle 的变量,例如示例中的sess->key_handle。例如OP-TEE的AES示例代码中调用TEE_InitRefAttribute所使用的TEE_ATTR_SECRET_VALUE属性,可以在GP规范中找到说明,例如AES、DES、SM4、HMAC等需要设置类似对称密钥的密码算法,均使用此属性进行密钥设置:

GlobalPlatform Technology TEE Internal Core API Specification Version 1.3.1

image

而RSA等其他非对称密码算法,通过如TEE_ATTR_RSA_MODULUS、TEE_ATTR_RSA_PRIME1、TEE_ATTR_RSA_PRIME2等其他属性,设置公私钥:

image

因此GP规范中设计的各种属性Attribute,就是统一密码算法接口的关键。现在回到对elf_decry的逆向中,之前已经通过TEE_AllocateOperation函数确定AES使用的加密模式为CBC,还需确定AES加密所使用的IV和KEY。通过TEE_CipherInit函数,结合此函数的定义就可以确定IV相关的数据:

  • 全局变量dword_156764:就是IV的地址
  • 全局变量dword_156758:除以2就是IV的长度
void TEE_CipherInit(TEE_OperationHandle operation, const void *IV, size_t IVLen);

TEE_CipherInit(v18, dword_156764, dword_156758 / 2);

但对于AES的KEY,我们并没有在elf_decry中看到设置AES密钥属性的TEE_PopulateTransientObject函数,只调用了TEE_SetOperationKey函数,此函数定义如下:

TEE_Result TEE_SetOperationKey(TEE_OperationHandle operation, TEE_ObjectHandle key);

按照GP规范,TEE_SetOperationKey是使用已经绑定密钥的TEE_ObjectHandle进行密钥设置的最后操作,在逆向结果中已经绑定密钥TEE_ObjectHandle变量是v20,看起来是一个结构体,内容也很干净,和其相关的全局变量看起来也只有dword_156764和dword_156758,和之前确定的IV相关的全部变量完全一致。这就非常令人困惑了,那这俩全局变量到底是在表达KEY,还是在表达IV呢?

其实最后发现,他这里的IV就是复用了KEY的前16个字节…

image

这需要对v20这个结构体分析清楚后,才能更加明确。所以找一下TEE_ObjectHandle背后对应的结构体定义应该就可以分析明白了,目标结构体从名字上可以理解为TEE_ObjectHandle去掉Handle,就是找到一个差不多叫TEE_Object的结构体,但当我着手从TEE_ObjectHandle开始探索时,却陷入了困境…

TEE_ObjectHandle 到底是啥?

对于ARM TEE的相关函数、常量、结构体,有两种非常直接的理解方式:查GP标准和看OP-TEE的源码。不过当前目标的TEE方案并非OP-TEE,而是华为的TrustedCore,因此某些实现细节未必和OP-TEE一致,对于目标中的TEE_ObjectHandle实现理解,因为没有调试符号和源码,因此只能通过逆向理解。因此我们就从这三种角度来理解TEE_ObjectHandle,一切从TEE_SetOperationKey这个函数出发:

TEE_Result TEE_SetOperationKey(TEE_OperationHandle operation, TEE_ObjectHandle key);

逆向

逆向globaltask的elf_decry函数,分析关键变量如下:

  • 46行:根据TEE_SetOperationKey函数定义,v20的类型就是TEE_ObjectHandle,其值为栈上地址
  • 35行:v20[67]上存了v9,即一个堆地址,指向一个12字节的堆块(堆块1)
  • 41行:v9也即v17,即堆块1的4字节偏移处,又存了一个堆地址,指向v11大小字节的堆块(堆块2)
  • 39行:v11的值为全局变量dword_156758,也即堆块2的大小
  • 45行:v12,也即堆块2的地址,拷贝了全局变量dword_156764指向的内容,拷贝大小为dword_156758

因此v20指向的这段栈空间就可以理解为TEE_ObjectHandle指向的结构体,其67*4字节的偏移处的位置存放了堆块1的地址,堆块1中又存了堆块2的地址,在堆块2中存放了全局变量dword_156764指向的内容。而v20确实作为TEE_SetOperationKey函数设置密钥的关键变量,所以全局变量dword_156764指向的内容应该就是AES的密钥。那么v20的67*4字节偏移,和通过两层堆块保存密钥的结构,应该就是GP标准中设计出来的属性Attribute,在TrustedCore中的具体实现。

image

那么接下来我们就通过GP标准和OP-TEE来辅助证明一下这个逆向结果。

GP标准

在GP标准中定义TEE_ObjectHandle为结构体指针,但找不到目标结构体具体的定义:

GlobalPlatform Technology TEE Internal Core API Specification Version 1.3.1

image

还能找到一个各种Handle的表:

image

除此之外还能找到一张关于TEE_ObjectHandle状态变化表:

Figure 5-1: State Diagram for TEE_ObjectHandle (Informative)

image

但以上的这些表达都还是太抽象了,我就只是想知道TEE_ObjectHandle指针指向的结构体长啥样而已。但看起来GP标准无法直接回答我的问题,也就是在规范中没有定义这个结构体,所以接下来还是要去OP-TEE中寻找答案。

OP-TEE

找到源码中的TEE_ObjectHandle定义,符合GP标准,确实定义为结构体指针,其指向的结构体类型具体为__TEE_ObjectHandle:

https://elixir.bootlin.com/op-tee/latest/source/lib/libutee/include/tee_api_types.h#L84

typedef struct __TEE_ObjectHandle *TEE_ObjectHandle;

但是当我搜索__TEE_ObjectHandle这个结构体时,却发现没有其他定义了,整个OP-TEE OS里就这一处有这个玩意:

  optee_os git:(master) grep -ri "__TEE_ObjectHandle" ./
./lib/libutee/include/tee_api_types.h:typedef struct __TEE_ObjectHandle *TEE_ObjectHandle;

但在TEE_ObjectHandle附近定义的另一个结构体指针TEE_OperationHandle,却可以找到对应的结构体定义:

https://elixir.bootlin.com/op-tee/latest/source/lib/libutee/tee_api_operations.c#L19

typedef struct __TEE_OperationHandle *TEE_OperationHandle;

struct __TEE_OperationHandle {
	TEE_OperationInfo info;
	TEE_ObjectHandle key1;
	TEE_ObjectHandle key2;
	uint32_t operationState;/* Operation state : INITIAL or ACTIVE */

	/*
	 * buffer to collect complete blocks or to keep a complete digest
	 * for TEE_DigestExtract().
	 */
	uint8_t *buffer;
	bool buffer_two_blocks;	/* True if two blocks need to be buffered */
	size_t block_size;	/* Block size of cipher */
	size_t buffer_offs;	/* Offset in buffer */
	uint32_t state;		/* Handle to state in TEE Core */
};

这实在是太令人费解了,我就是想找个结构体定义而已,却从逆向开始一步步走向困境。逆向虽然确定了globaltask的v20变量(即TEE_ObjectHandle)和全局变量dword_156764相关,但却没有在GP标准中和OP-TEE中寻找到可以支持我们逆向结果的证据,并且在OP-TEE中还陷入了新的困惑:结构体__TEE_ObjectHandle的定义哪去了?如果真就是没有这个结构体的定义,那么定义一个指向其的指针是难不成是合法的?于是我使用如下代码进行测试:

#include <stdio.h>
typedef struct __TEE_ObjectHandle *TEE_ObjectHandle;

int main(){
    TEE_ObjectHandle a = 0;
    printf("[+] %p\n",a);
    return 0;
}

结果还真能编译运行:

  gcc test.c -o test && ./test
[+] 0x0

所以源码中确实没有结构体__TEE_ObjectHandle的定义的可能性是存在的。这其实就是GP规范中对TEE_ObjectHandle说明中提到的不透明指针:opaque handle,主要作用就是在闭源代码对外提供的接口处,隐藏实现细节:

但OP-TEE是开源的,没有必要隐藏实现。而且就算是隐藏实现,他也是先实现了定义,然后藏起来,但这里是压根没有定义。所以还是不知道OP-TEE使用不透明指针定义TEE_ObjectHandle的目的,但既然找不到结构体定义,那么我们就先找找这个TEE_ObjectHandle是怎么来的吧,因为他毕竟是个指针,总有初始化的地方。找到初始化TEE_ObjectHandle的函数TEE_AllocateTransientObject:

https://elixir.bootlin.com/op-tee/latest/source/lib/libutee/include/tee_internal_api.h#L173

TEE_Result TEE_AllocateTransientObject(TEE_ObjectType objectType, 
                                       uint32_t maxObjectSize, 
                                       TEE_ObjectHandle *object);

分析其实现,TEE_ObjectHandle这个指针在__GP11_TEE_AllocateTransientObject函数中赋值为变量obj,变量obj来自于_utee_cryp_obj_alloc函数:

https://elixir.bootlin.com/op-tee/latest/source/lib/libutee/tee_api_objects.c#L308

TEE_Result TEE_AllocateTransientObject(TEE_ObjectType objectType,
				       uint32_t maxObjectSize,
				       TEE_ObjectHandle *object)
{
	if (objectType == TEE_TYPE_DATA)
		return TEE_ERROR_NOT_SUPPORTED;

	return __GP11_TEE_AllocateTransientObject(objectType, maxObjectSize,
						  object);
}

TEE_Result __GP11_TEE_AllocateTransientObject(TEE_ObjectType objectType,
					      uint32_t maxKeySize,
					      TEE_ObjectHandle *object)
{
	TEE_Result res;
	uint32_t obj;

	__utee_check_out_annotation(object, sizeof(*object));

	res = _utee_cryp_obj_alloc(objectType, maxKeySize, &obj);

	if (res != TEE_SUCCESS &&
	    res != TEE_ERROR_OUT_OF_MEMORY &&
	    res != TEE_ERROR_NOT_SUPPORTED)
		TEE_Panic(res);

	if (res == TEE_SUCCESS)
		*object = (TEE_ObjectHandle)(uintptr_t)obj;

	return res;
}

_utee_cryp_obj_alloc会进入OP-TEE OS的系统调用,即此功能是OP-TEE OS内核实现的,对应的内核函数是syscall_cryp_obj_alloc。其传回给用户态的变量obj,来自于其中调用的copy_kaddr_to_uref,拷贝的内核变量为o,而变量o来自于tee_obj_alloc函数:

https://elixir.bootlin.com/op-tee/latest/source/core/tee/tee_svc_cryp.c#L1589

TEE_Result syscall_cryp_obj_alloc(unsigned long obj_type,
			unsigned long max_key_size, uint32_t *obj)
{
	struct ts_session *sess = ts_get_current_session();
	TEE_Result res = TEE_SUCCESS;
	struct tee_obj *o = NULL;


	o = tee_obj_alloc();
	if (!o)
		return TEE_ERROR_OUT_OF_MEMORY;

	res = tee_obj_set_type(o, obj_type, max_key_size);
	if (res != TEE_SUCCESS) {
		tee_obj_free(o);
		return res;
	}

	tee_obj_add(to_user_ta_ctx(sess->ctx), o);

	res = copy_kaddr_to_uref(obj, o);
	if (res != TEE_SUCCESS)
		tee_obj_close(to_user_ta_ctx(sess->ctx), o);
	return res;
}

变量o的类型为tee_obj,找到其定义如下,至此我们终于找到OP-TEE中TEE_ObjectHandle背后对应的结构体,妈的,居然在OP-TEE OS的内核里:

https://elixir.bootlin.com/op-tee/latest/source/core/include/tee/tee_obj.h#L16

struct tee_obj {
	TAILQ_ENTRY(tee_obj) link;
	TEE_ObjectInfo info;
	bool busy;		/* true if used by an operation */
	uint32_t have_attrs;	/* bitfield identifying set properties */
	void *attr;
	size_t ds_pos;
	struct tee_pobj *pobj;	/* ptr to persistant object */
	struct tee_file_handle *fh;
};

那难不成在OP-TEE 用户态TA中使用的指针TEE_ObjectHandle,指向了一个OP-TEE OS的内核地址?如果真是这样那不就直接泄露了内核地址么?在OP-TEE示例AES的TA代码中,进行如下修改,尝试打印TEE_ObjectHandle的值:

https://github.com/linaro-swg/optee_examples/blob/master/aes/ta/aes_ta.c

diff --git a/aes/ta/aes_ta.c b/aes/ta/aes_ta.c
index b259a01..bcb26d5 100644
--- a/aes/ta/aes_ta.c
+++ b/aes/ta/aes_ta.c
@@ -198,7 +198,7 @@ static TEE_Result alloc_resources(void *session, uint32_t param_types,
                EMSG("TEE_PopulateTransientObject failed, %x", res);
                goto err;
        }
-
+       EMSG("[+] 0x%x",sess->key_handle);
        res = TEE_SetOperationKey(sess->op_handle, sess->key_handle);
        if (res != TEE_SUCCESS) {
                EMSG("TEE_SetOperationKey failed %x", res);

然后编译运行,执行optee_example_aes,结果如下,可以见这个数显然不是内核地址:

E/TA:  alloc_resources:202 [+] 0x82950

重新分析syscall_cryp_obj_alloc内核函数中的copy_kaddr_to_uref,可见其将内核地址拷贝到用户态时,是通过kaddr_to_uref函数将内核地址进行了处理,所以用户态拿到并非直接的内核地址:

https://elixir.bootlin.com/op-tee/latest/source/core/kernel/user_access.c#L77

TEE_Result copy_kaddr_to_uref(uint32_t *uref, void *kaddr)
{
	uint32_t ref = kaddr_to_uref(kaddr);

	return copy_to_user_private(uref, &ref, sizeof(ref));
}


uint32_t kaddr_to_uref(void *kaddr)
{
	if (MEMTAG_IS_ENABLED) {
		unsigned int uref_tag_shift = 32 - MEMTAG_TAG_WIDTH;
		vaddr_t uref = memtag_strip_tag_vaddr(kaddr);

		uref -= VCORE_START_VA;
		assert(uref < (UINT32_MAX >> MEMTAG_TAG_WIDTH));
		uref |= memtag_get_tag(kaddr) << uref_tag_shift;
		return uref;
	}

	assert(((vaddr_t)kaddr - VCORE_START_VA) < UINT32_MAX);
	return (vaddr_t)kaddr - VCORE_START_VA;
}

所以仔细想想OP-TEE的TEE_ObjectHandle,虽然其定义为结构体指针,但OP-TEE根本不把他当一个C语言意义上的指针来使用,但确实符合其名字中的Handle,有点类似Linux中的文件描述符,其对应的更复杂更本质的结构体在内核中。所以可见OP-TEE中的TEE_ObjectHandle,和目标TrustedCore的TEE_ObjectHandle,有着完全不一样的实现:其背后对应的关键数据,如密钥,TrustedCore将其存放在用户态,OP-TEE存放在内核中。因此OP-TEE不能对本次对TrustedCore的结构体逆向结果提供证据支持。既然没有办法辅助证明逆向结果的正确与否,那我们就直接按照逆向结果,认为全局变量dword_156764指向的就是AES密钥,继续往下走。

全局变量的逆向

通过之前的逆向我们认为:

  • AES的KEY:内容为dword_156764指向的数据,大小为dword_156758
  • AES 的 IV:内容为dword_156764指向的数据,大小为dword_156758除2

IDA里改个名继续逆向:

  • dword_156764,改名为key_iv
  • dword_156758,改名为key_iv_size

关注key_iv,其还在parse_manifest中被使用:

image

继续分析parse_manifest,也就是RSA解密manifest并解析的处理函数:

  • 44行:key_iv也即v14,是TEE_Malloc出的一个堆块地址,堆块大小为key_iv_size
  • 51行:key_iv也即v14,拷贝自v13,大小为key_iv_size
  • 43行:v13,指向AES KEY,是v2 + dword_156754 + 52 加出来的

image

所以只要把v2和dword_156754分析明白即可,根据v2是manifest_rsa_decry函数的参数可以推测,v2就指向解密完的manifest的开头。按照这个推测,变量img_info就是解密后manifest的前四个字节:

image

仔细分析代码可以确定,img_info以及其之后的数据就是由parse_manifest中的while循环解析出来的,因此这部分内存很有可能就是解密后的manifest:

image

尝试计算dword_156754和img_info偏移差为0x28:

>>> hex(0x0156754 - 0x015672C)
'0x28'

因此按照此偏移解析manifest:

  • dword_156754: 0x20
  • dword_156758: 0x20 (key_iv_size)

image

因此拷贝到v14的v13,推测为AES KEY,其值为 v2 + dword_156754 + 52,即manifest开头处偏移 0x20 + 52 字节,即0x54(84)字节,因此按照此位置解析明文manifest中AES的KEY和IV,大小分别为32字节和16字节:

from Crypto.Cipher import AES

iv     =  open('manifest.bin','rb').read()[84:84+16]
key    =  open('manifest.bin','rb').read()[84:84+32]
cipher =  open('868ccafb-794b-46c6-b5c4-9f1462de4e02.sec','rb').read()[0x1b4:0x1b4+32]

a = AES.new(key, AES.MODE_CBC,iv)
msg = a.decrypt(cipher)
print(msg)

成功解密出ELF,证明了以上我们连蒙带猜的逆向结果是正确的:

  python3 exp.py
b'\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00'

完整解密脚本

from Crypto.Util.number import bytes_to_long
from Crypto.Util.number import long_to_bytes
from Crypto.Cipher import AES
import gmpy2

private_key  = b'\xcc\x29\xd6\x21\xb9\x86\xab\xa7\x13\xa7\xa2\x61\x06\x32\x1b\x33\x8d\xd1\x12\xd8'
private_key += b'\x6f\x36\x14\xaa\x39\xcd\x1c\xd5\x9b\x1d\xf1\xfd\x5a\x17\x58\xea\x64\xc5\x3d\x76'
private_key += b'\xcb\xce\x2a\x12\x04\x23\xf7\x78\x89\xbe\x63\x5b\xa1\xd4\x0b\x22\xb8\x78\x2a\x9c'
private_key += b'\xc3\xdd\xbf\xeb\xc2\xd1\x59\x53\x2b\x07\xaf\x45\x54\x90\x37\xae\xe9\x7b\x24\x57'
private_key += b'\x42\x68\x44\x59\xce\x72\xe7\x68\xfc\x07\xae\xa7\xcd\xdb\x87\x9b\x4f\x3b\x8c\x49'
private_key += b'\xfe\xe2\x66\xbd\xc8\x77\x89\x0d\xc6\xba\x07\xac\x7a\x9f\xc0\x84\x25\xa8\x62\x66'
private_key += b'\x55\xf7\xae\x43\x68\x15\xe1\xcd\x66\x7f\x62\x77\x8f\xf2\xe2\x5e\x80\xe9\x9a\x05'
private_key += b'\xe7\xdc\x63\xf7\x9f\xed\x24\xee\xef\xf6\x50\xad\x9d\x53\x32\x74\xb2\xe9\x77\xc1'
private_key += b'\xdf\xe6\xf4\xc6\xc8\x4c\x95\xac\xfc\x68\xc6\x8a\x40\xf5\xe5\x99\xe8\x5d\x62\xf8'
private_key += b'\x6f\xe8\x4a\xa6\xe5\xc1\xbe\x72\xf1\x8a\x74\x7d\x76\x3b\xd3\xb8\x53\xdf\x20\x12'
private_key += b'\x35\x96\x29\x15\x30\x82\x19\xb6\x13\x89\x70\x22\x08\xd7\x57\x76\x31\xae\xff\xe2'
private_key += b'\xbb\x5e\xc6\x58\x0d\xa8\x18\x26\x38\x58\x72\xfe\x2f\x11\xcc\xcd\xdd\x93\xbd\x60'
private_key += b'\x82\x33\x3e\x05\x75\x4d\x52\x1a\xc5\x85\xc1\xef\x0a\xd6\x6c\xe9\x22\x41\x21\xbc'
private_key += b'\xa3\x79\xea\x2e\xd1\x40\xd3\xcc\xd2\x75\xbb\xb4\x05\x86\x91\x7a\x17\xf9\xc2\xd5'
private_key += b'\x40\x63\xbb\xe0\x60\xb8\xaa\x85\xc9\x3e\x83\x19\xca\xfe\x1c\xd9\x17\x3c\x4c\x51'
private_key += b'\xc1\xa0\xa0\xd3\xbd\x7f\xa5\xd1\x91\xec\x6d\x03\x8c\x80\x8d\xe6\x7f\xf5\x7f\xba'

p  = bytes_to_long(private_key[:64])
q  = bytes_to_long(private_key[64:128])
dp = bytes_to_long(private_key[128:192])
e  = gmpy2.invert(dp,(p-1))
d  = gmpy2.invert(e,(p-1)*(q-1))
n  = p * q

c = bytes_to_long(open('868ccafb-794b-46c6-b5c4-9f1462de4e02.sec','rb').read()[0x18:0x18+128])
m = long_to_bytes(pow(c,d,n))
i = m.find(b'\x00') + 1

f = open('manifest.bin','wb')
f.write(m[i:])
f.close()

iv     =  open('manifest.bin','rb').read()[84:84+16]
key    =  open('manifest.bin','rb').read()[84:84+32]
cipher =  open('868ccafb-794b-46c6-b5c4-9f1462de4e02.sec','rb').read()[0x1b4:]

a   = AES.new(key, AES.MODE_CBC,iv)
msg = a.decrypt(cipher)
open('868ccafb-794b-46c6-b5c4-9f1462de4e02.elf','wb').write(msg)

可以发现解出来的TA是甚至是带符号的:

868ccafb-794b-46c6-b5c4-9f1462de4e02.elf

➜  file 868ccafb-794b-46c6-b5c4-9f1462de4e02.elf 
868ccafb-794b-46c6-b5c4-9f1462de4e02.elf: ELF 32-bit LSB relocatable, ARM, EABI5 version 1 (SYSV), not stripped

使用IDA逆向此TA可以发现确实有符号,但这个TA居然是可以调用外部函数的,例如SLog。这和OP-TEE的TA不同,OP-TEE的TA相当于静态链接,所有的函数都在静态链接到TA中,TA ELF 外部的功能只有通过svc系统调用进TEE OS完成:

image

回到华为 TrustedCore 方案中的TA,找到SLog函数的本体实现在globaltask中,所以此方案中globaltask和TA运行时应该在同一个内存环境下:

image

另外 P9 Lite 上这套 TrustedCore 方案的 TEE还是值得继续研究的,因为2016年极棒国外黑客Nick用鼻子解锁的正是这款P9 Lite,固件版本也和目前的示例相近:

Nick Stephens-how does someone unlock your phone with nose

image

P9 Lite是在国外上市的手机,不过闲鱼上也可以买到,我买了一个,但是系统版本在2020年,尝试使用以上的私钥无法解密其中的TA。应该可以刷低版本复现2016年极棒的漏洞,之后有缘再玩:

image

另外作者恢复globaltask和TEEOS的ELF结构的过程也还值得研究其处理过程,其他相关: