tp2 - tersafe2update工作分析

/data/data/包名/files/libtersafeupdate2.so 创建该文件即可触发逻辑

CRC32校验

相同的目录有个16字节的文件tss_up_flag.dat

其中的v12(汇编内为w20)就是crc32的值

阶段1 替换特定字符串

阶段1不具备什么验证,具体代码就是下面的了

    // Stage A: patch all "#*0789#" occurrences by writing "12345678" at [match+7]
    if (filesize > 7) {
        const uint64_t patch8 = a12345678z_qword; // "12345678"
        for (uint32_t off = 0; off + 7 <= filesize - 1; ++off) {
            if (memcmp(map_base + off, NEEDLE7, 7) == 0) {
                *(uint64_t*)(map_base + off + 7) = patch8;
            }
        }
    }

阶段2 寻找hdr地址

 extern void*  dec_str_func_sub_4f841c(uint32_t key);         // 0x7851 -> 16 bytes ("ASS_SDK_SIGDATA"), then sig24[0]='T'
 
	  uint8_t sig24[24] = {0};
    {
        const uint8_t* p16 = (const uint8_t*)dec_str_func_sub_4f841c(0x7851);
        memcpy(sig24, p16, 16);
        sig24[0] = 'T';
    }

    // Find header marker (stride 0x100 then linear)
    uint8_t* match = NULL;
    if (filesize > 0x18) {
        for (uint32_t off = 0; off + 0x18 <= filesize; off += 0x100) {
            if (memcmp(map_base + off, sig24, 24) == 0) { match = map_base + off; break; }
        }
        if (!match) {
            for (uint32_t off = 0; off + 0x18 <= filesize; ++off) {
                if (memcmp(map_base + off, sig24, 24) == 0) { match = map_base + off; break; }
            }
        }
    }
    if (!match) return RET_NO_MARKER;

阶段2就是在寻找TSS_SDK_SIGDATA 开头大小为0x150的一个验证hdr

    const unsigned char TSS_SIG24_FULL[0x150 + 24] = {
        0x54,0x53,0x53,0x5F,0x53,0x44,0x4B,0x5F,0x53,0x49,0x47,0x44,0x41,0x54,0x41,0x00,
        0x00,0x00,0x00,0x00,  // flags = 0
        0x50,0x01,0x00,0x00,   // length = 0x150 (LE)
        // padding to 0x150
        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
      };

我们可以这样构造一个出来,这样就能通过阶段2的验证了

阶段3 清空hdr区域,对整个so文件进行sha1(标准)

    uint32_t flags  = *(uint32_t*)(match + 0x10);
    uint32_t length = *(uint32_t*)(match + 0x14);
    if (flags != 0) return RET_HDR_FLAG;
    if (length != 0x150) {
        // Length mismatch: fall into the same error as signature path does in your asm (via loc_1D4984)
        return RET_HDR_FLAG; // conservative: treat as unsupported header
    }

    // Copy 0x150 header to a safe buffer and wipe original area in map
    uint8_t hdr[0x150];
    memcpy(hdr, match, 0x150);
    memset(match, 0, 0x150);

    // Compute SHA-1 over the entire mapped file
    uint8_t sha20[20];
    sha1_encode(map_base, filesize, sha20);
    if (a3 > 0x13 && out_sha1_opt) {
        memcpy(out_sha1_opt, sha20, 20);
    }

阶段4 RSA解密HDR+0x13C部分

解密出来的部分才是sha1的真正的值,使用与内置公钥 unk_533950 匹配的私钥对 EM 进行签名(EM 的布局满足解密后 digest 在最后 20 字节,当前实现不做 PKCS#1 v1.5 全结构检查,但仍需要能让 y=x^e mod N 的结果在 +0x13C 放入 digest,这本质上还是“用私钥签名”问题)

    const void* key = pubkey_opt ? pubkey_opt : (const void*)/*builtin*/(uintptr_t)0x533950; // unk_533950
    int ok = sub_4984EC(key, &hdr[0x50], &hdr[0x50], 0x100);
    if (ok == 0) return RET_SIG_FAIL;

    // Compare SHA-1 against header's stored digest at +0x13C
    const uint8_t* expect = &hdr[0x13C];
    if (memcmp(sha20, expect, 20) == 0) {
        return 1;
    }

没有私钥,仅写 hdr+0x13C 为 SHA‑1 无法通过;因为解密会覆盖这 20 字节。

想要返回 1 (返回1,则tp2会dlopen该so文件):要么用对应私钥生成匹配签名,要么切换到你掌控的公钥并用你的私钥签名,要么在运行时修改/绕过对比逻辑。