首页
社区
课程
招聘
[原创]如何用纯猜的方式逆向喜马拉雅xm文件加密(wasm部分)
2023-8-29 17:05 8664

[原创]如何用纯猜的方式逆向喜马拉雅xm文件加密(wasm部分)

2023-8-29 17:05
8664

原文链接 here

chatgpt翻译.jpg

前言

在我之前的文章中,我留下了一个关于 wasm 内部解密方法的疑问。今天,让我们再次深入逆向工程的世界,揭开代码下的神秘面纱。

现在,你们中的一些人可能会想知道为什么这篇文章的标题是纯粹的猜测。好吧,那是因为我在处理这个*'逆向'*挑战时,实际上并没有进行任何真正的逆向工程。

向前思考

如果存在一个解密算法,那么必然存在一个相应的加密机制。在这种情况下,webassembly 中导出的函数 h 就是我想要的加密方法。

与其解密对应部分相似,它需要两个参数:加密的数据和 trackId。

1
2
3
function f_h(a:{ a:byte, b:byte }, b:int, c:long_ptr, d:int, e:int) {
  var m:int;
  ...

让我们探索这种加密是如何工作的。

猜测加密

首先,我们需要一个脚本来测试不同的参数如何影响加密结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from wasmer import engine,Store, Module, Instance,Memory,Uint8Array,Int32Array
import io,sys,pathlib
import re,base64
 
 
xm_encryptor = Instance(Module(
    Store(),
    pathlib.Path("./xm_encryptor.wasm").read_bytes()
))
 
def encrypt(data, key):
    stack_pointer = xm_encryptor.exports.a(-16)
    assert isinstance(stack_pointer, int)
    de_data_offset = xm_encryptor.exports.c(len(data))
    assert isinstance(de_data_offset,int)
    track_id_offset = xm_encryptor.exports.c(len(key))
    assert isinstance(track_id_offset, int)
    memory_i = xm_encryptor.exports.i
    memview_unit8:Uint8Array = memory_i.uint8_view(offset=de_data_offset)
    for i,b in enumerate(data):
        memview_unit8[i] = b
    memview_unit8: Uint8Array = memory_i.uint8_view(offset=track_id_offset)
    for i,b in enumerate(key):
        memview_unit8[i] = b
    xm_encryptor.exports.h(stack_pointer,de_data_offset,len(data),track_id_offset,len(key))
    memview_int32: Int32Array = memory_i.int32_view(offset=stack_pointer // 4)
    result_pointer = memview_int32[0]
    result_length = memview_int32[1]
    assert memview_int32[2] == 0, memview_int32[3] == 0
    result_data = bytearray(memory_i.buffer)[result_pointer:result_pointer+result_length].decode()
    return result_data
 
for i in range(0x20):
    data = b'A'*0x10+b"CCCCCCCC"+b"DDDDDDDD"
    # track_id = b'E'*0x8+b'F'*0x8+b'G'*0x7 # max 24
    track_id = b'\x41' * i
    # track_id = b'E'*0x8
    print(hex(i),encrypt(data,track_id))

从结果中,我们可以观察到,当 track_id 达到0x18字节的长度时,结果保持不变。这意味着超过0x18的 track_id 的长度不影响结果。

1
2
3
4
5
0x16 NrVlG9gtu3MmpUlXK8gIxHD0Kh07iORGc6Dz5tLaLSUBSffF0/FU1vB8OmX921rP
0x17 2HiMLe5mRt4yHMs3WUtr7L0Zt6MG/lLaeK/0rSiTeUwlTEYF2e/Y7w+S3v75Kw65
0x18 DzljUl9dgiE6eex/8OeN/DXnw0roUS9t00zDBXylzFphQ4/yMCxxOJuOPxifYEVS
0x19 DzljUl9dgiE6eex/8OeN/DXnw0roUS9t00zDBXylzFphQ4/yMCxxOJuOPxifYEVS
0x1a DzljUl9dgiE6eex/8OeN/DXnw0roUS9t00zDBXylzFphQ4/yMCxxOJuOPxifYEVS

0x18 是一个耐人寻味的值,正好是 192 位。这让我立刻联想到 AES 192 加密。事实上,如果你在谷歌上搜索 "192 位加密",第一个结果通常指向 AES。

但我们如何确定这确实是 AES 加密呢?虽然我们可以手动验证,但还需要确定加密模式及其 IV(初始化向量)。

我首先尝试了 CBC 模式,这主要是因为它很常用。此外,由于 CBC 模式的性质(详见 维基百科),使用初始 16 字节作为 IV 来验证它也很简单。

图片描述

图片描述

嗯,它确实是CBC模式(真酷)。现在,任务是确定IV。

寻找参数

由于加密函数没有明确要求使用 IV,因此有两种可能性。IV 可以根据 trackId 生成(因为数据最终会被加密),也可以随机生成,然后附加到返回值中。

后者很快就会被排除,因为返回值的长度似乎容不下附加的 IV。这就指向了前者--IV 可能来自于 trackId。但如何派生呢?

为了解决这个问题,我从最简单的假设入手:将 trackId 的前 16 个字节用作 IV。

图片描述

笑嘻了,还真是

但随后又出现了另一个挑战。xm_encryptor "也可以处理长度小于24字节的 "trackId"。由于 AES-192 无法处理长度小于 24 字节的密钥,我断言该算法必须以某种方式在 trackId 中添加一些额外的字符。

我们现在的任务是确定填充字符及其填充方法。由于加密需要支持可变的 "trackId "长度,而且填充是根据我们提供的 "trackId "进行的,所以最直接的解决方案就是填充一些常量字符。

最简单的填充方法也有两种,一种是在 trackId 后面填充,另一种是在前面填充。

现在是时候进行一些简单但有效的方法 - 暴力破解。一个字节接一个字节。我们只需要运行256*24=6144次迭代。甚至不到10k。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
xm_encryptor = Instance(Module(
    Store(),
    pathlib.Path("./xm_encryptor.wasm").read_bytes()
))
 
def encrypt(data, key):
    stack_pointer = xm_encryptor.exports.a(-16)
    assert isinstance(stack_pointer, int)
    de_data_offset = xm_encryptor.exports.c(len(data))
    assert isinstance(de_data_offset,int)
    track_id_offset = xm_encryptor.exports.c(len(key))
    assert isinstance(track_id_offset, int)
    memory_i = xm_encryptor.exports.i
    memview_unit8:Uint8Array = memory_i.uint8_view(offset=de_data_offset)
    for i,b in enumerate(data):
        memview_unit8[i] = b
    memview_unit8: Uint8Array = memory_i.uint8_view(offset=track_id_offset)
    for i,b in enumerate(key):
        memview_unit8[i] = b
    xm_encryptor.exports.h(stack_pointer,de_data_offset,len(data),track_id_offset,len(key))
    memview_int32: Int32Array = memory_i.int32_view(offset=stack_pointer // 4)
    result_pointer = memview_int32[0]
    result_length = memview_int32[1]
    assert memview_int32[2] == 0, memview_int32[3] == 0
    result_data = bytearray(memory_i.buffer)[result_pointer:result_pointer+result_length].decode()
    return result_data
 
fillup = []
 
for missing in range(1,0x18+1):
    data = b'A'*0x10+b"CCCCCCCC"+b"DDDDDDDD"
    key = b'\x41'*(0x18 - missing)
    for i in range(256):
        test_byte = i.to_bytes(1,"little")
        # filledup_key = key + b''.join(fillup) + test_byte
        filledup_key = b''.join(fillup) + test_byte + key
        try:
            result_data = encrypt(data,key)
            cipher = AES.new(filledup_key, AES.MODE_CBC, filledup_key[:16])
            decoded_data = unpad(cipher.decrypt(base64.b64decode(result_data)),16)
            assert data == decoded_data
            fillup.append(test_byte)
            print("found", fillup)
            break
        except Exception as e:
            pass
    assert len(fillup) == missing
 
 
print("found filled up: ", b''.join(fillup))

从结果中我们可以清晰地看出填充 (前面填充):

1
2
3
4
5
[aynakeya @ ThinkStation]:~/workspace/ximalaya
23:16:55 $ python test_wasm_3.py
found [b'1']
...
found filled up:  b'123456781234567812345678'

Verify Parameter with Memdump

为了验证填充方法的准确性,我还可以采用内存转储技术。虽然调试也可以,但我懒得调试 WebAssembly。

为此,我在 encrypt 函数中添加了几行:

1
2
3
4
result_data = bytearray(memory_i.buffer)[result_pointer:result_pointer+result_length].decode()
a = bytearray(memory_i.buffer)[0:track_id_offset*3]
off = a.find(key)
print(a[off-0x20:off+0x20])

By examining the output, we can clearly identify the padding:

1
bytearray(b'}\x11\x00@\x00\x00\x00@\x00\x00\x00\xe8\x01\x11\x00X}\x11\x00@\x00\x00\x00@\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00X}\x11\x00@\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00AAAA@\x00\x00\x00\x10\x00\x00\x000\xa4\x0e\x000\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\xa8\x00\x11\x00p\x08\x10\x00123456781AAAAAAAAAAAAAAA123456781AAAAAAA123456781AAAAAAAAAAAAAAA123456781AAAAAAA\xe8\x01\x11\x000\x00\x00\x000\x00\x00\x00AAAA\x08\x00\x11\x00 \x00\x00\x00 ')

The presence of sequences like 123456781AAAAAAAAAAAAAAA in the dumped data suggests that our assumption regarding the padding is indeed accurate.

通过所有的拼图部分,很明显 wasm 加密遵循以下步骤:

  1. 使用 Base64 解码输入。
  2. 使用 AES-192-CBC 加密。密钥来自 trackId。如果 trackId 少于24字节,它将以123456781234567812345678前置,以达到所需长度。而 IV 是密钥的前16字节。
  3. 使用 Base64 对结果进行编码。

[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。

收藏
点赞2
打赏
分享
最新回复 (4)
雪    币: 1429
活跃值: (386703)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
一笑人间万事 2023-8-29 17:21
2
0
感谢分享
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_sngdtqxn 2023-9-26 09:20
3
0
感谢分享
雪    币: 19431
活跃值: (29092)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2023-9-26 09:25
4
1
感谢分享
雪    币: 253
活跃值: (492)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
chengqiyan 2023-9-26 18:00
5
0
别逆向喜马了 我在喜马直播间做大哥
游客
登录 | 注册 方可回帖
返回