- 第一章 CPU 简介
- 第二章 Hello,world!
- 第三章 函数开始和结束
- 第四章 栈
- Chapter 5 printf() 与参数处理
- Chapter 6 scanf()
- CHAPER7 访问传递参数
- Chapter 8 一个或者多个字的返回值
- Chapter 9 指针
- Chapter 10 条件跳转
- 第 11 章 选择结构 switch()/case/default
- 第 12 章 循环结构
- 第 13 章 strlen()
- Chapter 14 Division by 9
- chapter 15 用 FPU 工作
- Chapter 16 数组
- Chapter 17 位域
- 第 18 章 结构体
- 19 章 联合体
- 第二十章 函数指针
- 第 21 章 在 32 位环境中的 64 位值
- 第二十二章 SIMD
- 23 章 64 位化
- 24 章 使用 x64 下的 SIMD 来处理浮点数
- 25 章 温度转换
- 26 章 C99 的限制
- 27 章 内联函数
- 第 28 章 得到不正确反汇编结果
- 第 29 章 花指令
- 第 30 章 16 位 Windows
- 第 31 章 类
- 三十二 ostream
- 34.2.2 MSVC
- 34.2.3 C++ 11 std::forward_list
- 34.3 std::vector
- 34.4 std::map and std::set
6.6 scanf() 结果检查
正如我之前所见的,现在使用 scanf() 有点过时了,但是如过我们不得不这样做时,我们需要检查 scanf() 执行完毕时是否发生了错误。
#!bash
#include <stdio.h>
int main()
{
int x;
printf ("Enter X:
");
if (scanf ("%d", &x)==1)
printf ("You entered %d...
", x);
else
printf ("What you entered? Huh?
");
return 0;
};
按标准,scanf() 函数返回成功获取的字段数。
在我们的例子中,如果事情顺利,用户输入一个数字,scanf() 将会返回 1 或 0 或者错误情况下返回 EOF.
这里,我们添加了一些检查 scanf() 结果的 c 代码,用来打印错误信息:
按照预期的回显:
#!bash
C:...>ex3.exe
Enter X:
123
You entered 123...
C:...>ex3.exe
Enter X:
ouch
What you entered? Huh?
6.6.1 MSVC: x86
我们可以得到这样的汇编代码(msvc2010):
#!bash
lea eax, DWORD PTR _x$[ebp]
push eax
push OFFSET $SG3833 ; ’%d’, 00H
call _scanf
add esp, 8
cmp eax, 1
jne SHORT $LN2@main
mov ecx, DWORD PTR _x$[ebp]
push ecx
push OFFSET $SG3834 ; ’You entered %d...’, 0aH, 00H
call _printf
add esp, 8
jmp SHORT $LN1@main
$LN2@main:
push OFFSET $SG3836 ; ’What you entered? Huh?’, 0aH, 00H
call _printf
add esp, 4
$LN1@main:
xor eax, eax
调用函数(main()) 必须能够访问到被调用函数(scanf()) 的结果,所以 callee 把这个值留在了 EAX 寄存器中。
然后我们在" CMP EAX, 1
"指令的帮助下,换句话说,我们将 eax 中的值与 1 进行比较。
JNE 根据 CMP 的结果判断跳至哪,JNE 表示(jump if Not Equal)
所以,如果 EAX 中的值不等于 1,那么处理器就会将执行流程跳转到 JNE 指向的,在我们的例子中是$LN2@main,当流程跳到这里时,CPU 将会带着参数"What you entered? Huh?"执行 printf(),但是执行正常,就不会发生跳转,然后另外一个 printf() 就会执行,两个参数为" You entered %d…
"及 x 变量的值。
因为第二个 printf() 并没有被执行,后面有一个 JMP(无条件跳转),就会将执行流程到第二个 printf() 后"XOR EAX, EAX"前,执行完返回 0。
那么,可以这么说,比较两个值通常使用 CMP/Jcc 这对指令,cc 是条件码,CMP 比较两个值,然后设置 processor flag,Jcc 检查 flags 然后判断是否跳。
但是事实上,这却被认为是诡异的。但是 CMP 指令事实上,但是 CMP 指令实际上是 SUB(subtract),所有算术指令都会设置 processor flags,不仅仅只有 CMP,当我们比较 1 和 1 时,1 结果就变成了 0,ZF flag 就会被设定(表示最后一次的比较结果为 0),除了两个数相等以外,再没有其他情况了。JNE 检查 ZF flag,如果没有设定就会跳转。JNE 实际上就是 JNZ(Jump if Not Zero) 指令。JNE 和 JNZ 的机器码都是一样的。所以 CMP 指令可以被 SUB 指令代替,几乎一切的都没什么变化。但是 SUB 会改变第一个数,CMP 是"SUB without saving result".
6.6.2 MSVC: x86:IDA
现在是时候打开 IDA 然后尝试做些什么了,顺便说一句。对于初学者来说使用在 MSVC 中使用/MD 是个非常好的主意。这样所有独立的函数不会从可执行文件中 link,而是从 MSVCR*.dll。因此这样可以简单明了的发现函数在哪里被调用。
当在 IDA 中分析代码时,建议一定要做笔记。比如在分析这个例子的时候,我们看到了 JNZ 将要被设置为 error,所以点击标注,然后标注为"error"。另外一处标注在"exit":
#!bash
.text:00401000 _main proc near
.text:00401000
.text:00401000 var_4 = dword ptr -4
.text:00401000 argc = dword ptr 8
.text:00401000 argv = dword ptr 0Ch
.text:00401000 envp = dword ptr 10h
.text:00401000
.text:00401000 push ebp
.text:00401001 mov ebp, esp
.text:00401003 push ecx
.text:00401004 push offset Format ; "Enter X:
"
.text:00401009 call ds:printf
.text:0040100F add esp, 4
.text:00401012 lea eax, [ebp+var_4]
.text:00401015 push eax
.text:00401016 push offset aD ; "%d"
.text:0040101B call ds:scanf
.text:00401021 add esp, 8
.text:00401024 cmp eax, 1
.text:00401027 jnz short error
.text:00401029 mov ecx, [ebp+var_4]
.text:0040102C push ecx
.text:0040102D push offset aYou ; "You entered %d...
"
.text:00401032 call ds:printf
.text:00401038 add esp, 8
.text:0040103B jmp short exit
.text:0040103D ; ---------------------------------------------------------------------------
.text:0040103D
.text:0040103D error: ; CODE XREF: _main+27
.text:0040103D push offset aWhat ; "What you entered? Huh?
"
.text:00401042 call ds:printf
.text:00401048 add esp, 4
.text:0040104B
.text:0040104B exit: ; CODE XREF: _main+3B
.text:0040104B xor eax, eax
.text:0040104D mov esp, ebp
.text:0040104F pop ebp
.text:00401050 retn
.text:00401050 _main endp
现在理解代码就变得非常简单了。然而过分的标注指令却不是一个好主意。
函数的一部分有可能也会被 IDA 隐藏:
我隐藏了两部分然后分别给它们命名:
#!bash
.text:00401000 _text segment para public ’CODE’ use32
.text:00401000 assume cs:_text
.text:00401000 ;org 401000h
.text:00401000 ; ask for X
.text:00401012 ; get X
.text:00401024 cmp eax, 1
.text:00401027 jnz short error
.text:00401029 ; print result
.text:0040103B jmp short exit
.text:0040103D ; ---------------------------------------------------------------------------
.text:0040103D
.text:0040103D error: ; CODE XREF: _main+27
.text:0040103D push offset aWhat ; "What you entered? Huh?
"
.text:00401042 call ds:printf
.text:00401048 add esp, 4
.text:0040104B
.text:0040104B exit: ; CODE XREF: _main+3B
.text:0040104B xor eax, eax
.text:0040104D mov esp, ebp
.text:0040104F pop ebp
.text:00401050 retn
.text:00401050 _main endp
如果要显示这些隐藏的部分,我们可以点击数字上的+。
为了压缩"空间",我们可以看到 IDA 怎样用图表代替一个函数的(见图 6.7),然后在每个条件跳转处有两个箭头,绿色和红色。绿色箭头代表如果跳转触发的方向,红色则相反。
当然可以折叠节点,然后备注名称,我像这样处理了 3 块(见图 6.8):
这个非常的有用。可以这么说,逆向工程师很重要的一点就是缩小他所有的信息。
图 6.7: IDA 图形模式
图 6.8: Graph mode in IDA with 3 nodes folded
6.6.3 MSVC: x86 + OllyDbg
让我们继续在 OllyDbg 中看这个范例程序,使它认为 scanf() 怎么运行都不会出错。
当本地变量地址被传递给 scanf() 时,这个变量还有一些垃圾数据。这里是 0x4CD478:见图 6.10
当 scanf() 执行时,我在命令行窗口输入了一些不是数字的东西,像"asdasd".scanf() 结束后 eax 变为了 0.也就意味着有错误发生:见图 6.11
我们也可以发现栈中的本地变量并没有发生变化,scanf() 会在那里写入什么呢?其实什么都没有,只是返回了 0.
现在让我们尝试修改这个程序,右击 EAX,在选项中有个"set to 1",这正是我们所需要的。
现在 EAX 是 1 了。那么接下来的检查就会按照我们的需求执行,然后 printf() 将会打印出栈上的变量。
按下 F9 我们可以在窗口中看到:
图 6.9
实际上,5035128 是栈上一个数据(0x4CD478) 的十进制表示!
图 6.10
图 6.11
6.6.4 MSVC: x86 + Hlew
这也是一个关于可执行文件 patch 的简单例子,我们之前尝试 patch 程序,所以程序总是打印数字,不管我们输入什么。
假设编译时并没有使用/MD,我们可以在.text 开始的地方找到 main() 函数,现在让我们在 Hiew 中打开执行文件。找到.text 的开始处(enter,F8,F6,enter,enter)
我们可以看到这个:表 6.13
然后按下 F9(update),现在文件保存在了磁盘中,就像我们想要的。
两个 NOP 可能看起来并不是那么完美,另一个方法是把 0 写在第二处(jump offset),所以 JNZ 就可以总是跳到下一个指令了。
另外我们也可以这样做:替换第一个字节为 EB,这样就不修改第二处(jump offset),这样就会无条件跳转,不管我们输入什么,错误信息都可以打印出来了。
图 6.12:main() 函数
图 6.13:Hiew 用两个 NOP 替换 JNZ
6.6.5 GCC: x86
生成的代码和 gcc 4.4.1 是一样的,除了我们之前已经考虑过的
6.6.6 MSVC: x64
因为我们这里处理的是无整型变量。在 x86-64 中还是 32bit,我们可以看出 32bit 的寄存器(前缀为 E) 在这种情况下是怎样使用的,然而 64bit 的寄存也有被使用(前缀 R)
#!bash
_DATA SEGMENT
$SG2924 DB ’Enter X:’, 0aH, 00H
$SG2926 DB ’%d’, 00H
$SG2927 DB ’You entered %d...’, 0aH, 00H
$SG2929 DB ’What you entered? Huh?’, 0aH, 00H
_DATA ENDS
_TEXT SEGMENT
x$ = 32
main PROC
$LN5:
sub rsp, 56
lea rcx, OFFSET FLAT:$SG2924 ; ’Enter X:’
call printf
lea rdx, QWORD PTR x$[rsp]
lea rcx, OFFSET FLAT:$SG2926 ; ’%d’
call scanf
cmp eax, 1
jne SHORT $LN2@main
mov edx, DWORD PTR x$[rsp]
lea rcx, OFFSET FLAT:$SG2927 ; ’You entered %d...’
call printf
jmp SHORT $LN1@main
$LN2@main:
lea rcx, OFFSET FLAT:$SG2929 ; ’What you entered? Huh?’
call printf
$LN1@main:
; return 0
xor eax, eax
add rsp, 56
ret 0
main ENDP
_TEXT ENDS
END
6.6.7 ARM:Optimizing Keil + thumb mode
#!bash
var_8 = -8
PUSH {R3,LR}
ADR R0, aEnterX ; "Enter X:
"
BL __2printf
MOV R1, SP
ADR R0, aD ; "%d"
BL __0scanf
CMP R0, #1
BEQ loc_1E
ADR R0, aWhatYouEntered ; "What you entered? Huh?
"
BL __2printf
loc_1A ; CODE XREF: main+26
MOVS R0, #0
POP {R3,PC}
loc_1E ; CODE XREF: main+12
LDR R1, [SP,#8+var_8]
ADR R0, aYouEnteredD___ ; "You entered %d...
"
BL __2printf
B loc_1A
这里有两个新指令 CMP 和 BEQ.
CMP 和 x86 指令中的相似,它会用一个参数减去另外一个参数然后保存 flag.
BEQ 是跳向另一处地址,如果数相等就会跳,如果最后一次比较结果为 0,或者 Z flag 是 1。和 x86 中的 JZ 是一样的。
其他的都很简单,执行流程分为两个方向,当 R0 被写入 0 后,两个方向则会合并,作为函数的返回值,然后函数结束。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论