IDA PRO 实践03 - 函数栈逆向分析

141 阅读5分钟

使用 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

公众号