# Chapter 6 scanf()
現在我們來使用scanf()。
```
#!bash
#include <stdio.h>
int main()
{
int x;
printf ("Enter X:
");
scanf ("%d", &x);
printf ("You entered %d...
", x);
return 0;
};
```
好吧,我承認現在使用scanf()是不明智的,但是我想說明如何把指針傳遞給int變量。
## 6.1 關于指針
這是計算機科學中最基礎的概念之一。通常,大數組、結構或對象經常被傳遞給其它函數,而傳遞它們的地址要更加簡單。更重要的是:如果調用函數要修改數組或結構中的數據,并且作為整體返回,那么最簡單的辦法就是把數組或結構的地址傳遞給函數,讓函數進行修改。
在C/C++中指針就是某處內存的地址。
在x86中,地址是以32位數表示的(占4字節);在x86-64中是64位數(占8字節)。順便一說,這也是為什么有些人在改用x86-64時感到憤怒——x64架構中所有的指針需要的空間是原來的兩倍。
通過某種方法,只使用無類型指針也是可行的。例如標準C函數memcpy(),用于把一個區塊復制到另外一個區塊上,需要兩個void*型指針作為輸入,因為你無法預知,也無需知道要復制區塊的類型,區塊的大小才是重要的。
當函數需要一個以上的返回值時也經常用到指針(等到第九章再講)。scanf()就是這樣,函數除了要顯示成功讀入的字符個數外,還要返回全部值。
在C/C++中,指針類型只是用于在編譯階段進行類型檢查。本質上,在已編譯的代碼中并不包含指針類型的信息。
## 6.2 x86
### 6.2.1 MSVC
MVSC 2010編譯后得到下面代碼
```
#!bash
CONST SEGMENT
$SG3831 DB ’Enter X:’, 0aH, 00H
$SG3832 DB ’%d’, 00H
35
6.2\. X86 CHAPTER 6\. SCANF()
$SG3833 DB ’You entered %d...’, 0aH, 00H
CONST ENDS
PUBLIC _main
EXTRN _scanf:PROC
EXTRN _printf:PROC
; Function compile flags: /Odtp
_TEXT SEGMENT
_x$ = -4 ; size = 4
_main PROC
push ebp
mov ebp, esp
push ecx
push OFFSET $SG3831 ; ’Enter X:’
call _printf
add esp, 4
lea eax, DWORD PTR _x$[ebp]
push eax
push OFFSET $SG3832 ; ’%d’
call _scanf
add esp, 8
mov ecx, DWORD PTR _x$[ebp]
push ecx
push OFFSET $SG3833 ; ’You entered %d...’
call _printf
add esp, 8
; return 0
xor eax, eax
mov esp, ebp
pop ebp
ret 0
_main ENDP
_TEXT ENDS
```
X是局部變量。
C/C++標準告訴我們它只對函數內部可見,無法從外部訪問。習慣上,局部變量放在棧中。也可能有其他方法,但在x86中是這樣。
函數序言后下一條指令PUSH ECX目的并不是要存儲ECX的狀態(注意程序結尾沒有與之相對的POP ECX)。
事實上這條指令僅僅是在棧中分配了4字節用于存儲變量x。
變量x可以用宏 `_x$` 來訪問(等于-4),EBP寄存器指向當前棧幀。
在一個函數執行完之后,EBP將指向當前棧幀,就無法通過`EBP+offset`來訪問局部變量和函數參數了。
也可以使用ESP寄存器,但由于它經常變化所以使用不方便。所以說在函數剛開始時,EBP的值保存了此時ESP的值。
下面是一個非常典型的32位棧幀結構 ... ... EBP-8 local variable #2, marked in IDA as var_8 EBP-4 local variable #1, marked in IDA as var_4 EBP saved value of EBP EBP+4 return address EBP+8 argument#1, marked in IDA as arg_0 EBP+0xC argument#2, marked in IDA as arg_4 EBP+0x10 argument#3, marked in IDA as arg_8 ... ...
在我們的例子中,scanf()有兩個參數。
第一個參數是指向"%d"的字符串指針,第二個是變量x的地址。
首先,`lea eax, DWORD PTR _x$[ebp]` 指令將變量x的地址放入EAX寄存器。LEA作用是"取有效地址",然而之后的主要用途有所變化(b.6.2)。
可以說,LEA在這里只是把EBP的值與宏 `_x$`的值相乘,并存儲在EAX寄存器中。
`lea eax, [ebp-4]` 也是一樣。
EBP的值減去4,結果放在EAX寄存器中。接著EAX寄存器的值被壓入棧中,再調用`printf()`。
之后,`printf()`被調用。第一個參數是一個字符串指針:"`You entered %d …` "。
第二個參數是通過`mov ecx, [ebp-4]`使用的,這個指令把變量x的內容傳給ECX而不是它的地址。
然后,ECX的值放入棧中,接著最后一次調用`printf()`。
### 6.2.2 MSVC+OllyDbg
讓我們在OllyDbg中使用這個例子。首先載入程序,按F8直到進入我們的可執行文件而不是ntdll.dll。往下滾動屏幕找到main()。點擊第一條指令(PUSH EBP),按F2,再按F9,觸發main()開始處的斷點。
讓我們來跟隨到準備變量x的地址的位置。圖6.2
可以右擊寄存器窗口的EAX,再點擊"堆棧窗口中跟隨"。這個地址會在堆棧窗口中顯示。觀察,這是局部棧中的一個變量。我在圖中用紅色箭頭標出。這里是一些無用數據(0x77D478)。PUSH指令將會把這個棧元素的地址壓入棧中。然后按F8直到scanf()函數執行完。在scanf()執行時,我們要在命令行窗口中輸入,例如輸入123。

