模拟执行
unidbg调用的视频已出:unidbg调用马蜂窝zzzghostsigh算法
这篇文章主要讲解算法还原
app version: 9.3.7
加密值:muyang
返回值:efa2ecf4644732bd5f2f2fa43fe702708674ee1d
本篇知识点:unidbg的hook,算法还原。ida分析。
算法还原
1.算法猜想
猜测这是sha1算法
1.经过测试发现不管输入多少位,结果都是40位。
2.并且ida查看到IV有5个.
2.具体分析
打开IDA发现,sub_30548。是一个签名校验函数。因为点进去会看到签名校验的三兄弟。(下图),并且返回值错误的时候,
会说。Illegal signature(非法的签名)
那么IDA只剩下下面2个函数没有分析了。sub_312E0.
将入参修改一下,然后v9是和inputtext,输入的值有关。v13是一个buffer。v10是v9的长度。那么由此可得这个sub_312E0,
应该就是加密的函数,因为sub_2E1F4,没有和输入值有关的。并且v13是一个buffer,这是C语言常用的一种格式。会将加密值放到
一个buffer中。
使用unidbg验证一下我们的猜想;使用HookZz 在函数进入前Hook参数1和参数3,函数出去后Hook 参数2。
public void hook_312E0(){
// 获取HookZz对象
IHookZz hookZz = HookZz.getInstance(emulator); // 加载HookZz
// hook MDStringOld
hookZz.wrap(module.base + 0x312E0 + 1, new WrapCallback<HookZzArm32RegisterContext>() { // inline wrap导出函数
@Override
// 方法执行前
public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
Pointer input = ctx.getPointerArg(0);
byte[] inputhex = input.getByteArray(0, ctx.getR2Int());
Inspector.inspect(inputhex, "input");
Pointer out = ctx.getPointerArg(1);
ctx.push(out);
};
@Override
// 方法执行后
public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
Pointer output = ctx.pop();
byte[] outputhex = output.getByteArray(0, 20);
Inspector.inspect(outputhex, "output");
}
});
};
那么我们发现和我们的猜想是一致的。所以只需要关注这个函数。
进入这个函数发现也有这么几个变量,按H可以切换到16进制。这就是sha-1的魔数。下图是标准的魔数,经过对比
可以发现,IV的第四个和第五个被改变了。
修改下源代码
# sha1-v1
import struct
bitlen = lambda s: len(s) * 8
def ROL4(x, n):
x &= 0xffffffff
return ((x << n) | (x >> (32 - n))) & 0xffffffff
def madd(*args):
return sum(args) & 0xffffffff
class sha1:
block_size = 64
digest_size = 20
def __init__(self, data=b''):
if data is None:
self._buffer = b''
elif isinstance(data, bytes):
self._buffer = data
elif isinstance(data, str):
self._buffer = data.encode('ascii')
else:
raise TypeError('object supporting the buffer API required')
self._sign = None
def update(self, content):
if isinstance(content, bytes):
self._buffer += content
elif isinstance(content, str):
self._buffer += content.encode('ascii')
else:
raise TypeError('object supporting the buffer API required')
self._sign = None
def copy(self):
other = self.__class__.__new__(self.__class__)
other._buffer = self._buffer
return other
def hexdigest(self):
result = self.digest()
return result.hex()
def digest(self):
if not self._sign:
self._sign = self._current()
return self._sign
def _current(self):
msg = self._buffer
# standard magic number
# A = 0x67452301
# B = 0xEFCDAB89
# C = 0x98BADCFE
# D = 0x10325476
# E = 0xC3D2E1F0
A = 0x67452301
B = 0xEFCDAB89
C = 0x98BADCFE
D = 0x5E4A1F7C
E = 0x10325476
msg_len = bitlen(msg) & 0xffffffffffffffff
zero_pad = (56 - (len(msg) + 1) % 64) % 64
msg = msg + b'\x80'
msg = msg + b'\x00' * zero_pad + struct.pack('>Q', msg_len)
for idx in range(0, len(msg), 64):
W = list(struct.unpack('>16I', msg[idx:idx + 64])) + [0] * 64
for t in range(16, 80):
T = W[t - 3] ^ W[t - 8] ^ W[t - 14] ^ W[t - 16]
W[t] = ROL4(T, 1)
a, b, c, d, e = A, B, C, D, E
# main loop:
for t in range(0, 80):
if 0 <= t <= 19:
f = (b & c) | ((~b) & d)
k = 0x5A827999
elif 20 <= t <= 39:
f = b ^ c ^ d
k = 0x6ED9EBA1
elif 40 <= t <= 59:
f = (b & c) | (b & d) | (c & d)
k = 0x8F1BBCDC
elif 60 <= t <= 79:
f = b ^ c ^ d
k = 0xCA62C1D6
S0 = madd(ROL4(a, 5), f, e, k, W[t])
S1 = ROL4(b, 30)
a, b, c, d, e = S0, a, S1, c, d
A = madd(A, a)
B = madd(B, b)
C = madd(C, c)
D = madd(D, d)
E = madd(E, e)
result = struct.pack('>5I', A, B, C, D, E)
return result
if __name__ == '__main__':
s = b'muyang'
s0 = sha1(s).hexdigest()
print(s0)
发现和马蜂窝的答案(efa2ecf4644732bd5f2f2fa43fe702708674ee1d)不一样。最开始就有标准答案。
那么我们该如何找到魔改的地方了?
一个哈希算法,可以简单划分成填充和加密两部分,直接Hook加密函数,看它的入参,依此判定填充部分是否发生
过改变。
public void hook_3151C(){
// 获取HookZz对象
IHookZz hookZz = HookZz.getInstance(emulator); // 加载HookZz
// hook MDStringOld
hookZz.wrap(module.base + 0x3151C + 1, new WrapCallback<HookZzArm32RegisterContext>() { // inline wrap导出函数
@Override
// 方法执行前
public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
// 类似于Frida args[0]
Pointer input = ctx.getPointerArg(0);
byte[] inputhex = input.getByteArray(0, 20);
Inspector.inspect(inputhex, "IV");
Pointer text = ctx.getPointerArg(1);
byte[] texthex = text.getByteArray(0, 64);
Inspector.inspect(texthex, "block");
ctx.push(input);
ctx.push(text);
};
@Override
// 方法执行后
public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
Pointer text = ctx.pop();
Pointer IV = ctx.pop();
byte[] IVhex = IV.getByteArray(0, 20);
Inspector.inspect(IVhex, "IV");
byte[] outputhex = text.getByteArray(0, 64);
Inspector.inspect(outputhex, "block out");
}
});
};
Hook结果反映了这样一个问题
魔改的是算法本身,因为运算函数的入参是正常的、填充后的明文,所以不存在自定义填充、或者对明文做变换的可能,
出参即是输出的结果,所以算法并不是在标准流程之后做了一些自定义步骤,它修改的——就是算法本身。
那么我们进入 sub_3151C 这个函数观察一下。发现了几个常数。
1518500249
1859775393
-1894007588
-899497514
转成16进制表示:
hex(1518500249) = 0x5a827999
hex(1859775393) = 0x6ed9eba1
hex(-1894007588 & 0xFFFFFFFF) = 0x8f1bbcdc
hex(-899497514 & 0xFFFFFFFF) = 0xca62c1d6
这4个数正是标准实现里的数字,每个数字20轮,一共80轮。然后我们统计一下函数里面这几个数的出现次数
0x5a827999 x 18
0x6ed9eba1 x 4
0x8f1bbcdc x 23
0x5a827999 x 20
0xca62c1d6 x 20
一定要注意这个顺序,因为加密的也是时候需要这个顺序
总共85次,比标准的多出现了5次,然后检查下,发现如下重复
去除重复出现的,上面的出现次数修正为
0x5a827999 x 16
0x6ed9eba1 x 4
0x8f1bbcdc x 20
0x5a827999 x 20
0xca62c1d6 x 20
正好80次,因此,我们将轮换的代码修改一下:
for t in range(80):
if t <= 15:
K = 0x5a827999
f = (b & c) ^ (~b & d)
elif t <= 19:
K = 0x6ed9eba1
f = b ^ c ^ d
elif t <= 39:
K = 0x8f1bbcdc
f = (b & c) ^ (b & d) ^ (c & d)
elif t <= 59:
K = 0x5a827999
f = (b & c) ^ (~b & d)
else:
K = 0xca62c1d6
f = b ^ c ^ d
efa2ecf4644732bd7a57bff224be72228674ee1d
这是这段代码的返回值
efa2ecf4644732bd5f2f2fa43fe702708674ee1d(标准答案)
虽然不一样,但是感觉已经很接近了对吧。
我们发现[0:8],[8:16],[32:40]是能够对的上的,但是[16:24],[24:32]就完全不同。这么相近的结果,说明我们在最后的实现和实际有些不同。
再次看看sub_3151C最后的代码
发现最后是4-2-3-1-0的顺序加密。因此,我们在最后一次轮换中也按这个顺序
if t == 79:
a, b, d, c, e = S0, a, S1, c, d
else:
a, b, c, d, e = S0, a, S1, c, d
最后完成。希望你可以有所收获。
课程样本 :链接:https://pan.baidu.com/s/1oN-AHME4Oe3v50EGEitZPQ?pwd=adpr
提取码:adpr
一起学习:Ays971124