逆向基礎(二)

wyzsk發表於2020-08-19
作者: reverse-engineering · 2014/05/08 17:41

from:http://yurichev.com/RE-book.html

Chapter 5 printf() 與引數處理


現在讓我們擴充套件"hello, world"(2)中的示例,將其中main()函式中printf的部分替換成這樣

#!cpp
#include <stdio.h>
int main()
{
    printf("a=%d; b=%d; c=%d", 1, 2, 3);
    return 0;
};

5.1 x86: 3個引數

5.1.1 MSVC

在我們用MSVC 2010 Express編譯後可以看到:

#!bash
$SG3830 DB ’a=%d; b=%d; c=%d’, 00H
...
        push 3
        push 2
        push 1
        push OFFSET $SG3830
        call _printf
        add esp, 16        ; 00000010H

這和之前的程式碼幾乎一樣,但是我們現在可以看到printf() 的引數被反序壓入了棧中。第一個引數被最後壓入。

另外,在32bit的環境下int型別變數佔4 bytes。

那麼,這裡有4個引數 4*4=16 —— 恰好在棧中佔據了16bytes:一個32bit字串指標,和3個int型別變數。

當函式執行完後,執行"ADD ESP, X"指令恢復棧指標暫存器(ESP 暫存器)。通常可以在這裡推斷函式引數的個數:用 X除以4。

當然,這隻涉及__cdecl函式呼叫方式。

也可以在最後一個函式呼叫後,把幾個"ADD ESP, X"指令合併成一個。

#!bash
push a1
push a2
call ...
...
push a1
call ...
...
push a1
push a2
push a3
call ...
add esp, 24

5.1.2 MSVC 與 ollyDbg

現在我們來在OllyDbg中載入這個範例。我們可以嘗試在MSVC 2012 加 /MD 引數編譯這個示例,也就是連結 MSVCR*.dll,那麼我們就可以在debugger中清楚的看到呼叫的函式。

在OllyDbg中載入程式,最開始的斷點在ntdll.dll中,接著按F9(run),然後第二個斷點在CRT-code中。現在我們來找main()函式。

往下滾動螢幕,找到下圖這段程式碼(MSVC把main()函式分配在程式碼段開始處) 見圖5.3

點選 PUSH EBP指令,按下F2(設定斷點)然後按下F9(run),透過這些操作來跳過CRT-code,因為我們現在還不必關注這部分。

按6次F8(step over)。見圖5.4 現在EIP 指向了CALL printf的指令。和其他偵錯程式一樣,OllyDbg高亮了有值改變的暫存器。所以每次你按下F8,EIP都在改變然後它看起來便是紅色的。ESP同時也在改變,因為它是指向棧的

棧中的資料又在哪?那麼看一下偵錯程式右下方的視窗:

enter image description here

圖 5.1

然後我們可以看到有三列,棧的地址,元組資料,以及一些OllyDbg的註釋,OllyDbg可以識別像printf()這樣的字串,以及後面的三個值。

右擊選中字串,然後點選”follow in dump”,然後字串就會出現在左側顯示記憶體資料的地方,這些記憶體的資料可以被編輯。我們可以修改這些字串,之後這個例子的結果就會變的不同,現在可能並不是很實用。但是作為練習卻非常好,可以體會每部分是如何工作的。

再按一次F8(step over)

然後我們就可以看到輸出

enter image description here

圖5.2 執行printf()函式

讓我們看看暫存器和棧是怎樣變化的 見圖5.5

EAX暫存器現在是0xD(13).這是正確的,printf()返回列印的字元,EIP也變了——

事實上現在指向CALL printf之後下一條指令的地址.ECX和EDX的值也改變了。顯然,printf()函式的內部機制對它們進行了使用。

很重要的一點ESP的值並沒有發生變化,棧的狀態也是!我們可以清楚地看到字串和相應的3個值還是在那裡,實際上這就是cdecl呼叫方式。被呼叫的函式並不清楚棧中引數,因為這是呼叫體的任務。

再按一下F8執行ADD ESP, 10 見圖5.6

ESP改變了,但是值還是在棧中!當然 沒有必要用0或者別的資料填充這些值。

因為在棧指標暫存器之上的資料都是無用的。

enter image description here

圖5.3 OllyDbg:main()初始處

enter image description here

