6.6 scanf()结果检查

正如我之前所见的,现在使用scanf()有点过时了,但是如过我们不得不这样做时,我们需要检查scanf()执行完毕时是否发生了错误。

  1. #include <stdio.h>
  2. int main()
  3. {
  4. int x;
  5. printf ("Enter X:
  6. ");
  7. if (scanf ("%d", &x)==1)
  8. printf ("You entered %d...
  9. ", x);
  10. else
  11. printf ("What you entered? Huh?
  12. ");
  13. return 0;
  14. };

按标准,scanf()函数返回成功获取的字段数。

在我们的例子中,如果事情顺利,用户输入一个数字,scanf()将会返回1或0或者错误情况下返回EOF.

这里,我们添加了一些检查scanf()结果的c代码,用来打印错误信息:

按照预期的回显:

  1. C:...>ex3.exe
  2. Enter X:
  3. 123
  4. You entered 123...
  5. C:...>ex3.exe
  6. Enter X:
  7. ouch
  8. What you entered? Huh?

6.6.1 MSVC: x86

我们可以得到这样的汇编代码(msvc2010):

  1. lea eax, DWORD PTR _x$[ebp]
  2. push eax
  3. push OFFSET $SG3833 ; ’%d’, 00H
  4. call _scanf
  5. add esp, 8
  6. cmp eax, 1
  7. jne SHORT $LN2@main
  8. mov ecx, DWORD PTR _x$[ebp]
  9. push ecx
  10. push OFFSET $SG3834 ; You entered %d...’, 0aH, 00H
  11. call _printf
  12. add esp, 8
  13. jmp SHORT $LN1@main
  14. $LN2@main:
  15. push OFFSET $SG3836 ; What you entered? Huh?’, 0aH, 00H
  16. call _printf
  17. add esp, 4
  18. $LN1@main:
  19. 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”:

  1. .text:00401000 _main proc near
  2. .text:00401000
  3. .text:00401000 var_4 = dword ptr -4
  4. .text:00401000 argc = dword ptr 8
  5. .text:00401000 argv = dword ptr 0Ch
  6. .text:00401000 envp = dword ptr 10h
  7. .text:00401000
  8. .text:00401000 push ebp
  9. .text:00401001 mov ebp, esp
  10. .text:00401003 push ecx
  11. .text:00401004 push offset Format ; "Enter X:
  12. "
  13. .text:00401009 call ds:printf
  14. .text:0040100F add esp, 4
  15. .text:00401012 lea eax, [ebp+var_4]
  16. .text:00401015 push eax
  17. .text:00401016 push offset aD ; "%d"
  18. .text:0040101B call ds:scanf
  19. .text:00401021 add esp, 8
  20. .text:00401024 cmp eax, 1
  21. .text:00401027 jnz short error
  22. .text:00401029 mov ecx, [ebp+var_4]
  23. .text:0040102C push ecx
  24. .text:0040102D push offset aYou ; "You entered %d...
  25. "
  26. .text:00401032 call ds:printf
  27. .text:00401038 add esp, 8
  28. .text:0040103B jmp short exit
  29. .text:0040103D ; ---------------------------------------------------------------------------
  30. .text:0040103D
  31. .text:0040103D error: ; CODE XREF: _main+27
  32. .text:0040103D push offset aWhat ; "What you entered? Huh?
  33. "
  34. .text:00401042 call ds:printf
  35. .text:00401048 add esp, 4
  36. .text:0040104B
  37. .text:0040104B exit: ; CODE XREF: _main+3B
  38. .text:0040104B xor eax, eax
  39. .text:0040104D mov esp, ebp
  40. .text:0040104F pop ebp
  41. .text:00401050 retn
  42. .text:00401050 _main endp

现在理解代码就变得非常简单了。然而过分的标注指令却不是一个好主意。

函数的一部分有可能也会被IDA隐藏:

我隐藏了两部分然后分别给它们命名:

  1. .text:00401000 _text segment para public CODE use32
  2. .text:00401000 assume cs:_text
  3. .text:00401000 ;org 401000h
  4. .text:00401000 ; ask for X
  5. .text:00401012 ; get X
  6. .text:00401024 cmp eax, 1
  7. .text:00401027 jnz short error
  8. .text:00401029 ; print result
  9. .text:0040103B jmp short exit
  10. .text:0040103D ; ---------------------------------------------------------------------------
  11. .text:0040103D
  12. .text:0040103D error: ; CODE XREF: _main+27
  13. .text:0040103D push offset aWhat ; "What you entered? Huh?
  14. "
  15. .text:00401042 call ds:printf
  16. .text:00401048 add esp, 4
  17. .text:0040104B
  18. .text:0040104B exit: ; CODE XREF: _main+3B
  19. .text:0040104B xor eax, eax
  20. .text:0040104D mov esp, ebp
  21. .text:0040104F pop ebp
  22. .text:00401050 retn
  23. .text:00401050 _main endp

如果要显示这些隐藏的部分,我们可以点击数字上的+。

为了压缩“空间”,我们可以看到IDA怎样用图表代替一个函数的(见图6.7),然后在每个条件跳转处有两个箭头,绿色和红色。绿色箭头代表如果跳转触发的方向,红色则相反。

当然可以折叠节点,然后备注名称,我像这样处理了3块(见图 6.8):

这个非常的有用。可以这么说,逆向工程师很重要的一点就是缩小他所有的信息。

6.6 scanf()结果检查 - 图1

图6.7: IDA 图形模式

6.6 scanf()结果检查 - 图2

图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.6 scanf()结果检查 - 图3

图6.9

实际上,5035128是栈上一个数据(0x4CD478)的十进制表示!

6.6 scanf()结果检查 - 图4

图6.10

6.6 scanf()结果检查 - 图5

图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.6 scanf()结果检查 - 图6

图6.12:main()函数

6.6 scanf()结果检查 - 图7

图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)

  1. _DATA SEGMENT
  2. $SG2924 DB Enter X:’, 0aH, 00H
  3. $SG2926 DB ’%d’, 00H
  4. $SG2927 DB You entered %d...’, 0aH, 00H
  5. $SG2929 DB What you entered? Huh?’, 0aH, 00H
  6. _DATA ENDS
  7. _TEXT SEGMENT
  8. x$ = 32
  9. main PROC
  10. $LN5:
  11. sub rsp, 56
  12. lea rcx, OFFSET FLAT:$SG2924 ; Enter X:’
  13. call printf
  14. lea rdx, QWORD PTR x$[rsp]
  15. lea rcx, OFFSET FLAT:$SG2926 ; ’%d
  16. call scanf
  17. cmp eax, 1
  18. jne SHORT $LN2@main
  19. mov edx, DWORD PTR x$[rsp]
  20. lea rcx, OFFSET FLAT:$SG2927 ; You entered %d...’
  21. call printf
  22. jmp SHORT $LN1@main
  23. $LN2@main:
  24. lea rcx, OFFSET FLAT:$SG2929 ; What you entered? Huh?’
  25. call printf
  26. $LN1@main:
  27. ; return 0
  28. xor eax, eax
  29. add rsp, 56
  30. ret 0
  31. main ENDP
  32. _TEXT ENDS
  33. END

6.6.7 ARM:Optimizing Keil + thumb mode

  1. var_8 = -8
  2. PUSH {R3,LR}
  3. ADR R0, aEnterX ; "Enter X:
  4. "
  5. BL __2printf
  6. MOV R1, SP
  7. ADR R0, aD ; "%d"
  8. BL __0scanf
  9. CMP R0, #1
  10. BEQ loc_1E
  11. ADR R0, aWhatYouEntered ; "What you entered? Huh?
  12. "
  13. BL __2printf
  14. loc_1A ; CODE XREF: main+26
  15. MOVS R0, #0
  16. POP {R3,PC}
  17. loc_1E ; CODE XREF: main+12
  18. LDR R1, [SP,#8+var_8]
  19. ADR R0, aYouEnteredD___ ; "You entered %d...
  20. "
  21. BL __2printf
  22. B loc_1A

这里有两个新指令CMP 和BEQ.

CMP和x86指令中的相似,它会用一个参数减去另外一个参数然后保存flag.

BEQ是跳向另一处地址,如果数相等就会跳,如果最后一次比较结果为0,或者Z flag是1。和x86中的JZ是一样的。

其他的都很简单,执行流程分为两个方向,当R0被写入0后,两个方向则会合并,作为函数的返回值,然后函数结束。