在黑客攻击-软件破解(2) 中通过Radare2的静态分析实现了破解。本文使用frida和radare2进行动态分析来对crackerMe系列中后面的例子进行破解。
首先通过agf查看流程图。
[0x080484fb]> agf
[0x08048484]> # sym.check (char *s);
┌─────────────────────────────────────┐
│ 0x8048484 │
│ ; CALL XREF from main @ 0x8048559 │
│ 133: sym.check (char *s); │
│ ; var char *var_dh @ ebp-0xd │
│ ; var uint32_t var_ch @ ebp-0xc │
│ ; var uint32_t var_8h @ ebp-0x8 │
│ ; var int32_t var_4h @ ebp-0x4 │
│ ; arg char *s @ ebp+0x8 │
│ ; var char *format @ esp+0x4 │
│ ; var int32_t var_sp_8h @ esp+0x8 │
│ push ebp │
│ mov ebp, esp │
│ sub esp, 0x28 │
│ mov dword [var_8h], 0 │
│ mov dword [var_ch], 0 │
└─────────────────────────────────────┘
v
│
│
┌────────────────────────────────────────────────────────┐
│ │ │
│ ┌────────────────────────────────────────┐
│ │ 0x8048498 │
│ │ ; CODE XREF from sym.check @ 0x80484f9 │
│ │ mov eax, dword [s] │
│ │ ; const char *s │
│ │ mov dword [esp], eax │
│ │ ; size_t strlen(const char *s) │
│ │ call sym.imp.strlen;[oa] │
│ │ cmp dword [var_ch], eax │
│ │ jae 0x80484fb │
│ └────────────────────────────────────────┘
│ f t
│ │ │
│ │ └───────────────────┐
│ ┌───────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────────────────────────────────┐ ┌────────────────────────────────────────────┐
│ │ 0x80484a8 │ │ 0x80484fb │
│ │ mov eax, dword [var_ch] │ │ ; const char *format │
│ │ add eax, dword [s] │ │ ; CODE XREF from sym.check @ 0x80484a6 │
│ │ movzx eax, byte [eax] │ │ ; [0x8048649:4]=0x73736150 │
│ │ mov byte [var_dh], al │ │ ; "Password Incorrect!\n" │
│ │ lea eax, [var_4h] │ │ mov dword [esp], str.Password_Incorrect │
│ │ ; ... │ │ ; int printf(const char *format) │
│ │ mov dword [var_sp_8h], eax │ │ call sym.imp.printf;[oc] │
│ │ ; const char *format │ │ leave │
│ │ ; [0x8048638:4]=0x50006425 │ │ ret │
│ │ mov dword [format], 0x8048638 │ └────────────────────────────────────────────┘
│ │ lea eax, [var_dh] │
│ │ ; const char *s │
│ │ mov dword [esp], eax │
│ │ ; int sscanf(const char *s, const char *format, ...) │
│ │ call sym.imp.sscanf;[ob] │
│ │ mov edx, dword [var_4h] │
│ │ lea eax, [var_8h] │
│ │ add dword [eax], edx │
│ │ cmp dword [var_8h], 0xf │
│ │ jne 0x80484f4 │
│ └────────────────────────────────────────────────────────┘
│ f t
│ │ │
│ │ └───────────────────────┐
│ ┌───────────────┘ │
│ │ │
│┌─────────────────────────────────────┐ ┌────────────────────────────────────────┐
││ 0x80484dc │ │ 0x80484f4 │
││ ; const char *format │ │ ; CODE XREF from sym.check @ 0x80484da │
││ ; [0x804863b:4]=0x73736150 │ │ lea eax, [var_ch] │
││ ; "Password OK!\n" │ │ inc dword [eax] │
││ mov dword [esp], str.Password_OK │ │ jmp 0x8048498 │
││ ; int printf(const char *format) │ └────────────────────────────────────────┘
││ call sym.imp.printf;[oc] │ v
││ ; int status │ │
││ mov dword [esp], 0 │ │
││ ; void exit(int status) │ │
││ call sym.imp.exit;[od] │ │
│└─────────────────────────────────────┘ │
│ │
│ │
└──────────────────────────────────────────────┘
sscanf() 函数的声明。
int sscanf(const char *str, const char *format, ...)
作用是从字符串读取格式化输入。
cmp指令: 该指令与SUB指令一样执行减法的操作,但它并不保存运算结果,只是根据结果设置相关的条件标志位(SF、ZF、CF、OF)。CMP指令后往往跟着条件转移指令,实现根据比较的结果产生不同的程序分支的功能。 inc是增量指令。 inc指令: 该指令对操作数oprd加1(增量),它是一个单操作数指令。操作数可以是寄存器或存储器。由于增量指令主要用于对计数器和地址指针的调整,所以它不影响进位标志CF,对其他状态标志位的影响与add指令一样。
通过pdg查看以下decompiler的代码
[0x080484fb]> pdg
// WARNING: [r2ghidra] Detected overlap for variable var_dh
void sym.check(char *s)
{
uint32_t uVar1;
char var_dh;
uint32_t var_ch;
uint32_t var_8h;
int32_t var_4h;
var_8h = 0;
var_ch = 0;
while( true ) {
uVar1 = sym.imp.strlen(s);
if (uVar1 <= var_ch) break;
var_dh = s[var_ch];
sym.imp.sscanf(&var_dh, 0x8048638, &var_4h);
var_8h = var_8h + var_4h;
if (var_8h == 0xf) {
sym.imp.printf("Password OK!\n");
sym.imp.exit(0);
}
var_ch = var_ch + 1;
}
sym.imp.printf("Password Incorrect!\n");
return;
}
通过流程图可知核心算法在中间部分,并且可以看出存在循环。
第一次循环:
1.把var_ch内存中的值赋值给eax mov eax, dword [var_ch]
2.把arg_8h(check函数参数,也就是输入的字符串)中的值和eax相加 add eax, dword [arg_8h]
3.将eax取byte扩充(相当于取低8位) movzx eax, byte [eax]
mov byte [var_dh], al
4.将var_4h赋值给eax,注意这里是lea指令lea eax, [var_4h]
5.将eax的值赋给var_sp_8h指向的内存(为sscanf传递参数,从右向左入栈) mov dword [var_sp_8h], eax
6.将0x8048638("%d")赋给var_sp_4h指向的内存 mov dword [var_sp_4h], 0x8048638
7.eax入栈(eax和var_dh的值是一样的) lea eax, [var_dh]
mov dword [esp], eax
8.调用用sscanf函数 call sym.imp.sscanf
9.将var_4h指向的值赋给edx mov edx, dword [var_4h]
10.将var_8h的值赋给eax lea eax, [var_8h]
11.edx和eax指向的值相加 add dword [eax], edx
12.比较var_8h内存中的值和0xf cmp dword [var_8h], 0xf
13.如果相等,则出现成功的提示,如果不相等,则跳转到0x80484f4 cmp dword [var_8h], 0xf
jne 0x80484f4
假设不相等,开始第二轮的循环。
通过上面的分析,可知check函数的功能:对字符串中的每个字符取整(类似atoi),然后对每个字符相加,和0xf进行比较,只有相等的情况,才能Pass.
下面通过多种方式来破解这个程序。
知道逻辑,很容易破解这个程序。输入12345(和是0xf),Pass.
修改参数为96(和是0xf),则无论输入什么都可以使程序Pass. 32位的函数参数传递方式在strcpy为何不安全 中有所介绍。
"use strict"
var targetModule = Process.findModuleByName("crackme0x04");
var symbols = targetModule.enumerateSymbols();
var targetFun = null;
for(var i = 0; i < symbols.length; i++) {
targetFun = ptr(symbols[i].address);
if (symbols[i].name == "check" && symbols[i].type == "function"){
Interceptor.attach(targetFun,{
onEnter:function(args){
this.tmp = ptr(args[0]);
this.tmp.writeByteArray([0x39,0x36]);
console.log("success");
console.log(hexdump(this.tmp, {
offset: 0,
length: 64,
header: true,
ansi: true
}));
Thread.sleep(6);//注意这里,让线程睡眠6秒,否则console不能输出
}
});
break;
}
}
通过r2来获取96对应的asci码是0x39 0x36
运行frida脚本,这样无论输入什么的都可以Pass.
Thread.sleep(delay): suspend execution of the current thread for delay seconds specified as a number. 由于Ansi相关的函数只能用在Windows平台中,所以这里才使用byte数组。
Note that writeAnsiString() is only available (and relevant) on Windows.
修改跳转指令。使得程序无论如何都可以成功运行。
1.如果相等,则出现成功的提示,如果不相等,则跳转到0x80484f4
cmp dword [var_8h], 0xf
jne 0x80484f4
代码实现如下:
"use strict"
var targetModule = Process.findModuleByName("crackme0x04");
var symbols = targetModule.enumerateSymbols();
var targetFun = null;
for(var i = 0; i < symbols.length; i++) {
targetFun = ptr(symbols[i].address);
if (symbols[i].name == "check" && symbols[i].type == "function"){
const maxPatchSize = 64; // Do not write out of bounds, may be a temporary buffer!
Memory.patchCode(targetFun.add(0x56), maxPatchSize, code => {
const cw = new X86Writer(code);
cw.putJmpAddress(targetFun.add(0x56).add(0x2));//跳转指令,跳转到success分支
console.log(hexdump(targetFun.add(0x56).add(0x2), {
offset: 0,
length: 64,
header: true,
ansi: true
}));
cw.flush();
});
break;
}
}
代码中的0x56是跳转指令与check函数地址的偏移(offset),0x2是跳转指令的长度。
运行脚本,无论输入什么,都可以Pass。
官方文档如下,简单点说就是r2脚本编程的接口。
Radare2 provides a wide set of a features to automate boring work. It ranges from the simple sequencing of the commands to the calling scripts/another programs via IPC (Inter-Process Communication), called r2pipe.
举个栗子(本文基于的是python3.8), 获取通过r2分析得到的函数列表并且以json格式输出
import r2pipe
r2 = r2pipe.open("/bin/ls")
r2.cmd('aa')
print(r2.cmd("afl"))
print(r2.cmdj("aflj")) # evaluates JSONs and returns an object
基于此可以调试和破解程序。
这个方法综合了上述的一些方式, 我们可以用暴力破解的方式来获取密码, 也可以利用libFuzzer 来自动化找出该程序潜在的bug. 这种方式的坏处是太暴力了, 让妹子不敢靠近(逃); 好处则是在一定程度上解放了大脑, 用计算机来帮我们计算, 算力越强就越有可能找到突破点!
对于libfuzz的使用,在漏洞挖掘(1) 中对libFuzzer进行了介绍。
crackme0x04开始涉及到算法,crackerMe0x05之后难度会逐步增加,后续文章会陆续介绍。R2pipe和fuzz在这里挖个坑(主要是一两句说不清楚),哈哈。
更多逆向破解内容,欢迎关注我的微信公众号:无情剑客。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。