圖5.4 OllyDbg:printf()執行時

enter image description here

圖5.5 Ollydbg:printf()執行後

enter image description here

圖5.6 OllyDbg ADD ESP, 10執行完後

5.1.3 GCC

現在我們將同樣的程式在linux下用GCC4.4.1編譯後放入IDA看一下:

#!bash
main            proc near

var_10          = dword ptr -10h
var_C           = dword ptr -0Ch
var_8           = dword ptr -8
var_4           = dword ptr -4

                push    ebp
                mov     ebp, esp
                and     esp, 0FFFFFFF0h
                sub     esp, 10h
                mov     eax, offset aADBDCD ; "a=%d; b=%d; c=%d"
                mov     [esp+10h+var_4], 3
                mov     [esp+10h+var_8], 2
                mov     [esp+10h+var_C], 1
                mov     [esp+10h+var_10], eax
                call    _printf
                mov     eax, 0
                leave
                retn
main            endp

MSVC與GCC編譯後程式碼的不同點只是引數入棧的方法不同,這裡GCC不用PUSH/POP而是直接對棧操作。

5.1.4 GCC與GDB

接著我們嘗試在linux中用GDB執行下這個示例程式。

-g 表示將debug資訊插入可執行檔案中

#!bash
$ gcc 1.c -g -o 1

反編譯:

#!bash
$ gdb 1
GNU gdb (GDB) 7.6.1-ubuntu
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/dennis/polygon/1...done.

表5.1 在printf()處設定斷點

#!bash
(gdb) b printf
Breakpoint 1 at 0x80482f0

Run 這裡沒有printf()函式的原始碼,所以GDB沒法顯示出原始碼,但是卻可以這樣做

#!bash
(gdb) run
Starting program: /home/dennis/polygon/1

Breakpoint 1, __printf (format=0x80484f0 "a=%d; b=%d; c=%d") at printf.c:29
29 printf.c: No such file or directory.

列印10組棧中的元組資料,左邊是棧中的地址

#!bash
(gdb) x/10w $esp
0xbffff11c: 0x0804844a 0x080484f0 0x00000001 0x00000002
0xbffff12c: 0x00000003 0x08048460 0x00000000 0x00000000
0xbffff13c: 0xb7e29905 0x00000001

最開始的是返回地址(0x0804844a),我們可以確定在這裡,於是可以反彙編這裡的程式碼

#!bash
(gdb) x/5i 0x0804844a
0x804844a <main+45>: mov $0x0,%eax
0x804844f <main+50>: leave
0x8048450 <main+51>: ret
0x8048451: xchg %ax,%ax
0x8048453: xchg %ax,%ax

兩個XCHG指令,明顯是一些垃圾資料,可以忽略 第二個(0x080484f0)是一處格式化字串

#!bash
(gdb) x/s 0x080484f0
0x80484f0: "a=%d; b=%d; c=%d"

而其他三個則是printf()函式的引數,另外的可能只是棧中的垃圾資料,但是也可能是其他函式的資料,例如它們的本地變數。這裡可以忽略。 執行 finish ,表示執行到函式結束。在這裡是執行到printf()完。

#!bash
(gdb) finish
Run till exit from #0 __printf (format=0x80484f0 "a=%d; b=%d; c=%d") at printf.c:29
main () at 1.c:6
6 return 0;
Value returned is $2 = 13

GDB顯示了printf()函式在eax中的返回值,這是列印字元的數量,就像在OllyDbg中一樣。

我們同樣看到了”return 0;” 及這在1.c檔案中第6行所代表的含義。1.c檔案就在當前目錄下,GDB就在那找到了字串。但是GDB又是怎麼知道當前執行到了哪一行?

事實上這和編譯器有關,當生成除錯資訊時,同樣也儲存了一張程式碼行號與指令地址的關係表。

檢視EAX中儲存的13:

#!bash
(gdb) info registers
eax            0xd      13
ecx            0x0      0
edx            0x0      0
ebx            0xb7fc0000       -1208221696
esp            0xbffff120       0xbffff120
ebp            0xbffff138       0xbffff138
esi            0x0      0
edi            0x0      0
eip            0x804844a        0x804844a <main+45>
...

反彙編當前的指令