圖6.1 命令行輸出
scanf()在這里執行。圖6.3。scanf()在EAX中返回1,這意味著成功讀入了一個值。現在我們關心的那個棧元素中的值是0x7B(123)。
接下來,這個值從棧中復制到ECX寄存器中,然后傳遞給printf()。圖6.4

圖6.2 OllyDbg:計算局部變量的地址

圖6.3:OllyDbg:scanf()執行

圖6.4:OllyDbg:準備把值傳遞給printf()
### 6.2.3 GCC
讓我們在Linux GCC 4.4.1下編譯這段代碼
GCC把第一個調用的printf()替換成了puts(),原因在2.3.3節中講過了。
和之前一樣,參數都是用MOV指令放入棧中。
## 6.3 x64
和原來一樣,只是傳遞參數時不使用棧而使用寄存器。
### 6.3.1 MSVC
```
#!bash
_DATA SEGMENT
$SG1289 DB ’Enter X:’, 0aH, 00H
$SG1291 DB ’%d’, 00H
$SG1292 DB ’You entered %d...’, 0aH, 00H
_DATA ENDS
_TEXT SEGMENT
x$ = 32
main PROC
$LN3:
sub rsp, 56
lea rcx, OFFSET FLAT:$SG1289 ; ’Enter X:’
call printf
lea rdx, QWORD PTR x$[rsp]
lea rcx, OFFSET FLAT:$SG1291 ; ’%d’
call scanf
mov edx, DWORD PTR x$[rsp]
lea rcx, OFFSET FLAT:$SG1292 ; ’You entered %d...’
call printf
; return 0
xor eax, eax
add rsp, 56
ret 0
main ENDP
_TEXT ENDS
```
### 6.3.2 GCC
```
#!bash
.LC0:
.string "Enter X:"
.LC1:
.string "%d"
.LC2:
.string "You entered %d...
"
main:
sub rsp, 24
mov edi, OFFSET FLAT:.LC0 ; "Enter X:"
call puts
lea rsi, [rsp+12]
mov edi, OFFSET FLAT:.LC1 ; "%d"
xor eax, eax
call __isoc99_scanf
mov esi, DWORD PTR [rsp+12]
mov edi, OFFSET FLAT:.LC2 ; "You entered %d...
"
xor eax, eax
call printf
; return 0
xor eax, eax
add rsp, 24
ret
```
## 6.4 ARM
### 6.4.1 keil優化+thumb mode
```
#!bash
.text:00000042 scanf_main
.text:00000042
.text:00000042 var_8 = -8
.text:00000042
.text:00000042 08 B5 PUSH {R3,LR}
.text:00000044 A9 A0 ADR R0, aEnterX ; "Enter X:
"
.text:00000046 06 F0 D3 F8 BL __2printf
.text:0000004A 69 46 MOV R1, SP
.text:0000004C AA A0 ADR R0, aD ; "%d"
.text:0000004E 06 F0 CD F8 BL __0scanf
.text:00000052 00 99 LDR R1, [SP,#8+var_8]
.text:00000054 A9 A0 ADR R0, aYouEnteredD___ ; "You entered %d...
"
.text:00000056 06 F0 CB F8 BL __2printf
.text:0000005A 00 20 MOVS R0, #0
.text:0000005C 08 BD POP {R3,PC}
```
必須把一個指向int變量的指針傳遞給scanf(),這樣才能通過這個指針返回一個值。Int是一個32位的值,所以我們在內存中需要4字節存儲,并且正好符合32位的寄存器。局部變量x的空間分配在棧中,IDA把他命名為var_8。然而并不需要分配空間,因為棧指針指向的空間可以被立即使用。所以棧指針的值被復制到R1寄存器中,然后和格式化字符串一起送入scanf()。然后LDR指令將這個值從棧中送入R1寄存器,用以送入printf()中。
用ARM-mode和Xcode LLVM編譯的代碼區別不大,這里略去。
## 6.5 Global Variables
如果之前的例子中的x變量不再是本地變量而是全局變量呢?那么就有機會接觸任何指針,不僅僅是函數體,全局變量被認為anti-pattern(通常被認為是一個不好的習慣),但是為了試驗,我們可以這樣做。
```
#!cpp
#include <stdio.h>
int x;
int main()
{
printf ("Enter X:
");
scanf ("%d", &x);
printf ("You entered %d...
", x);
return 0;
};
```
### 6.5.1 MSVC: x86
```
#!bash
_DATA SEGMENT
COMM _x:DWORD
$SG2456 DB ’Enter X:’, 0aH, 00H
$SG2457 DB ’%d’, 00H
$SG2458 DB ’You entered %d...’, 0aH, 00H
_DATA ENDS
PUBLIC _main
EXTRN _scanf:PROC
EXTRN _printf:PROC
; Function compile flags: /Odtp
_TEXT SEGMENT
_main PROC
push ebp
mov ebp, esp
push OFFSET $SG2456
call _printf
add esp, 4
push OFFSET _x
push OFFSET $SG2457
call _scanf
add esp, 8
mov eax, DWORD PTR _x
push eax
push OFFSET $SG2458
call _printf
add esp, 8
xor eax, eax
pop ebp
ret 0
_main ENDP
_TEXT ENDS
```
現在x變量被定義為在_DATA部分,局部堆棧不允許再分配任何內存,除了直接訪問內存所有通過棧的訪問都不被允許。在執行的文件中全局變量還未初始化(實際上,我們為什么要在執行文件中為未初始化的變量分配一塊?)但是當訪問這里時,系統會在這里分配一塊0值。
現在讓我們明白的來分配變量吧"
```
#!bash
int x=10; // default value
```
我們得到:
```
_DATA SEGMENT
_x DD 0aH
...
```
這里我們看見一個雙字節的值0xA(DD 表示雙字節 = 32bit)
如果你在IDA中打開compiled.exe,你會發現x變量被放置在_DATA塊的開始處,接著你就會看見文本字符串。
如果你在IDA中打開之前例子中的compiled.exe中X變量沒有定義的地方,你就會看見像這樣的東西:
```
#!bash
.data:0040FA80 _x dd ? ; DATA XREF: _main+10
.data:0040FA80 ; _main+22
.data:0040FA84 dword_40FA84 dd ? ; DATA XREF: _memset+1E
.data:0040FA84 ; unknown_libname_1+28
.data:0040FA88 dword_40FA88 dd ? ; DATA XREF: ___sbh_find_block+5
.data:0040FA88 ; ___sbh_free_block+2BC
.data:0040FA8C ; LPVOID lpMem
.data:0040FA8C lpMem dd ? ; DATA XREF: ___sbh_find_block+B
.data:0040FA8C ; ___sbh_free_block+2CA
.data:0040FA90 dword_40FA90 dd ? ; DATA XREF: _V6_HeapAlloc+13
.data:0040FA90 ; __calloc_impl+72
.data:0040FA94 dword_40FA94 dd ? ; DATA XREF: ___sbh_free_block+2FE
```
被`_x`替換了?其它變量也并未要求初始化,這也就是說在載入exe至內存后,在這里有一塊針對所有變量的空間,并且還有一些隨機的垃圾數據。但在在exe中這些沒有初始化的變量并不影響什么,比如它適合大數組。
### 6.5.2 MSVC: x86 + OllyDbg
到這里事情就變得簡單了(見表6.5),變量都在data部分,順便說一句,在PUSH指令后,壓入x的地址,被執行后,地址將會在棧中顯示,那么右擊元組數據,點擊"Fllow in dump",然后變量就會在左側內存窗口顯示.
在命令行窗口中輸入123后,這里就會顯示0x7B
但是為什么第一個字節是7B?合理的猜測,這里會有一組00 00 7B,被稱為是字節順序,然后在x86中使用的是小端,也就是說低位數據先寫,高位數據后寫。
不一會,這里的32-bit值就會載入到EAX中,然后被傳遞給printf().
X變量地址是0xDC3390.在OllyDbg中我們看進程內存映射(Alt-M),然后發現這個地在PE文件.data結構處。見表6.6

