函数调用、花指令与smc
函数调用
以下以x86汇编为例。
#栈
从两个角度去理解栈的概念:
-
数据结构:后进先出(last-in-first-out)的一种数据结构
-
二进制程序:程序中用来存储局部变量和返回地址的一块连续内存
在pwndbg(增强版gdb)中使用
vmmap
指令可以查看程序内存空间。可以看见有一段属于栈空间
|
|
#寄存器
-
esp(extended stack pointer):指向栈顶
-
ebp(extended base pointer):指向栈底,栈基址
-
eip(extended instruction pointer):指向下一条要执行的指令
#push & pop
-
push var:将var入栈。先
esp -= 4
,然后向esp指向的地方写入xxx -
pop reg:将栈顶元素出栈存在reg中。从esp指向的地方取4字节值,放到reg中,然后
esp += 4
#函数调用过程
-
调用函数:
1
call sub_xxx ; push eip, eip = xxx
-
初始化栈:
1 2 3
push ebp ; 保存调用函数栈基址 mov ebp, esp ; 开启空的新栈 sub esp, xxx ; 给局部变量预留空间
-
执行函数体:
函数返回值会保存在eax中
-
函数返回:
1 2 3
leave ; mov esp, ebp ; pop ebp retn ; pop eip
-
调用者清理调用时栈上分配的参数(cdecl)
1
add esp, xxx
以一个简单的C程序为例:
|
|
gcc编译:gcc -o main -m32 main.c
分析add
函数调用过程栈和寄存器的变化:
#调用约定
微软就喜欢搞事情
X86:
-
cdecl(C declaration):C语言的事实上的标准。参数从右至左入栈,调用者清理栈上参数。
-
stdcall:Windows API的标准调用约定。参数从右往左入栈,被调用者清理栈上参数。
-
pascal:基于Pascal语言的调用约定。参数从左至右入栈,被调用者清理栈上参数。
X64:
与X86的区别主要是前6个参数使用寄存器传递。
-
微软x86-64调用约定:使用RCX, RDX, R8, R9四个寄存器用于存储函数调用时的4个参数(从左到右),使用XMM0, XMM1, XMM2, XMM3来传递浮点变量。其他的参数直接入栈(从右至左)。整型返回值放置在RAX中,浮点返回值在XMM0中。
-
System V AMD64 ABI:主要在Solaris,GNU/Linux,FreeBSD和其他非微软OS上使用。头六个整型参数放在寄存器RDI, RSI, RDX, RCX, R8和R9上;同时XMM0到XMM7用来放置浮点变元.
SMC与花指令
#逆向与反逆向的博弈
-
逆向:一般是指从二进制文件倒推回源代码进行分析的过程
-
反逆向:开发人员为了避免软件被随意修改,想出了一系列方法,在不影响软件使用的前提下,提高软件的逆向分析难度
#两种基础的反逆向手段
-
SMC
-
花指令
#Self-Modifying Code
-
即代码自修改技术,简称SMC
-
当你直接用IDA打开查看源码时,被修改的部分会呈现出乱码的状态,程序在运行过程中会执行一段修改自身的代码,使得这部分代码变成正确的指令,从而正确执行
-
我们要做的就是通过分析程序未加密的部分,找到用来修改自身的那部分代码,然后手动进行修复并解密
特征:乱码,virtualprotect(PE)、mprotect(ELF),将函数作为地址进行运算
#花指令
-
由设计者特别构思,希望使反汇编的时候出错,让破解者无法清楚正确地反汇编程序的内容,迷失方向
-
直接导致的结果就是,会使IDA的自动分析失败,产生大量未知数据
-
这时就需要我们来识破这些花指令,引导IDA正常地分析
举例:
- 垃圾字节:最常见
|
|
- 纯垃圾代码:ransomware
|
|
- 扰乱堆栈平衡的垃圾代码:eflo
|
|
- ret实现隐式跳转:
|
|
思路来源:NCTF2022的ccccha