#!bash
(gdb) disas
Dump of assembler code for function main:
    0x0804841d <+0>:    push    %ebp
    0x0804841e <+1>:    mov     %esp,%ebp
    0x08048420 <+3>:    and     $0xfffffff0,%esp
    0x08048423 <+6>:    sub     $0x10,%esp
    0x08048426 <+9>:    movl    $0x3,0xc(%esp)
    0x0804842e <+17>:   movl    $0x2,0x8(%esp)
    0x08048436 <+25>:   movl    $0x1,0x4(%esp)
    0x0804843e <+33>:   movl    $0x80484f0,(%esp)
    0x08048445 <+40>:   call    0x80482f0 <[email protected]>
=>  0x0804844a <+45>:   mov     $0x0,%eax
    0x0804844f <+50>:   leave
    0x08048450 <+51>:   ret
End of assembler dump.

GDB預設使用AT&T語法顯示,當然也可以轉換至intel:

#!bash
(gdb) set disassembly-flavor intel
(gdb) disas
Dump of assembler code for function main:
    0x0804841d <+0>:    push    ebp
    0x0804841e <+1>:    mov     ebp,esp
    0x08048420 <+3>:    and     esp,0xfffffff0
    0x08048423 <+6>:    sub     esp,0x10
    0x08048426 <+9>:    mov     DWORD PTR [esp+0xc],0x3
    0x0804842e <+17>:   mov     DWORD PTR [esp+0x8],0x2
    0x08048436 <+25>:   mov     DWORD PTR [esp+0x4],0x1
    0x0804843e <+33>:   mov     DWORD PTR [esp],0x80484f0
    0x08048445 <+40>:   call    0x80482f0 <[email protected]>
=>  0x0804844a <+45>:   mov     eax,0x0
    0x0804844f <+50>:   leave
    0x08048450 <+51>:   ret
End of assembler dump.

執行下一條指令,GDB顯示了結束大括號,代表著這裡是函式結束部分。

#!bash
(gdb) step
7 };

在執行完MOV EAX, 0後我們可以看到EAX就已經變為0了。

#!bash
(gdb) info registers
eax 0x0 0
ecx 0x0 0
edx 0x0 0
ebx 0xb7fc0000 -1208221696
esp 0xbffff120 0xbffff120
ebp 0xbffff138 0xbffff138
esi 0x0 0
edi 0x0 0
eip 0x804844f 0x804844f <main+50>
...

5.2 x64: 8個引數

為了看其他引數如何透過棧傳遞的,我們再次修改程式碼將引數個數增加到9個(printf()格式化字串和8個int 變數)

