如何用纯猜的方式逆向喜马拉雅xm文件加密(wasm部分)
在之前的文章,我留下了一个关于 wasm 内部解密方法的疑问。今天,让我们再次深入逆向工程的世界,揭开代码下的神秘面纱。
现在,你们中的一些人可能会想知道为什么这篇文章的标题是纯粹的猜测。好吧,那是因为我在处理这个*'逆向'*挑战时,实际上并没有进行任何真正的逆向工程。
向前思考
如果存在一个解密算法,那么必然存在一个相应的加密机制。在这种情况下,webassembly 中导出的函数h
就是我想要的加密方法。
与其解密对应部分相似,它需要两个参数:加密的数据和 trackId。
function f_h(a:{ a:byte, b:byte }, b:int, c:long_ptr, d:int, e:int) {
var m:int;
...
让我们探索这种加密是如何工作的。
猜测加密
首先,我们需要一个脚本来测试不同的参数如何影响加密结果:
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
的长度不影响结果。
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 模式的性质(详见维基百科https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_block_chaining_(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。
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))
从结果中我们可以清晰地看出填充 (前面填充):
[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
函数中添加了几行:
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:
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 like123456781AAAAAAAAAAAAAAA
in the dumped data suggests that our assumption regarding the padding is indeed accurate.
通过所有的拼图部分,很明显 wasm 加密遵循以下步骤:
trackId
。如果trackId
少于24字节,它将以123456781234567812345678
前置,以达到所需长度。而 IV 是密钥的前16字节。看雪ID:Aynakeya
https://bbs.kanxue.com/user-home-967169.htm
# 往期推荐
3、安卓加固脱壳分享
球分享
球点赞
球在看