unidbg 算法还原术· 某民宿app 篇· 中卷
本文由 简悦
SimpRead 转码, 原文地址 mp.weixin.qq.com
上回说到
伪代码又臭又长,看不懂没关系,从下往上看;找关键的函数;
而且之前猜测是
伪代码虽然很长,但是函数还是没几个的,都点进去看看
有个
哈希算法最明显的特征就是初始化链接常量
初始化链接常量
A=0x01234567,B=0x89abcdef,C=0xfedcba98,D=0x76543210
但考虑到内存数据存储大小端的问题我们将其赋值为:
A=0x67452301,B=0xefcdab89,C=0x98badcfe,D=0x10325476
A=0x67452301,B=0xefcdab89,C=0x98badcfe,D=0x10325476,E=0xCA62C1D6
constTable=[
0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf,
0x4787c62a, 0xa8304613, 0xfd469501, 0x698098d8, 0x8b44f7af,
0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e,
0x49b40821, 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa,
0xd62f105d, 0x2441453, 0xd8a1e681, 0xe7d3fbc8, 0x21e1cde6,
0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8,
0x676f02d9, 0x8d2a4c8a, 0xfffa3942, 0x8771f681, 0x6d9d6122,
0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x4881d05, 0xd9d4d039,
0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, 0xf4292244, 0x432aff97,
0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d,
0x85845dd1, 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391]
第1轮 0≤t≤19步 Kt=0x5A827999
第2轮 20≤t≤39步 Kt=0x6ED9EBA1
第3轮 40≤t≤59步 Kt=0x8F188CDC
第4轮 60≤t≤7步 Kt=0xCA62C1D6
回到
这个
而在工程标准化中,哈希加密一般分为
简单来说:
-
Init 是一个初始化函数,初始化核心变量 -
Update 是主计算过程 -
Final 整理和填写输出结果
看汇编就知道了
为啥还是少了一个幻数
因为
鼠标移到
再按
再按
然后找下面的
int __fastcall tjreset(signed __int64 a1, int a2)
{
int v2; // r4
_BYTE *v3; // r5
int v4; // r6
v2 = a2;
v3 = (_BYTE *)HIDWORD(a1);
v4 = a1;
while ( v2 )
{
*(_BYTE *)(v4 + *(_DWORD *)(v4 + 64)) = *v3;
*(_BYTE *)(v4 + *(_DWORD *)(v4 + 64)) ^= 0x21u;
LODWORD(a1) = *(_DWORD *)(v4 + 64) + 1;
*(_DWORD *)(v4 + 64) = a1;
if ( (_DWORD)a1 == 64 )
{
j_tjupdate(v4, v4);
*(_DWORD *)(v4 + 64) = 0;
a1 = *(_QWORD *)(v4 + 72) + 512LL;
*(_QWORD *)(v4 + 72) = a1;
}
++v3;
--v2;
}
return a1;
}
一般来说只要
我们先用
function myhexdump(name, hexdump_obj, len_obj){
console.log("-------------------"+ name.toString()+"-------------------\\n");
console.log(hexdump(hexdump_obj,{
length:len_obj
}) );
console.log("-------------------ENDEND-------------------\\n")
}
function tjreset() {
var pointer = Module.findBaseAddress("libtujia_encrypt.so").add(0x2C94 + 1);
Interceptor.attach(pointer,
{
onEnter: function (args) {
myhexdump("tjreset arg0:", args[0], 128)
this.buffer = args[0];
myhexdump('tjreset arg1:', args[1], parseInt(args[2]))
console.log('tjreset arg2:' + parseInt(args[2]));
},
onLeave: function (retval) {
myhexdump("tjreset ret:", this.buffer, 16)
}
}
);
}
注意:这里
R0、R1、
开始
unidbg hook:
public void hook2c94(){
IHookZz hookZz = HookZz.getInstance(emulator); // 加载HookZz
hookZz.wrap(module.base + 0x2C94 + 1, new WrapCallback<HookZzArm32RegisterContext>() {
@Override
// 方法执行前
public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
Pointer arg1 = ctx.getPointerArg(1);
int length = ctx.getIntArg(2);
Pointer out = ctx.getPointerArg(0);
ctx.push(out);
ctx.push(length);
Inspector.inspect(arg1.getByteArray(0, length),"tjreset arg1");
};
@Override
// 方法执行后
public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
int length =ctx.pop();
Pointer output = ctx.pop();
byte[] outputhex = output.getByteArray(0, length);
Inspector.inspect(outputhex, "tjreset ret");
}
});
hookZz.disable_arm_arm64_b_branch();
}
public static void main(String[] args) throws FileNotFoundException {
Test test = new Test();
test.hook2c94();//要在函数调用之前hook,不然hook个p
test.call_encrypt();
// test.get_bodyencrypt();
}
一般情况来说
打开逆向之友,去碰碰运气
https://gchq.github.io/CyberChef/
结果对不上,难道
莫急,冷静分析,再看下伪代码,和标准的有没有啥区别
发现
*(_BYTE *)(v4 + *(_DWORD *)(v4 + 64)) ^= 0x21u;
这是把
那我们再用
看看明文是啥呢
看着是
哦?明文出来了,茅塞顿开
所以这个明文组成就是:
- 字符串反转
不过
有人说了,我就是看不出这有个异或
行,再露一手
来看看
int __fastcall tjupdate(int a1, int a2)
{
int i; // r3
int v3; // r1
char *v4; // r5
int v5; // r2
int v6; // r3
int v7; // r5
int v8; // r10
int v9; // r1
int v10; // r6
int v11; // r11
int v12; // r3
int v13; // r0
int v14; // r2
int v15; // r4
int v16; // r3
int v17; // r5
int v18; // r0
int v19; // r6
int v20; // r1
int v21; // r8
int v22; // r0
int v23; // r2
int v24; // r6
int v25; // r2
int v26; // r12
int v27; // r3
int v28; // r0
int v29; // r4
int v30; // r5
int v31; // r3
int v32; // r0
int v33; // r1
__int64 v34; // r3
int v36; // [sp+4h] [bp-17Ch]
int v37; // [sp+8h] [bp-178h]
int v38; // [sp+Ch] [bp-174h]
int v39; // [sp+10h] [bp-170h]
int v40; // [sp+14h] [bp-16Ch]
int v41; // [sp+1Ch] [bp-164h]
char v42[320]; // [sp+20h] [bp-160h]
char _70[320]; // [sp+70h] [bp-110h]
char _C0[320]; // [sp+C0h] [bp-C0h]
char _110[320]; // [sp+110h] [bp-70h]
int v46; // [sp+160h] [bp-20h]
for ( i = 0; i != 64; i += 4 )
*(_DWORD *)&v42[i] = bswap32(*(_DWORD *)(a2 + i));
v3 = 0;
while ( v3 != 256 )
{
v4 = &v42[v3];
v5 = *(_DWORD *)&v42[v3 + 8] ^ *(_DWORD *)&v42[v3 + 32] ^ *(_DWORD *)&v42[v3 + 52];
v6 = *(_DWORD *)&v42[v3];
v3 += 4;
*((_DWORD *)v4 + 16) = __ROR4__(v5 ^ v6, 31);
}
v8 = *(_QWORD *)(a1 + 80) >> 32;
v7 = *(_QWORD *)(a1 + 80);
v9 = *(_DWORD *)(a1 + 100);
v10 = 0;
v12 = *(_DWORD *)(a1 + 92);
v11 = *(_DWORD *)(a1 + 96);
v41 = a1;
v38 = *(_DWORD *)(a1 + 88);
v13 = *(_DWORD *)(a1 + 88);
v14 = v12;
v37 = v12;
v40 = v7;
v39 = v8;
v36 = v11;
while ( 1 )
{
v15 = v14;
v14 = v13;
v16 = v7;
if ( v10 == 20 )
break;
v17 = *(_DWORD *)&v42[4 * v10];
v18 = (v13 & v8 | v15 & ~v8) + v11 + __ROR4__(v16, 27) + v9;
++v10;
v11 = v15;
v7 = v17 + v18;
v13 = __ROR4__(v8, 2);
v8 = v16;
}
v19 = 0;
while ( 1 )
{
v20 = v15;
v15 = v14;
v21 = v16;
if ( v19 == 20 )
break;
v22 = *(_DWORD *)&_70[4 * v19++];
v23 = (v14 ^ v8 ^ v20) + __ROR4__(v16, 27) + v11;
v11 = v20;
v16 = v23 + *(_DWORD *)(v41 + 104) + v22;
v14 = __ROR4__(v8, 2);
v8 = v21;
}
v24 = 0;
while ( 1 )
{
v25 = v20;
v20 = v15;
v26 = v21;
if ( v24 == 20 )
break;
v15 = __ROR4__(v8, 2);
v27 = *(_DWORD *)&_C0[4 * v24];
v28 = (v25 & v20 ^ (v25 ^ v20) & v8) + v11 + __ROR4__(v21, 27) + *(_DWORD *)(v41 + 108);
++v24;
v8 = v21;
v21 = v28 + v27;
v11 = v25;
}
v29 = 0;
while ( 1 )
{
v30 = v25;
v25 = v20;
v31 = v26;
if ( v29 == 20 )
break;
v32 = *(_DWORD *)&_110[4 * v29++];
v33 = (v20 ^ v8 ^ v30) + __ROR4__(v26, 27) + v11;
v11 = v30;
v26 = v33 + *(_DWORD *)(v41 + 112) + v32;
v20 = __ROR4__(v8, 2);
v8 = v31;
}
LODWORD(v34) = v26 + v40;
HIDWORD(v34) = v39 + v8;
*(_QWORD *)(v41 + 80) = v34;
*(_DWORD *)(v41 + 88) = v20 + v38;
*(_DWORD *)(v41 + 92) = v37 + v30;
*(_DWORD *)(v41 + 96) = v36 + v11;
return _stack_chk_guard - v46;
}
这个函数就是
1 在信息的后面填充一个1和无数个0,直到满足上面的条件时才停止用0对信息的填充。
2 在这个结果后面附加一个以64位二进制表示的填充前信息长度(单位为Bit),如果二
进制表示的填充前信息长度超过64位,则取低64位。
经过这两步的处理,信息的位长=N*512+448+64=(N+1)*512,即长度恰好是512的整数倍。
这样做的原因是为满足后面处理中对信息长度的要求。
要讲明白还是有点难(为了不误人子弟)大家感兴趣的自己搜资料研究;
回到
每一个分组进行
我们
function tjupdate() {
var pointer = Module.findBaseAddress("libtujia_encrypt.so").add(0x2AB4 + 1);
Interceptor.attach(pointer,
{
onEnter: function (args) {
myhexdump("update arg0:", args[1], 64)
console.log("update arg0:"+Memory.readUtf8String(args[1]))
},
onLeave: function (retval) {
}
}
);
unidbg hook:
public void hook_2AB4(){
IHookZz hookZz = HookZz.getInstance(emulator);
hookZz.enable_arm_arm64_b_branch();
hookZz.wrap(module.base + 0x2AB4 + 1, new WrapCallback<HookZzArm32RegisterContext>() {
@Override
// 方法执行前
public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
Pointer text = ctx.getPointerArg(1);
byte[] texthex = text.getByteArray(0, 64);
Inspector.inspect(texthex, "block");
};
@Override
// 方法执行后
public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
}
});
hookZz.disable_arm_arm64_b_branch();
};
然后把每次的明文拼接起来,就是整个的明文
然后再
话说回来,
有些情况不参与计算,有些情况参与计算,也就是
function tjget() {
var pointer = Module.findExportByName("libtujia_encrypt.so",'tjget');
console.log('case tjget:' + pointer);
Interceptor.attach(pointer,
{
onEnter: function (args) {
this.buffer = args[1];
},
onLeave: function (retval) {
myhexdump("tjget 结果:", this.buffer, 20)
}
}
);
}
unidbg hook:
public void hook_2CEA(){
IHookZz hookZz = HookZz.getInstance(emulator); // 加载HookZz
hookZz.wrap(module.base + 0x2CEA + 1, new WrapCallback<HookZzArm32RegisterContext>() {
@Override
// 方法执行前
public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
Pointer arg1 = ctx.getPointerArg(1);
ctx.push(arg1);
};
@Override
// 方法执行后
public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
Pointer output = ctx.pop();
byte[] outputhex = output.getByteArray(0, 20);
Inspector.inspect(outputhex, "tjget ret");
}
});
hookZz.disable_arm_arm64_b_branch();
}
明文的生成
分析完最后一步的标准算法,回过头来分析明文的生成
看看具体实现的代码:
首先这里生成了固定的
这里用
在这个地方下个断点
代码如下:
public void HookByConsoleDebugger() {
Debugger debugger = emulator.attach();
//在module.base+0x3108地址处添加一个断点
debugger.addBreakPoint(module.base+0x3108);
}
public static void main(String[] args) throws FileNotFoundException {
Test test = new Test();
//test.hook2c94();
// test.hook_2AB4();
test.HookByConsoleDebugger();
test.call_encrypt();
// test.get_bodyencrypt();
}
运行,程序会断下
这里的
怎么查看
输入:mr0
也可以输入地址:m0xbffff65c
默认
所以这里的参数
返回值怎么看呢,根据
这下面几个地址都可以
在
然后按
然后查看
验证后发现是标准的
还有一种方法能获取返回值
debugger.addBreakPoint(module.base+0x291C);
重新运行程序
这时
然后输入命令
c(跳到一下个断点)
查看
生成了
可以看到很多地方都在异或
这些都是把各个参数先异或
来打个断点瞧瞧
debugger.addBreakPoint(module.base+0x2DEC);
这个参数
这里就已经拼接好了嘛
看看返回值,blr,c
这里的返回值在
直接
所以我们
或者输入
然后把这个值异或
然后又是
上面那个调用第三个参数是
进去看看
编码之前参数
编码之后参数
所以总结下来就是,
-
每个参数和固定的
key 分别异或0x21 (有个参数要排序) -
拼接
-
翻转(j_tjsplittxt)
-
异或
0x21 (tjtxtutf8) -
base64 编码(tjtxtutf8) -
异或
0x21 (tjtxtutf8) -
异或
0x21 (j_tjreset) -
sha1(j_tjreset)
发现没有,
-
参数拼接(有个参数要排序)
-
翻转
-
sha1
剩下就是写代码了。。
下篇
彩蛋
初始化链接常数加载汇编指令解析
上面说了转成小端就是
0x67452301 0xEFCDAB89 0x98BADCFE 0x10325476 0xC3DEE1F0
看看汇编是怎么加载的
ADR R1, dword_2C80
即
看看内存中的值
然后就是下面的两行
VLD1.64 {D16-D17}, [R1]
VST1.64 {D16-D17}, [R0]
什么是
Arm NEON 技术是针对 Arm Cortex-A 系列和 Cortex-R52 处理器的高级 SIMD(单指令多数据)架构扩展。
具有NEON技术的处理器都会配备了32个64位的寄存器和16个128位的寄存器,它们分别被标识为(D0-D31),(Q0-Q15)
vld1.64 {d16, d17}, [r1]
将r1内存中的数据映射到d16, d17寄存器上面。这样就可以直接通过d16, d17寄存器来操作数据,64表示一个寄存器有64位(8个16进制)
在这里就是把
vst1.64 {d16, d17}, [r0]
将d16, d17寄存器中的数据映射回r0内存中,这样就可以通过打印r0来看到结果
在这里就是把
之后呢在
汇编
重点说下这个地方
LDMIA.W R11, {R2,R3,R11}
LDMIA指令,IA表示每次传送后地址加4,W修饰一个操作数为Double Word,即4字节
依次把
按
可以看到
其他的地方可以自己动态调试看看