表6.5 OllyDbg: scanf()執行后

表6.6: OllyDbg 進程內存映射
### 6.5.3 GCC: x86
這和linux中幾乎是一樣的,除了segment的名稱和屬性:未初始化變量被放置在_bss部分。
在ELF文件格式中,這部分數據有這樣的屬性:
```
; Segment type: Uninitialized
; Segment permissions: Read/Write
```
如果靜態的分配一個值,比如10,它將會被放在_data部分,這部分有下面的屬性:
```
; Segment type: Pure data
; Segment permissions: Read/Write
```
### 6.5.4 MSVC: x64
```
#!bash
_DATA SEGMENT
COMM x:DWORD
$SG2924 DB ’Enter X:’, 0aH, 00H
$SG2925 DB ’%d’, 00H
$SG2926 DB ’You entered %d...’, 0aH, 00H
_DATA ENDS
_TEXT SEGMENT
main PROC
$LN3:
sub rsp, 40
lea rcx, OFFSET FLAT:$SG2924 ; ’Enter X:’
call printf
lea rdx, OFFSET FLAT:x
lea rcx, OFFSET FLAT:$SG2925 ; ’%d’
call scanf
mov edx, DWORD PTR x
lea rcx, OFFSET FLAT:$SG2926 ; ’You entered %d...’
call printf
; return 0
xor eax, eax
add rsp, 40
ret 0
main ENDP
_TEXT ENDS
```
幾乎和x86中的代碼是一樣的,發現x變量的地址傳遞給scanf()用的是LEA指令,盡管第二處傳遞給printf()變量時用的是MOV指令,"DWORD PTR"——是匯編語言中的一部分(和機器碼沒有聯系)。這就表示變量數據類型是32-bit,于是MOV指令就被編碼了。
### 6.5.5 ARM:Optimizing Keil + thumb mode
```
#!bash
.text:00000000 ; Segment type: Pure code
.text:00000000 AREA .text, CODE
...
.text:00000000 main
.text:00000000 PUSH {R4,LR}
.text:00000002 ADR R0, aEnterX ; "Enter X:
"
.text:00000004 BL __2printf
.text:00000008 LDR R1, =x
.text:0000000A ADR R0, aD ; "%d"
.text:0000000C BL __0scanf
.text:00000010 LDR R0, =x
.text:00000012 LDR R1, [R0]
.text:00000014 ADR R0, aYouEnteredD___ ; "You entered %d...
"
.text:00000016 BL __2printf
.text:0000001A MOVS R0, #0
.text:0000001C POP {R4,PC}
...
.text:00000020 aEnterX DCB "Enter X:",0xA,0 ; DATA XREF: main+2
.text:0000002A DCB 0
.text:0000002B DCB 0
.text:0000002C off_2C DCD x ; DATA XREF: main+8
.text:0000002C ; main+10
.text:00000030 aD DCB "%d",0 ; DATA XREF: main+A
.text:00000033 DCB 0
.text:00000034 aYouEnteredD___ DCB "You entered %d...",0xA,0 ; DATA XREF: main+14
.text:00000047 DCB 0
.text:00000047 ; .text ends
.text:00000047
...
.data:00000048 ; Segment type: Pure data
.data:00000048 AREA .data, DATA
.data:00000048 ; ORG 0x48
.data:00000048 EXPORT x
.data:00000048 x DCD 0xA ; DATA XREF: main+8
.data:00000048 ; main+10
.data:00000048 ; .data ends
```
那么,現在x變量以某種方式變為全局的,現在被放置在另一個部分中。命名為data塊(.data)。有人可能會問,為什么文本字符串被放在了代碼塊(.text),而且x可以被放在這?因為這是變量,而且根據它的定義,它可以變化,也有可能會頻繁變化,不頻繁變化的代碼塊可以被放置在ROM中,變化的變量在RAM中,當有ROM時在RAM中儲存不變的變量是不利于節約資源的。
此外,RAM中數據部分常量必須在之前初始化,因為在RAM使用后,很明顯,將會包含雜亂的信息。
繼續向前,我們可以看到,在代碼片段,有個指針指向X變量(0ff_2C)。然后所有關于變量的操作都是通過這個指針。這也是x變量可以被放在遠離這里地方的原因。所以他的地址一定被存在離這很近的地方。LDR指令在thumb模式下只可訪問指向地址在1020bytes內的數據。同樣的指令在ARM模式下——范圍就達到了4095bytes,也就是x變量地址一定要在這附近的原因。因為沒法保證鏈接時會把這個變量放在附近。
另外,如果變量以const聲明,Keil編譯環境下則會將變量放在.constdata部分,大概從那以后,鏈接時就可以把這部分和代碼塊放在ROM里了。
## 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后,兩個方向則會合并,作為函數的返回值,然后函數結束。
- 第一章 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