从trace分析vmp中的SHA256算法
1. 前言
- 在分析稍微难一点的样本的时候,伪代码往往是不具有参考性质的;所以在分析混淆强度比较大的标准算法时,就比较考验分析者对于算法的熟悉程度;
- 这篇文章的trace文件只有200w行左右,是一个难度不大的样本,中间出现so的名字你就知道是哪个app了,trace文件我会放在文件末尾,想复现的读者直接去链接取就好了;
- 先看分析目标吧:
00000000 f5 7d 3b 08 e8 1f 4f 1f 11 d0 24 7b 17 63 76 24 |õ};.è.O..Ð${.cv$|
00000010 c3 c9 bc 78 3a 74 41 b5 02 73 56 3e 84 d7 00 d8 |Ãɼx:tAµ.sV>.×.Ø|
- 我们的目标就是在trace文件中找到这串数据的明文以及算法;
2. 对sha256的分析
- 实际上直接从这里写略微有些干,最好是结合unidbg来分析,但是这里也不算太违和;我们怎么分析比较好?我并不想直接说哦它是用了sha256,而是想通过一些证据来证明它是sha256;
- 搜索结果的前四字节,一般来说,hash算法在trace中的表现都会是4字节,但这不代表所有;

- 第一条是加法运算,而且是出现的第一次,我们一般认为在trace里出现最早的时候就是生成位置,当然前提得是运算出来的而不是ldr之类的指令;
- 这里的指令是:
"add w9, w10, w9" w10=0x6a09e667 w9=0x8b7354a1 => w9=0xf57d3b08
- 第一个值:0x6a09e667,我们可以去google一下;

- 结果告诉我们这是一个sha256,同样可以在trace里搜索结果的第二个4字节,一样可以在w10的位置搜到sha256的另一个常数;

- 这两个常数是sha256的魔数,所以这个结果来源很有可能是一个sha256的函数,而结果的位数是两排hexdump,也就是32字节,也是符合sha256标准的,所以我们后续的分析就需要往这方面靠;
- 下一步确定其是否是一个标准的算法,但我们在初始分析的时候并不能完全确定是否魔改,只能说先进行一个基本的排查;
- 我们可以拿一份sha256的源码,搜一下其中的常数是否在trace里都存在,如果都存在,我们可以暂时认为是标准的sha256,之所以只是暂时认为,是因为魔改不仅仅局限于常数,但它是最基础的魔改,一般来说这个是最容易出现的情况;当然也不排除只改逻辑不动常数的情况,但这种情况相对来说比较少见;
- 经过搜索,所有常数包括k表均出现在trace中,暂时认为是标准sha256,这里我不贴图了;那我们接下来如何分析?
- 既然是标准的,我们就可以使用下面介绍的算法分析论,先看一份sha256的算法;
import struct
# 常量
K = [
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
]
# 初始哈希值
H = [
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
]
def right_rotate(value, bits):
return ((value >> bits) | (value << (32 - bits))) & 0xffffffff
def sha256(data):
# 步骤 1: 填充消息
original_byte_len = len(data)
original_bit_len = original_byte_len * 8
data += b'\x80'
data += b'\x00' * ((56 - (original_byte_len + 1) % 64) % 64)
data += struct.pack('>Q', original_bit_len)
# 步骤 2: 解析消息为512-bit块
blocks = []
for i in range(0, len(data), 64):
blocks.append(data[i:i + 64])
# 步骤 3: 初始化工作变量
hash_pieces = H[:]
# 步骤 4: 处理每一个块
for block in blocks:
W = list(struct.unpack('>16L', block)) + [0] * 48
for i in range(16, 64):
s0 = right_rotate(W[i-15], 7) ^ right_rotate(W[i-15], 18) ^ (W[i-15] >> 3)
s1 = right_rotate(W[i-2], 17) ^ right_rotate(W[i-2], 19) ^ (W[i-2] >> 10)
W[i] = (W[i-16] + s0 + W[i-7] + s1) & 0xffffffff
a, b, c, d, e, f, g, h = hash_pieces
for i in range(64):
S1 = right_rotate(e, 6) ^ right_rotate(e, 11) ^ right_rotate(e, 25) # e
ch = (e & f) ^ (~e & g) # e f g
temp1 = (h + S1 + ch + K[i] + W[i]) & 0xffffffff # h
S0 = right_rotate(a, 2) ^ right_rotate(a, 13) ^ right_rotate(a, 22) # a
maj = (a & b) ^ (a & c) ^ (b & c) # a b c
temp2 = (S0 + maj) & 0xffffffff
h = g
g = f
f = e
e = (d + temp1) & 0xffffffff
d = c
c = b
b = a
a = (temp1 + temp2) & 0xffffffff
hash_pieces = [(x + y) & 0xffffffff for x, y in zip(hash_pieces, [a, b, c, d, e, f, g, h])]
# 步骤 5: 拼接哈希值
return ''.join(f'{piece:08x}' for piece in hash_pieces)
hash_value = sha256('123456789'.encode())
print(f'SHA-256: {hash_value}')
- 这是一份python版的源码,我们以它为基准来分析这个算法;
- 我在星球里发过两篇类似的文章:

- 是我从龙哥那里学到的知识,在此向龙哥致谢;而这一篇文章与星球里的sha256分析有所差别,但我认为却是更简单的方式;
- 我们的思想是找锚点,这个锚点是什么意思?
- 指在这个位置经过一些推测,可以推测出目标数据;如key、明文等等;这里显然我们要推理的是明文,一般来说,锚点找那种有目标参与的位置,比如这里是找明文,就去找和明文有计算的位置,并且和明文计算的其他数据最好是可以推出来或者是一些常量,这样我们会比较容易分析算法;
- 对于这个样本,我们可以选择这个位置;
for i in range(64):
S1 = right_rotate(e, 6) ^ right_rotate(e, 11) ^ right_rotate(e, 25) # e
ch = (e & f) ^ (~e & g) # e f g
temp1 = (h + S1 + ch + K[i] + W[i]) & 0xffffffff # 以这里为锚点
- 它在加密的途中,这里有w,也就是明文,我们需要前16组,因为后续是扩展出来的;
- 这里是五个加法(最后一行),而且h、S1、ch、K[i]在第一轮,均是已知的或者说能够间接计算出来;但是我们不可以直接找K[i] + W[i],因为加法是不具备顺序性质的,谁先和谁加我们无法确定,所以需要找一个出发点;
- 以h、s1、ch、k[i]谁作为出发点均是可以的,我这里以ch为出发点,这里的ch值是:0x1f85c98c;ch需要你自己去计算,来自e、f、g三者,它们均已知,我的建议就是本地pycharm调试,不用自己手动计算,还可以实时的观察每个变量的值;
- 我们在trace中搜索这个值,重点找加法,结果很多的话,你也可以正则筛选一下;

- 这里共有16次匹配,加法则是两次匹配,也许意味着这里有着两次sha256的运算;后者在160w行了,显然不应该是我们分析的目标,这里看第一次匹配;
"add x9, x10, x9" x10=0x14e18ab9167f444 x9=0x1f85c98c => x9=0x14e18abb0edbdd0
- 问题来了,0x14e18ab9167f444是什么?结果又做了什么?
- 先看0x14e18ab9167f444,它很显然是上一次加法的结果,是谁和谁相加我们不知道,搜索它;

"add x9, x10, x9" x10=0x5be0cd19 x9=0x14e18ab3587272b => x9=0x14e18ab9167f444
- 也是一个加法,这正是我们想要的,这里出现了一个常数,0x5be0cd19;它是最后一个iv,在锚点出对应的是h;
for i in range(64):
S1 = right_rotate(e, 6) ^ right_rotate(e, 11) ^ right_rotate(e, 25) # e
ch = (e & f) ^ (~e & g) # e f g
temp1 = (h + S1 + ch + K[i] + W[i]) & 0xffffffff # 以这里为锚点
- 我们这么找的目的是找出已知的,剩下未知的就是w;再搜索0x14e18ab3587272b;

- 这里是一个异或运算,那这里肯定是S1了,毕竟我们是以ch出发的,不过这里只取了后四字节;

- 到这里还剩下K[i]和W[i],不过需要往下找,因为前面已经没有加法了,异或肯定是在temp1的运算前面的;
- 搜一下ch加法后的结果是不是在和k做加法,搜索0x14e18abb0edbdd0,这个是第一次搜索的结果;
0x40024704: "add x9, x10, x9" x10=0x14e18abb0edbdd0 x9=0x428a2f98 => x9=0x14e18abf377ed68

- 确实是这样,到这里我们就只剩下w不知道了,但是我们目前没有看到有什么未知的数据出现;但是根据前面的分析我们可以得出这样的结论:0x14e18abf377ed68这部分的结果就是h + S1 + ch + K[i]的结果,所以我们可以断定,它的add是这样运转的;
temp1 = (h + S1 + ch + K[i]) + W[i]
- 而K[i]则是第一个大的集体里最后做加法的,你不确定的话可以按照trace的行号来判断;所以我们根据K[i]为出发点,搜索与它有关的加法,然后再搜和这个结果加法相关的加法,对应的值应该就是w[i],去看一下吧;

- 注意别找错了,是和它相加不是加起来等于它,所以w就是0x5c0f60b8;
- 我们可以把这部分改到本地sha256的明文里,因为它计算出的东西后续会改变e的值,所以我们本地需要保持一致;
- 然后按照前面的理论,和K[i]相加的是前面三部分,那这个add的结果再和w相加;所以后续我们只需要搜索k的加法,然后再找这个结果相关的加法,与之对应的就是明文w;我们按照这个理论搜索五次后,得到这样的结果;

- 这里的明文是0x80000000,这个很明显是填充,意味着明文结束了,所以就是前四次的明文组合起来,如下:
5c0f60b84e028b6330387d0f567eb4c7

- 这正是我们的目标数据,所以这个算法是sha256,加密的明文是 5c0f60b84e028b6330387d0f567eb4c7;
- 这个明文还经过了其他运算,但不在本篇文章的范畴内,而这个sha256是这个样本最难的点(也许是),学会了这个分析方法是可以延伸到其他样本的,标准算法都可以这么分析;就算是不熟悉的算法拿一份源码阅读几遍找找锚点也是可以分析的;
3. 总结