<span id="7ztzv"></span>
<sub id="7ztzv"></sub>

<span id="7ztzv"></span><form id="7ztzv"></form>

<span id="7ztzv"></span>

        <address id="7ztzv"></address>

            原文地址:http://drops.wooyun.org/tips/1931

            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后,兩個方向則會合并,作為函數的返回值,然后函數結束。

            <span id="7ztzv"></span>
            <sub id="7ztzv"></sub>

            <span id="7ztzv"></span><form id="7ztzv"></form>

            <span id="7ztzv"></span>

                  <address id="7ztzv"></address>

                      亚洲欧美在线