栈帧是由当前函数管理的栈的一部分,包含该函数使用的数据。
背景
栈帧通常包含以下数据:
-
局部和临时变量; -
传入参数(对于使用栈传递参数的调用约定); -
保存的易失性寄存器; -
其他记录信息(例如x86上的返回地址)。
由于栈在执行过程中可能会不可预测地变化,栈帧及其部分没有固定地址。因此,IDA使用伪结构来表示其布局。这个结构与结构视图中的其他结构非常相似,但有一些区别:
-
帧结构没有名称,不包含在全局结构列表中;只能从相应的函数中访问。 -
显示的是从帧指针的偏移(正负都有),而不是从结构开始的偏移。 -
它可能包含特殊成员来表示保存的返回地址和/或保存的寄存器区域。
栈帧视图
要打开栈帧视图:
-
编辑 > 函数 > 栈变量… 或在反汇编(IDA视图)中定位到一个函数时按 Ctrl
–K
; -
在反汇编或伪代码中双击或按 Enter
键选择一个栈变量。
在此视图中,您可以执行与结构视图中大多数相同的操作:
-
定义新的或更改现有的栈变量( D
); -
重命名变量( N
); -
创建数组( *
)或结构实例(Alt
–Q
)。
示例
考虑这个有漏洞的程序:
#include <stdio.h>
int main () {
char username[8];
int allow = 0;
printf("Enter your username, please: ");
gets(username); // 用户输入“malicious”
if (grantAccess(username)) {
allow = 1;
}
if (allow != 0) { // 被用户名的溢出覆盖。
privilegedAction();
return 0;
}
}
当由旧版GCC编译时,可能会生成以下汇编:
.text:0000000000400580 main proc near; DATA XREF: _start+1D↑o
.text:0000000000400580
.text:0000000000400580 var_10= byte ptr -10h
.text:0000000000400580 var_4= dword ptr -4
.text:0000000000400580
.text:0000000000400580 ; __unwind {
.text:0000000000400580 push rbp
.text:0000000000400581 mov rbp, rsp
.text:0000000000400584 sub rsp, 10h
.text:0000000000400588 mov [rbp+var_4], 0
.text:000000000040058F mov edi, offset format; "Enter your username, please: "
.text:0000000000400594 mov eax, 0
.text:0000000000400599 call printf
.text:000000000040059E lea rax, [rbp+var_10]
.text:00000000004005A2 mov rdi, rax
.text:00000000004005A5 call gets
.text:00000000004005AA lea rax, [rbp+var_10]
.text:00000000004005AE mov rdi, rax
.text:00000000004005B1 call grantAccess
.text:00000000004005B6 test eax, eax
.text:00000000004005B8 jz short loc_4005C1
.text:00000000004005BA mov [rbp+var_4], 1
.text:00000000004005C1
.text:00000000004005C1 loc_4005C1: ; CODE XREF: main+38↑j
.text:00000000004005C1 cmp [rbp+var_4], 0
.text:00000000004005C5 jz short loc_4005D1
.text:00000000004005C7 mov eax, 0
.text:00000000004005CC call privilegedAction
.text:00000000004005D1
.text:00000000004005D1 loc_4005D1: ; CODE XREF: main+45↑j
.text:00000000004005D1 mov eax, 0
.text:00000000004005D6 leave
.text:00000000004005D7 retn
.text:00000000004005D7 ; } // starts at 400580
.text:00000000004005D7 main endp
打开栈帧后,我们可以看到以下图像:
通过比较源代码和反汇编,我们可以推断出var_10
是username
,var_4
是allow
。由于代码只获取缓冲区的起始地址,IDA无法检测其完整大小并创建了一个单字节变量。为了改进它,按*
键将var_10
转换为8字节数组。我们还可以将变量重命名为其正确名称。
由于IDA以自然内存顺序显示栈帧布局(地址向下增加),我们可以立即看到漏洞代码演示的问题:gets
函数没有边界检查,因此输入长字符串可以溢出username
缓冲区并覆盖allow
变量。由于代码只检查非零值,这将绕过检查并导致执行privilegedAction
函数。
帧偏移和栈变量
如上所述,在栈帧视图中,结构偏移是相对于帧指针显示的。在某些情况下,如上例中,它是一个实际的处理器寄存器(RBP
)。例如,变量allow
位于距帧指针-4
的偏移处,IDA在反汇编列表中使用此值作为符号名称而不是原始数值偏移:
.text:0000000000400580 allow= dword ptr -4
[...]
.text:0000000000400588 mov [rbp+allow], 0
[...]
通过按#
或K
键,您可以要求IDA显示指令的原始形式:
.text:0000000000400588 mov dword ptr [rbp-4], 0
再次按K
键返回到栈变量表示。
在其他情况下,帧指针可以只是一个用于方便的任意位置(通常是函数入口处栈指针值的固定偏移)。这在使用帧指针省略编译的二进制文件中很常见,这是一种常见的优化技术。在这种情况下,IDA可能会使用额外的增量来补偿函数不同部分中栈指针的变化。例如,考虑这个函数:
.text:10001030 sub_10001030 proc near; DATA XREF: sub_100010B0:loc_100010E7↓o
.text:10001030
.text:10001030 LCData= byte ptr -0Ch
.text:10001030 var_4= dword ptr -4
.text:10001030
.text:10001030 sub esp, 0Ch
.text:10001033 mov eax, dword_100B2960
.text:10001038 push esi
.text:10001039 mov [esp+10h+var_4], eax
.text:1000103D xor esi, esi
.text:1000103F call ds:GetThreadLocale
.text:10001045 push 7 ; cchData
.text:10001047 lea ecx, [esp+14h+LCData]
.text:1000104B push ecx ; lpLCData
.text:1000104C push 1004h ; LCType
.text:10001051 push eax ; Locale
.text:10001052 call ds:GetLocaleInfoA
.text:10001058 test eax, eax
.text:1000105A jz short loc_1000107D
.text:1000105C mov al, [esp+10h+LCData]
.text:10001060 test al, al
.text:10001062 lea ecx, [esp+10h+LCData]
.text:10001066 jz short loc_1000107D
在这里,显式帧指针(ebp)未使用,IDA安排栈帧以便返回地址放置在偏移0处:
-00000010 ; Frame size: 10; Saved regs: 0; Purge: 0
-00000010 ;
-00000010
-00000010 db ? ; undefined
-0000000F db ? ; undefined
-0000000E db ? ; undefined
-0000000D db ? ; undefined
-0000000C LCData db ?
-0000000B db ? ; undefined
-0000000A db ? ; undefined
-00000009 db ? ; undefined
-00000008 db ? ; undefined
-00000007 db ? ; undefined
-00000006 db ? ; undefined
-00000005 db ? ; undefined
-00000004 var_4 dd ?
+00000000 rdb 4 dup(?)
+00000004
+00000004 ; end of stack variables
为了补偿栈指针的变化(sub esp, 0Ch
和push
指令),在栈变量操作数中需要添加值10h
或14h
。因此,我们可以很容易地看到10001047
和1000105C
处的指令引用相同的变量,即使在原始形式中它们使用不同的偏移([esp+8]
和[esp+4]
)。
更多文章
立即关注【二进制磨剑】公众号
原文始发于微信公众号(二进制磨剑):IDA 技巧(65) 栈帧视图
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论