#!cpp
#include <stdio.h>
int main() {
        printf("a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d
", 1, 2, 3, 4, 5, 6, 7, 8);
        return 0;
};

5.2.1 MSVC

正如我們之前所見,在win64下開始的4個引數傳遞至RCX,RDX,R8,R9暫存器,

然而 MOV指令,替代PUSH指令。用來準備棧資料,所以值都是直接寫入棧中

#!bash
$SG2923 DB ’a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d’, 0aH, 00H

main    PROC
        sub     rsp, 88

        mov     DWORD PTR [rsp+64], 8
        mov     DWORD PTR [rsp+56], 7
        mov     DWORD PTR [rsp+48], 6
        mov     DWORD PTR [rsp+40], 5
        mov     DWORD PTR [rsp+32], 4
        mov     r9d, 3
        mov     r8d, 2
        mov     edx, 1
        lea     rcx, OFFSET FLAT:$SG2923
        call    printf

        ; return 0
        xor eax, eax

        add     rsp, 88
        ret     0
main ENDP
_TEXT ENDS
END

表5.2:msvc 2010 x64

5.2.2 GCC

在*NIX系統,對於x86-64這也是同樣的原理,除了前6個引數傳遞給了RDI,RSI,RDX,RCX,R8,R9暫存器。GCC將生成的程式碼字元指標寫入了EDI而不是RDI(如果有的話)——我們在2.2.2節看到過這部分

同樣我們也看到在暫存器EAX被清零前有個printf() call:

.LC0:
    .string "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d
"

main:
    sub     rsp, 40

    mov     r9d, 5
    mov     r8d, 4
    mov     ecx, 3
    mov     edx, 2
    mov     esi, 1
    mov     edi, OFFSET FLAT:.LC0
    xor     eax, eax ; number of vector registers passed
    mov     DWORD PTR [rsp+16], 8
    mov     DWORD PTR [rsp+8], 7
    mov     DWORD PTR [rsp], 6
    call    printf

    ; return 0

    xor     eax, eax
    add     rsp, 40
    ret

表5.3:GCC 4.4.6 –o 3 x64

5.2.3 GCC + GDB

讓我們在GDB中嘗試這個例子。

#!bash
$ gcc -g 2.c -o 2

反編譯:

#!bash
$ gdb 2
GNU gdb (GDB) 7.6.1-ubuntu
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/dennis/polygon/2...done.

表5.4:在printf()處下斷點,然後run

(gdb) b printf
Breakpoint 1 at 0x400410
(gdb) run
Starting program: /home/dennis/polygon/2
Breakpoint 1, __printf (format=0x400628 "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d
") at
printf.c:29
29 printf.c: No such file or directory.

暫存器RSI/RDX/RCX/R8/R9都有應有的值,RIP則是printf()函式地址

#!bash
(gdb) info registers
rax     0x0     0
rbx     0x0     0
rcx     0x3     3
rdx     0x2     2
rsi     0x1     1
rdi     0x400628 4195880
rbp     0x7fffffffdf60 0x7fffffffdf60
rsp     0x7fffffffdf38 0x7fffffffdf38
r8      0x4     4
r9      0x5     5
r10     0x7fffffffdce0 140737488346336
r11     0x7ffff7a65f60 140737348263776
r12     0x400440 4195392
r13     0x7fffffffe040 140737488347200
r14     0x0     0
r15     0x0     0
rip     0x7ffff7a65f60 0x7ffff7a65f60 <__printf>
...

表5.5 檢查格式化字串

#!bash
(gdb) x/s $rdi
0x400628: "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d
"

用 x/g命令顯示棧內容

#!bash
(gdb) x/10g $rsp
0x7fffffffdf38: 0x0000000000400576 0x0000000000000006
0x7fffffffdf48: 0x0000000000000007 0x00007fff00000008
0x7fffffffdf58: 0x0000000000000000 0x0000000000000000
0x7fffffffdf68: 0x00007ffff7a33de5 0x0000000000000000
0x7fffffffdf78: 0x00007fffffffe048 0x0000000100000000

與之前一樣,第一個棧元素是返回地址,我們也同時也看到在高32位的8也沒有被清除。 0x00007fff00000008,這是因為是32位int型別的,因此,高暫存器或堆疊部分可能包含一些隨機垃圾數值。

printf()函式執行之後將返回控制,GDB會顯示整個main()函式。

#!bash
(gdb) set disassembly-flavor intel
(gdb) disas 0x0000000000400576
Dump of assembler code for function main:
    0x000000000040052d <+0>:    push    rbp
    0x000000000040052e <+1>:    mov     rbp,rsp
    0x0000000000400531 <+4>:    sub     rsp,0x20
    0x0000000000400535 <+8>:    mov     DWORD PTR [rsp+0x10],0x8
    0x000000000040053d <+16>:   mov     DWORD PTR [rsp+0x8],0x7
    0x0000000000400545 <+24>:   mov     DWORD PTR [rsp],0x6
    0x000000000040054c <+31>:   mov     r9d,0x5
    0x0000000000400552 <+37>:   mov     r8d,0x4
    0x0000000000400558 <+43>:   mov     ecx,0x3
    0x000000000040055d <+48>:   mov     edx,0x2
    0x0000000000400562 <+53>:   mov     esi,0x1
    0x0000000000400567 <+58>:   mov     edi,0x400628
    0x000000000040056c <+63>:   mov     eax,0x0
    0x0000000000400571 <+68>:   call    0x400410 <[email protected]>
    0x0000000000400576 <+73>:   mov     eax,0x0
    0x000000000040057b <+78>:   leave
    0x000000000040057c <+79>:   ret
End of assembler dump.

執行完printf()後,就會清零EAX,然後發現EAX早已為0,RIP現在則指向LEAVE指令。

#!bash
(gdb) finish
Run till exit from #0 __printf (format=0x400628 "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d
n") at printf.c:29
a=1; b=2; c=3; d=4; e=5; f=6; g=7; h=8
main () at 2.c:6
6       return 0;
Value returned is $1 = 39
(gdb) next
7 };
(gdb) info registers
rax     0x0     0
rbx     0x0     0
rcx     0x26    38
rdx     0x7ffff7dd59f0 140737351866864
rsi     0x7fffffd9 2147483609
rdi     0x0     0
rbp     0x7fffffffdf60 0x7fffffffdf60
rsp     0x7fffffffdf40 0x7fffffffdf40
r8      0x7ffff7dd26a0 140737351853728
r9      0x7ffff7a60134 140737348239668
r10     0x7fffffffd5b0 140737488344496
r11     0x7ffff7a95900 140737348458752
r12     0x400440 4195392
r13     0x7fffffffe040 140737488347200
r14     0x0     0
r15     0x0     0
rip     0x40057b 0x40057b <main+78>
...

5.3 ARM:3個引數

習慣上,ARM傳遞引數的規則(引數呼叫)如下:前4個引數傳遞給了R0-R3暫存器,其餘的引數則在棧中。這和fastcall或者win64傳遞引數很相似

5.3.1 Non-optimizing Keil + ARM mode(非最佳化keil編譯模式 + ARM環境)

#!bash
.text:00000014            printf_main1
.text:00000014 10 40 2D E9       STMFD   SP!, {R4,LR}
.text:00000018 03 30 A0 E3       MOV     R3, #3
.text:0000001C 02 20 A0 E3       MOV     R2, #2
.text:00000020 01 10 A0 E3       MOV     R1, #1
.text:00000024 1D 0E 8F E2       ADR     R0, aADBDCD ; "a=%d; b=%d; c=%d
"
.text:00000028 0D 19 00 EB       BL      __2printf
.text:0000002C 10 80 BD E8       LDMFD   SP!, {R4,PC}

所以 前四個引數按照它們的順序傳遞給了R0-R3, printf()中的格式化字串指標在R0中,然後1在R1,2在R2,3在R3. 到目前為止沒有什麼不尋常的。

5.3.2 Optimizing Keil + ARM mode(最佳化的keil編譯模式 + ARM環境)

#!bash
.text:00000014     EXPORT printf_main1
.text:00000014     printf_main1
.text:00000014 03 30 A0 E3     MOV    R3, #3
.text:00000018 02 20 A0 E3     MOV    R2, #2
.text:0000001C 01 10 A0 E3     MOV    R1, #1
.text:00000020 1E 0E 8F E2     ADR    R0, aADBDCD ; "a=%d; b=%d; c=%d
"
.text:00000024 CB 18 00 EA     B     __2printf

表5.7: Optimizing Keil + ARM mode

這是在針對ARM optimized (-O3)版本下的,我們可以B作為最後一個指令而不是熟悉的BL。另外一個不同之處在optimized與之前的(compiled without optimization)對比發現函式prologue 和 epilogue(儲存R0和LR值的暫存器),B指令僅僅跳向另一處地址,沒有任何關於LR暫存器的操作,也就是說它和x86中的jmp相似,為什麼會這樣?因為程式碼就是這樣,事實上,這和前面相似,主要有兩點原因 1)不管是棧還是SP(棧指標),都有被修改。2)printf()的呼叫是最後的指令,所以之後便沒有了。完成之後,printf()函式就返回到LR儲存的地址處。但是指標地址從函式呼叫的地方轉移到了LR中!接著就會從printf()到那裡。結果,我們不需要儲存LR,因為我們沒有必要修改LR。因為除了printf()函式外沒有其他函式了。另外,除了這個呼叫外,我們不需要再做別的。這就是為什麼這樣編譯是可行的。

