如何用纯猜的方式逆向喜马拉雅xm文件加密(wasm部分)
2023-10-3 17:21:54 Author: mp.weixin.qq.com(查看原文) 阅读量:1 收藏

在之前的文章,我留下了一个关于 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 like123456781AAAAAAAAAAAAAAAin 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 对结果进行编码。

看雪ID:Aynakeya

https://bbs.kanxue.com/user-home-967169.htm

*本文由 Aynakeya 原创,转载请注明来自看雪社区

# 往期推荐

1、在 Windows下搭建LLVM 使用环境

2、深入学习smali语法

3、安卓加固脱壳分享

4、Flutter 逆向初探

5、一个简单实践理解栈空间转移

6、记一次某盾手游加固的脱壳与修复

球分享

球点赞

球在看


文章来源: https://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458520903&idx=2&sn=e6e0bce3e2e7eda7d02c5612d42c0e32&chksm=b18d3dcd86fab4dbf5e0349d4569df9289c0fc2208dd9cd1857a27b911d56b4540bdbb33f6b5&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh