使用 IDA 打开 HOLA_REVERSER.exe 文件,IDA 会提示我们是否加载符号文件。由于这个程序不是我们写的,所以我们没有符号文件。点击否即可。
IDA 7.7 已经很聪明了,打开该文件后,自动定位到了 main 函数。
这个程序很简单,我们只需要分析 main 函数即可。
main函数
; Attributes: bp-based frame
; int __cdecl main(int argc, const char **argv, const char **envp)
_main proc near
Buffer= byte ptr -7Ch
var_4= dword ptr -4
argc= dword ptr 8
argv= dword ptr 0Ch
envp= dword ptr 10h
这里的信息可以看出来,该函数有3个参数,2个局部变量,以前说过,局部变量为负偏移。
单击其中任何一处,IDA 将会跳转到一个栈视图,静态地显示函数参数、局部变量,以及他们之间的距离:
可以看到 Buffer 变量占据了 120 个字节。那么它是一个什么类型呢?结构体还是数组还是其他的。
这里就需要结合汇编与反汇编代码来分析了。
切到反汇编视图,可以清楚的看到它是一个 char[] 类型。
我们看汇编代码,使用 Buffer 的位置:
lea eax, [ebp+Buffer]
push 14h ; Size
push eax ; Buffer
call ds:gets_s
-----------------------------------------------------------------------
lea eax, [ebp+Buffer]: 将 Buffer 的地址加载到 eax 寄存器中。
push 14h: 将缓冲区的大小(20 字节,16 进制表示)压入栈中。
push eax: 将指向缓冲区的指针压入栈中。
call ds:gets_s: 调用 gets_s 函数,从标准输入读取字符串并存储到指定的缓冲区中。
这里调用了 gets_s 函数,而且参数的传递没有使用 eax 等寄存器,是直接使用的栈(push指令)。
这些汇编指令对应的代码为:
char Buffer[120];
gets_s(Buffer, 0x14u);
gets_s是一个标准函数,它接收一个 char * 类型:
char *gets_s(char *str, rsize_t size);
所以,我们确定 Buffer 是一个 char[] 类型。
在栈视图里面,右键单击 Buffer 这个位置,选择 Array 选项转化为数组可以将其转化为字符数组或者说缓存区:
点击 OK 之后,栈视图如下:
DUP 表示重复(duplicate)120 次。
来详细分析这个静态栈视图,先看x86函数调用过程与栈帧:
有了上图的概念之后,我们先来关注下 Buffer 下面的一个命名为 var_4,大小是 dword 的局部变量。
它使用的位置:
mov eax, ___security_cookie
xor eax, ebp
mov [ebp+var_4], eax
...
mov ecx, [ebp+var_4]
xor eax, eax
xor ecx, ebp ; StackCookie
call @__security_check_cookie@4 ; __security_check_cookie(x)
在反汇编代码中,我们并没有看到这个变量。有些读者可能不知道,这是源代码中没有的用来防止栈溢出的 cookie。在函数的开始保存这个 cookie值,结束之前再检查这个值。
剩下的3个参数,程序并没有使用到,直接跳过。
接着分析程序汇编代码:
push esi ; ArgList
push offset Format ; "Pone un numerito\n"
call sub_401010
调用了sub_401010
这个函数,参数采用栈传递。
我们看看 sub_401010
这个函数的内容:
; Attributes: bp-based frame
; int sub_401010(char *Format, ...)
sub_401010 proc near
Format= dword ptr 8
ArgList= byte ptr 0Ch
push ebp
mov ebp, esp
push esi
mov esi, [ebp+Format]
push 1 ; Ix
call ds:__acrt_iob_func
add esp, 4
lea ecx, [ebp+ArgList]
push ecx ; ArgList
push 0 ; Locale
push esi ; Format
push eax ; Stream
call sub_401000
push dword ptr [eax+4]
push dword ptr [eax] ; Options
call ds:__stdio_common_vfprintf
add esp, 18h
pop esi
pop ebp
retn
sub_401010 endp
没什么特殊的内容,基本上就是调用了一下 vfprintf
函数。我们可以将这个函数重命名一下,便于后续分析。
接下来:
lea eax, [ebp+Buffer]
push 14h ; Size
push eax ; Buffer
call ds:gets_s
显然就是调用了 gets_s
函数,将用户的输入放到 Buffer 中。
再接下来:
lea eax, [ebp+Buffer]
push eax ; String
call ds:atoi
将输入的字符串转成整数。
注意:atoi 函数将字符串转换为一个整数,如果因为数字太大导致无法转换,会产生错误并且返回 0。当然如果比最小的负整数(int)还小,也会出错返回 0。所有输入的内容都会被转换为一个整数。如果输入 41424344,将会被转换为一个十六进制数保存到 EAX。
再接下来:
mov esi, eax
lea eax, [ebp+Buffer]
push eax ; ArgList
push offset aTipeasteS ; "Tipeaste: %s\n"
call sub_401010
先将 EAX 返回值会传给 ESI。
然后是打印字符串原始内容,展示给用户。
再接下来:
add esp, 18h
cmp esi, 124578h
pop esi
jnz short loc_401094
在输出用户输入的原始字符串后,ESI 的值会与 0x124578 进行比较。
我们也可以高亮 esi 寄存器,看看分析流程是否有遗漏:
可以看到,只有上面只有一处赋值,而 eax 又是存放函数返回值的,所以 esi 就是 atoi 函数的返回值。
用户输入的十进制数字符串会被解析并返回一个十六进制数,然后再与那个硬编值进行比较,如果输入硬编值对应的十进制数,应该会成功。
如果比较不相等或不为零(JNZ),将会输出 BAD REVERSER,如果相等,将会输出 GOOD REVERSER。
使用 Python 来转换 0x124578 对应的十进制数:1197432。
我们重新运行一下程序,输入 1197432:
这是非常简单的一个静态逆向案例。
二手的程序员
主要研究Android逆向相关的知识。文章均会同步到博客,且持续修订:lyldalek.top
公众号