5.3.3 Optimizing Keil + thumb mode

#!bash
.text:0000000C     printf_main1
.text:0000000C 10 B5           PUSH {R4,LR}
.text:0000000E 03 23           MOVS R3, #3
.text:00000010 02 22           MOVS R2, #2
.text:00000012 01 21           MOVS R1, #1
.text:00000014 A4 A0           ADR R0, aADBDCD ; "a=%d; b=%d; c=%d
"
.text:00000016 06 F0 EB F8     BL __2printf
.text:0000001A 10 BD           POP {R4,PC}

表5.8:Optimizing Keil + thumb mode

和non-optimized for ARM mode程式碼沒什麼明顯的區別

5.4 ARM: 8 arguments

我們再用之前9個引數的那個例子

#!bash
void printf_main2()
{
    printf("a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d
", 1, 2, 3, 4, 5, 6, 7, 8);
};

5.4.1 Optimizing Keil: ARM mode

#!bash
.text:00000028      printf_main2
.text:00000028
.text:00000028      var_18 = -0x18
.text:00000028      var_14 = -0x14
.text:00000028      var_4 = -4
.text:00000028
.text:00000028 04 E0 2D E5      STR     LR, [SP,#var_4]!
.text:0000002C 14 D0 4D E2      SUB     SP, SP, #0x14
.text:00000030 08 30 A0 E3      MOV     R3, #8
.text:00000034 07 20 A0 E3      MOV     R2, #7
.text:00000038 06 10 A0 E3      MOV     R1, #6
.text:0000003C 05 00 A0 E3      MOV     R0, #5
.text:00000040 04 C0 8D E2      ADD     R12, SP, #0x18+var_14
.text:00000044 0F 00 8C E8      STMIA   R12, {R0-R3}
.text:00000048 04 00 A0 E3      MOV     R0, #4
.text:0000004C 00 00 8D E5      STR     R0, [SP,#0x18+var_18]
.text:00000050 03 30 A0 E3      MOV     R3, #3
.text:00000054 02 20 A0 E3      MOV     R2, #2
.text:00000058 01 10 A0 E3      MOV     R1, #1
.text:0000005C 6E 0F 8F E2      ADR     R0, aADBDCDDDEDFDGD ; "a=%d; b=%d; c=%d; d=%d;
e=%d; f=%d; g=%"...
.text:00000060 BC 18 00 EB      BL      __2printf
.text:00000064 14 D0 8D E2      ADD     SP, SP, #0x14
.text:00000068 04 F0 9D E4      LDR     PC, [SP+4+var_4],#4

這些程式碼可以分成幾個部分:

Function prologue:

最開始的”STR LR, [SP,#var_4]!”指令將LR儲存在棧中,因為我們將用這個暫存器呼叫printf()。

第二個” SUB SP, SP, #0x14”指令減了SP(棧指標),為了在棧上分配0x14(20)bytes的記憶體,實際上我們需要傳遞5個 32-bit的資料透過棧傳遞給printf()函式,而且每個佔4bytes,也就是5*4=20。另外4個32-bit的資料將會傳遞給暫存器。

透過棧傳遞5,6,7和8:

然後,5,6,7,8分別被寫入了R0,R1,R2及R3暫存器。然後”ADD R12, SP,#0x18+var_14”指令將棧中指標的地址寫入,並且在這裡會向R12寫入4個值,var_14是一個彙編宏,相當於0x14,這些都由IDA簡明的建立表示訪問棧的變數,var_?在IDA中表示棧中的本地變數,所以SP+4將被寫入R12暫存器。下一步的” STMIA R12, R0-R3”指令將R0-R3暫存器的內容寫在了R2指向的指標處。STMIA指令指Store Multiple Increment After, Increment After指R12暫存器在有值寫入後自增4。

透過棧傳遞4:

4存在R0中,然後這個值在” STR R0, [SP,#0x18+var_18]”指令幫助下,存在了棧上,var_18是0x18,偏移量為0.所以R0暫存器中的值將會寫在SP指標指向的指標處。

透過暫存器傳遞1,2,3:

開始3個數(a,b,c)(分別是1,2,3)正好在printf()函式呼叫前被傳遞到了R1,R2,R3暫存器中。 然後另外5個值透過棧傳遞。

printf() 呼叫

Function epilogue:

ADD SP, SP, #0x14”指令將SP指標返回到之前的指標處,因此清除了棧,當然,棧中之前寫入的資料還在那,但是當後來的函式被呼叫時那裡則會被重寫。 “LDR PC, [SP+4+var_4],#4"指令將LR中儲存的值載入到PC指標,因此函式結束。

5.4.2 Optimizing Keil: thumb mode

#!bash
.text:0000001C      printf_main2
.text:0000001C
.text:0000001C      var_18 = -0x18
.text:0000001C      var_14 = -0x14
.text:0000001C      var_8 = -8
.text:0000001C
.text:0000001C 00 B5        PUSH    {LR}
.text:0000001E 08 23        MOVS    R3, #8
.text:00000020 85 B0        SUB     SP, SP, #0x14
.text:00000022 04 93        STR     R3, [SP,#0x18+var_8]
.text:00000024 07 22        MOVS    R2, #7
.text:00000026 06 21        MOVS    R1, #6
.text:00000028 05 20        MOVS    R0, #5
.text:0000002A 01 AB        ADD     R3, SP, #0x18+var_14
.text:0000002C 07 C3        STMIA   R3!, {R0-R2}
.text:0000002E 04 20        MOVS    R0, #4
.text:00000030 00 90        STR     R0, [SP,#0x18+var_18]
.text:00000032 03 23        MOVS    R3, #3
.text:00000034 02 22        MOVS    R2, #2
.text:00000036 01 21        MOVS    R1, #1
.text:00000038 A0 A0        ADR     R0, aADBDCDDDEDFDGD ; "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%"...
.text:0000003A 06 F0 D9 F8  BL __2printf
.text:0000003E
.text:0000003E              loc_3E ; CODE XREF: example13_f+16
.text:0000003E 05 B0        ADD SP, SP, #0x14
.text:00000040 00 BD        POP {PC}

幾乎和之前的例子是一樣的,然後這是thumb 程式碼,值入棧的確不同:先是8,然後5,6,7,第三個是4。

5.4.3 Optimizing Xcode (LLVM): ARM mode

#!bash
__text:0000290C     _printf_main2
__text:0000290C
__text:0000290C     var_1C = -0x1C
__text:0000290C     var_C = -0xC
__text:0000290C
__text:0000290C 80 40 2D E9     STMFD   SP!, {R7,LR}
__text:00002910 0D 70 A0 E1     MOV     R7, SP
__text:00002914 14 D0 4D E2     SUB     SP, SP, #0x14
__text:00002918 70 05 01 E3     MOV     R0, #0x1570
__text:0000291C 07 C0 A0 E3     MOV     R12, #7
__text:00002920 00 00 40 E3     MOVT    R0, #0
__text:00002924 04 20 A0 E3     MOV     R2, #4
__text:00002928 00 00 8F E0     ADD     R0, PC, R0
__text:0000292C 06 30 A0 E3     MOV     R3, #6
__text:00002930 05 10 A0 E3     MOV     R1, #5
__text:00002934 00 20 8D E5     STR     R2, [SP,#0x1C+var_1C]
__text:00002938 0A 10 8D E9     STMFA   SP, {R1,R3,R12}
__text:0000293C 08 90 A0 E3     MOV     R9, #8
__text:00002940 01 10 A0 E3     MOV     R1, #1
__text:00002944 02 20 A0 E3     MOV     R2, #2
__text:00002948 03 30 A0 E3     MOV     R3, #3
__text:0000294C 10 90 8D E5     STR     R9, [SP,#0x1C+var_C]
__text:00002950 A4 05 00 EB     BL      _printf
__text:00002954 07 D0 A0 E1     MOV     SP, R7
__text:00002958 80 80 BD E8     LDMFD   SP!, {R7,PC}

幾乎和我們之前遇到的一樣,除了STMFA(Store Multiple Full Ascending)指令,它和STMIB(Store Multiple Increment Before)指令一樣,這個指令直到下個暫存器的值寫入記憶體時會增加SP暫存器中的值,但是反過來卻不同。

另外一個地方我們可以輕鬆的發現指令是隨機分佈的,例如,R0暫存器中的值在三個地方初始,在0x2918,0x2920,0x2928。而這一個指令就可以搞定。然而,optimizing compiler有它自己的原因,對於如何更好的放置指令,通常,處理器嘗試同時執行並行的指令,例如像” MOVT R0, #0”和” ADD R0, PC,R0”就不能同時執行了,因為它們同時都在修改R0暫存器,另一方面”MOVT R0, #0”和”MOV R2, #4”指令卻可以同時執行,因為執行效果並沒有任何衝突。 大概,編譯器就是這樣嘗試編譯的,可能。

5.4.4 Optimizing Xcode (LLVM): thumb-2 mode

#!bash
__text:00002BA0     _printf_main2
__text:00002BA0
__text:00002BA0     var_1C = -0x1C
__text:00002BA0     var_18 = -0x18
__text:00002BA0     var_C = -0xC
__text:00002BA0
__text:00002BA0 80 B5           PUSH    {R7,LR}
__text:00002BA2 6F 46           MOV     R7, SP
__text:00002BA4 85 B0           SUB     SP, SP, #0x14
__text:00002BA6 41 F2 D8 20     MOVW    R0, #0x12D8
__text:00002BAA 4F F0 07 0C     MOV.W   R12, #7
__text:00002BAE C0 F2 00 00     MOVT.W  R0, #0
__text:00002BB2 04 22           MOVS    R2, #4
__text:00002BB4 78 44           ADD     R0, PC ; char *
__text:00002BB6 06 23           MOVS    R3, #6
__text:00002BB8 05 21           MOVS    R1, #5
__text:00002BBA 0D F1 04 0E     ADD.W   LR, SP, #0x1C+var_18
__text:00002BBE 00 92           STR     R2, [SP,#0x1C+var_1C]
__text:00002BC0 4F F0 08 09     MOV.W   R9, #8
__text:00002BC4 8E E8 0A 10     STMIA.W LR, {R1,R3,R12}
__text:00002BC8 01 21           MOVS    R1, #1
__text:00002BCA 02 22           MOVS    R2, #2
__text:00002BCC 03 23           MOVS    R3, #3
__text:00002BCE CD F8 10 90     STR.W   R9, [SP,#0x1C+var_C]
__text:00002BD2 01 F0 0A EA     BLX     _printf
__text:00002BD6 05 B0           ADD     SP, SP, #0x14
__text:00002BD8 80 BD           POP     {R7,PC}

幾乎和前面的例子相同,除了thumb-instructions在這裡被替代使用了

5.5 by the way

值得一提的是,這些x86,x64,fastcall和ARM傳遞引數的不同表現了CPU並不在意函式引數是怎樣傳遞的,同樣也假想編譯器可能用特殊的結構傳送引數而一點也不是透過棧。

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。

enter image description here

圖6.1 命令列輸出

scanf()在這裡執行。圖6.3。scanf()在EAX中返回1,這意味著成功讀入了一個值。現在我們關心的那個棧元素中的值是0x7B(123)。

接下來,這個值從棧中複製到ECX暫存器中,然後傳遞給printf()。圖6.4

enter image description here

圖6.2 OllyDbg:計算區域性變數的地址

enter image description here

圖6.3:OllyDbg:scanf()執行

enter image description here

圖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

enter image description here

表6.5 OllyDbg: scanf()執行後

enter image description here

表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 [email protected]
        mov     ecx, DWORD PTR _x$[ebp]
        push    ecx
        push    OFFSET $SG3834 ; ’You entered %d...’, 0aH, 00H
        call    _printf
        add     esp, 8
        jmp     SHORT [email protected]
[email protected]:
        push    OFFSET $SG3836 ; ’What you entered? Huh?’, 0aH, 00H
        call    _printf
        add     esp, 4
[email protected]:
        xor     eax, eax

呼叫函式(main())必須能夠訪問到被呼叫函式(scanf())的結果,所以callee把這個值留在了EAX暫存器中。

然後我們在"CMP EAX, 1"指令的幫助下,換句話說,我們將eax中的值與1進行比較。

JNE根據CMP的結果判斷跳至哪,JNE表示(jump if Not Equal)

所以,如果EAX中的值不等於1,那麼處理器就會將執行流程跳轉到JNE指向的,[email protected],當流程跳到這裡時,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):

這個非常的有用。可以這麼說,逆向工程師很重要的一點就是縮小他所有的資訊。

enter image description here

圖6.7: IDA 圖形模式

enter image description here

圖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我們可以在視窗中看到:

enter image description here

圖6.9

實際上,5035128是棧上一個資料(0x4CD478)的十進位制表示!

enter image description here

圖6.10

enter image description here

圖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),這樣就會無條件跳轉,不管我們輸入什麼,錯誤資訊都可以列印出來了。

enter image description here

圖6.12:main()函式

enter image description here

圖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 [email protected]
            mov         edx, DWORD PTR x$[rsp]
            lea         rcx, OFFSET FLAT:$SG2927 ; ’You entered %d...’
            call        printf
            jmp         SHORT [email protected]
[email protected]:
            lea rcx, OFFSET FLAT:$SG2929 ; ’What you entered? Huh?’
            call printf
[email protected]:
            ; 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後,兩個方向則會合並,作為函式的返回值,然後函式結束。

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章