逆向基礎(三)
CHAPER7
訪問傳遞引數
現在我們來看函式呼叫者透過棧把引數傳遞到被呼叫函式。被呼叫函式是如何訪問這些引數呢?
#!cpp
#include <stdio.h>
int f (int a, int b, int c)
{
return a*b+c;
};
int main()
{
printf ("%d
", f(1, 2, 3));
return 0;
};
7.1 X86
7.1.1 MSVC
如下為相應的反彙編程式碼(MSVC 2010 Express)
Listing 7.2 MSVC 2010 Express
#!bash
_TEXT SEGMENT
_a$ = 8 ; size = 4
_b$ = 12 ; size = 4
_c$ = 16 ; size = 4
_f PROC
push ebp
mov ebp, esp
mov eax, DWORD PTR _a$[ebp]
imul eax, DWORD PTR _b$[ebp]
add eax, DWORD PTR _c$[ebp]
pop ebp
ret 0
_f ENDP
_main PROC
push ebp
mov ebp, esp
push 3 ; 3rd argument
push 2 ; 2nd argument
push 1 ; 1st argument
call _f
add esp, 12
push eax
push OFFSET $SG2463 ; ’%d’, 0aH, 00H
call _printf
add esp, 8
; return 0
xor eax, eax
pop ebp
ret 0
_main ENDP
我們可以看到函式main()中3個數字被圧棧,然後函式f(int, int, int)被呼叫。函式f()內部訪問引數時使用了像_ a$=8 的宏,同樣,在函式內部訪問區域性變數也使用了類似的形式,不同的是訪問引數時偏移值(為正值)。因此EBP暫存器的值加上宏_a$的值指向壓棧引數。
_a$[ebp]
的值被儲存在暫存器eax中,IMUL指令執行後,eax的值為eax與_b$[ebp]
的乘積,然後eax與_c$[ebp]
的值相加並將和放入eax暫存器中,之後返回eax的值。返回值作為printf()的引數。
7.1.2 MSVC+OllyDbg
我們在OllyDbg中觀察,跟蹤到函式f()使用第一個引數的位置,可以看到暫存器EBP指向棧底,圖中使用紅色箭頭標識。棧幀中第一個被儲存的是EBP的值,第二個是返回地址(RA),第三個是引數1,接下來是引數2,以此類推。因此,當我們訪問第一個引數時EBP應該加8(2個32-bit位元組寬度)。
Figure 7.1: OllyDbg: 函式f()內部
7.1.3 GCC
使用GCC4.4.1編譯後在IDA中檢視
Listing 7.3: GCC 4.4.1
#!bash
public f
f proc near
arg_0 = dword ptr 8
arg_4 = dword ptr 0Ch
arg_8 = dword ptr 10h
push ebp
mov ebp, esp
mov eax, [ebp+arg_0] ; 1st argument
imul eax, [ebp+arg_4] ; 2nd argument
add eax, [ebp+arg_8] ; 3rd argument
pop ebp
retn
f endp
public main
main proc near
var_10 = dword ptr -10h
var_C = dword ptr -0Ch
var_8 = dword ptr -8
push ebp
mov ebp, esp
and esp, 0FFFFFFF0h
sub esp, 10h
mov [esp+10h+var_8], 3 ; 3rd argument
mov [esp+10h+var_C], 2 ; 2nd argument
mov [esp+10h+var_10], 1 ; 1st argument
call f
mov edx, offset aD ; "%d
"
mov [esp+10h+var_C], eax
mov [esp+10h+var_10], edx
call _printf
mov eax, 0
leave
retn
main endp
幾乎相同的結果。
執行兩個函式後棧指標ESP並沒有顯示恢復,因為倒數第二個指令LEAVE(B.6.2)會自動恢復棧指標。
7.2 X64
x86-64架構下有點不同,函式引數(4或6)使用暫存器傳遞,被呼叫函式透過訪問暫存器來訪問傳遞進來的引數。
7.2.1 MSVC
MSVC最佳化後:
Listing 7.4: MSVC 2012 /Ox x64
#!bash
$SG2997 DB ’%d’, 0aH, 00H
main PROC
sub rsp, 40
mov edx, 2
lea r8d, QWORD PTR [rdx+1] ; R8D=3
lea ecx, QWORD PTR [rdx-1] ; ECX=1
call f
lea rcx, OFFSET FLAT:$SG2997 ; ’%d’
mov edx, eax
call printf
xor eax, eax
add rsp, 40
ret 0
main ENDP
f PROC
; ECX - 1st argument
; EDX - 2nd argument
; R8D - 3rd argument
imul ecx, edx
lea eax, DWORD PTR [r8+rcx]
ret 0
f ENDP
我們可以看到函式f()直接使用暫存器來操作引數,LEA指令用來做加法,編譯器認為使用LEA比使用ADD指令要更快。在mian()中也使用了LEA指令,編譯器認為使用LEA比使用MOV指令效率更高。
我們來看看MSVC沒有最佳化的情況:
Listing 7.5: MSVC 2012 x64
#!bash
f proc near
; shadow space:
arg_0 = dword ptr 8
arg_8 = dword ptr 10h
arg_10 = dword ptr 18h
; ECX - 1st argument
; EDX - 2nd argument
; R8D - 3rd argument
mov [rsp+arg_10], r8d
mov [rsp+arg_8], edx
mov [rsp+arg_0], ecx
mov eax, [rsp+arg_0]
imul eax, [rsp+arg_8]
add eax, [rsp+arg_10]
retn
f endp
main proc near
sub rsp, 28h
mov r8d, 3 ; 3rd argument
mov edx, 2 ; 2nd argument
mov ecx, 1 ; 1st argument
call f
mov edx, eax
lea rcx, $SG2931 ; "%d
"
call printf
; return 0
xor eax, eax
add rsp, 28h
retn
main endp
這裡從暫存器傳遞進來的3個引數因為某種情況又被儲存到棧裡。這就是所謂的“shadow space”2:每個Win64通常(不是必需)會儲存所有4個暫存器的值。這樣做由兩個原因:1)為輸入引數分配所有暫存器(即使是4個)太浪費,所以要透過堆疊來訪問;2)每次中斷下來偵錯程式總是能夠定位函式引數3。
呼叫者負責在棧中分配“shadow space”。
7.2.2 GCC
GCC最佳化後的程式碼:
Listing 7.6: GCC 4.4.6 -O3 x64
#!bash
f:
; EDI - 1st argument
; ESI - 2nd argument
; EDX - 3rd argument
imul esi, edi
lea eax, [rdx+rsi]
ret
main:
sub rsp, 8
mov edx, 3
mov esi, 2
mov edi, 1
call f
mov edi, OFFSET FLAT:.LC0 ; "%d
"
mov esi, eax
xor eax, eax ; number of vector registers passed
call printf
xor eax, eax
add rsp, 8
ret
GCC無最佳化程式碼:
Listing 7.7: GCC 4.4.6 x64
#!bash
f:
; EDI - 1st argument
; ESI - 2nd argument
; EDX - 3rd argument
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov DWORD PTR [rbp-8], esi
mov DWORD PTR [rbp-12], edx
mov eax, DWORD PTR [rbp-4]
imul eax, DWORD PTR [rbp-8]
add eax, DWORD PTR [rbp-12]
leave
ret
main:
push rbp
mov rbp, rsp
mov edx, 3
mov esi, 2
mov edi, 1
call f
mov edx, eax
mov eax, OFFSET FLAT:.LC0 ; "%d
"
mov esi, edx
mov rdi, rax
mov eax, 0 ; number of vector registers passed
call printf
mov eax, 0
leave
ret
System V *NIX [21]沒有“shadow space”,但被呼叫者可能會儲存引數,這也是造成暫存器短缺的原因。
7.2.3 GCC: uint64_t instead int
我們例子使用的是32位int,暫存器也為32位暫存器(字首為E-)。
為處理64位數值內部會自動調整為64位暫存器:
#!cpp
#include <stdio.h>
#include <stdint.h>
uint64_t f (uint64_t a, uint64_t b, uint64_t c)
{
return a*b+c;
};
int main()
{
printf ("%lld
", f(0x1122334455667788,0x1111111122222222,0x3333333344444444));
return 0;
};
Listing 7.8: GCC 4.4.6 -O3 x64
#!cpp
f proc near
imul rsi, rdi
lea rax, [rdx+rsi]
retn
f endp
main proc near
sub rsp, 8
mov rdx, 3333333344444444h ; 3rd argument
mov rsi, 1111111122222222h ; 2nd argument
mov rdi, 1122334455667788h ; 1st argument
call f
mov edi, offset format ; "%lld
"
mov rsi, rax
xor eax, eax ; number of vector registers passed
call _printf
xor eax, eax
add rsp, 8
retn
main endp
程式碼非常相似,只是使用了64位暫存器(字首為R)。
7.3 ARM
7.3.1 未最佳化的Keil + ARM mode
#!bash
.text:000000A4 00 30 A0 E1 MOV R3, R0
.text:000000A8 93 21 20 E0 MLA R0, R3, R1, R2
.text:000000AC 1E FF 2F E1 BX LR
...
.text:000000B0 main
.text:000000B0 10 40 2D E9 STMFD SP!, {R4,LR}
.text:000000B4 03 20 A0 E3 MOV R2, #3
.text:000000B8 02 10 A0 E3 MOV R1, #2
.text:000000BC 01 00 A0 E3 MOV R0, #1
.text:000000C0 F7 FF FF EB BL f
.text:000000C4 00 40 A0 E1 MOV R4, R0
.text:000000C8 04 10 A0 E1 MOV R1, R4
.text:000000CC 5A 0F 8F E2 ADR R0, aD_0 ; "%d
"
.text:000000D0 E3 18 00 EB BL __2printf
.text:000000D4 00 00 A0 E3 MOV R0, #0
.text:000000D8 10 80 BD E8 LDMFD SP!, {R4,PC}
main()函式里呼叫了另外兩個函式,3個值被傳遞到f();
正如前面提到的,ARM通常使用前四個暫存器(R0-R4)傳遞前四個值。
f()函式使用了前三個暫存器(R0-R2)作為引數。
MLA (Multiply Accumulate)指令將R3暫存器和R1暫存器的值相乘,然後再將乘積與R2暫存器的值相加將結果存入R0,函式返回R0。
一條指令完成乘法和加法4,如果不包括SIMD新的FMA指令5,通常x86下沒有這樣的指令。
第一條指令MOV R3,R0,看起來冗餘是因為該程式碼是非最佳化的。
BX指令返回到LR暫存器儲存的地址,處理器根據狀態模式從Thumb狀態轉換到ARM狀態,或者反之。函式f()可以被ARM程式碼或者Thumb程式碼呼叫,如果是Thumb程式碼呼叫BX將返回到呼叫函式並切換到Thumb模式,或者反之。
7.3.2 Optimizing Keil + ARM mode
#!bash
.text:00000098 f
.text:00000098 91 20 20 E0 MLA R0, R1, R0, R2
.text:0000009C 1E FF 2F E1 BX LR
這裡f()編譯時使用完全最佳化模式(-O3),MOV指令被最佳化,現在MLA使用所有輸入暫存器並將結果置入R0暫存器。
7.3.3 Optimizing Keil + thumb mode
#!bash
.text:0000005E 48 43 MULS R0, R1
.text:00000060 80 18 ADDS R0, R0, R2
.text:00000062 70 47 BX LR
Thumb模式下沒有MLA指令,編譯器做了兩次間接處理,MULS指令使R0暫存器的值與R1暫存器的值相乘並將結果存入R0。ADDS指令將R0與R2的值相加並將結果存入R0。
Chapter 8
一個或者多個字的返回值
X86架構下通常返回EAX暫存器的值,如果是單位元組char,則只使用EAX的低8位AL。如果返回float型別則使用FPU暫存器ST(0)。ARM架構下通常返回暫存器R0。
假如main()函式的返回值是void而不是int會怎麼樣?
通常啟動函式呼叫main()為:
#!bash
push envp
push argv
push argc
call main
push eax
call exit
換句話說為
#!cpp
exit(main(argc,argv,envp));
如果main()宣告為void型別並且函式沒有明確返回狀態值,通常在main()結束時EAX暫存器的值被返回,然後作為exit()的引數。大多數情況下函式返回的是隨機值。這種情況下程式的退出程式碼為偽隨機的。
我們看一個例項,注意main()是void型別:
#!cpp
#include <stdio.h>
void main()
{
printf ("Hello, world!
");
};
我們在linux下編譯。
GCC 4.8.1會使用puts()替代printf()(看前面章節2.3.3),沒有關係,因為puts()會返回列印的字元數,就行printf()一樣。請注意,main()結束時EAX暫存器的值是非0的,這意味著main()結束時保留puts()返回時EAX的值。
Listing 8.1: GCC 4.8.1
#!bash
.LC0:
.string "Hello, world!"
main:
push ebp
mov ebp, esp
and esp, -16
sub esp, 16
mov DWORD PTR [esp], OFFSET FLAT:.LC0
call puts
leave
ret
我們寫bash指令碼來看退出狀態:
Listing 8.2: tst.sh
#!bash
#!/bin/sh
./hello_world
echo $?
執行:
#!bash
$ tst.sh
Hello, world!
14
14為列印的字元數。
回到返回值是EAX暫存器值的事實,這也就是為什麼老的C編譯器不能夠建立返回資訊無法擬合到一個暫存器(通常是int型)的函式。如果必須這樣,應該透過指標來傳遞。現在可以這樣,比如返回整個結構體,這種情況應該避免。如果必須要返回大的結構體,呼叫者必須開闢儲存空間,並透過第一個引數傳遞指標,整個過程對程式是透明的。像手動透過第一個引數傳遞指標一樣,只是編譯器隱藏了這個過程。
小例子:
#!cpp
struct s
{
int a;
int b;
int c;
};
struct s get_some_values (int a)
{
struct s rt;
rt.a=a+1;
rt.b=a+2;
rt.c=a+3;
return rt;
};
…我們可以得到(MSVC 2010 /Ox):
#!bash
$T3853 = 8 ; size = 4
_a$ = 12 ; size = 4
?get_some_values@@YA?AUs@@[email protected] PROC ; get_some_values
mov ecx, DWORD PTR _a$[esp-4]
mov eax, DWORD PTR $T3853[esp-4]
lea edx, DWORD PTR [ecx+1]
mov DWORD PTR [eax], edx
lea edx, DWORD PTR [ecx+2]
add ecx, 3
mov DWORD PTR [eax+4], edx
mov DWORD PTR [eax+8], ecx
ret 0
?get_some_values@@YA?AUs@@[email protected] ENDP ; get_some_values
內部變數傳遞指標到結構體的宏為$T3853。
這個例子可以用C99語言擴充套件來重寫:
#!bash
struct s
{
int a;
int b;
int c;
};
struct s get_some_values (int a)
{
return (struct s){.a=a+1, .b=a+2, .c=a+3};
};
Listing 8.3: GCC 4.8.1
#!bash
_get_some_values proc near
ptr_to_struct = dword ptr 4
a = dword ptr 8
mov edx, [esp+a]
mov eax, [esp+ptr_to_struct]
lea ecx, [edx+1]
mov [eax], ecx
lea ecx, [edx+2]
add edx, 3
mov [eax+4], ecx
mov [eax+8], edx
retn
_get_some_values endp
我們可以看到,函式僅僅填充呼叫者申請的結構體空間的相應欄位。因此沒有效能缺陷。
Chapter 9
指標
指標通常被用作函式返回值(recall scanf() case (6)).例如,當函式返回兩個值時。
9.1 Global variables example
#!bash
#include <stdio.h>
void f1 (int x, int y, int *sum, int *product)
{
*sum=x+y;
*product=x*y;
};
int sum, product;
void main()
{
f1(123, 456, &sum, &product);
printf ("sum=%d, product=%d
", sum, product);
};
編譯後
Listing 9.1: Optimizing MSVC 2010 (/Ox /Ob0)
#!bash
COMM _product:DWORD
COMM _sum:DWORD
$SG2803 DB ’sum=%d, product=%d’, 0aH, 00H
_x$ = 8 ; size = 4
_y$ = 12 ; size = 4
_sum$ = 16 ; size = 4
_product$ = 20 ; size = 4
_f1 PROC
mov ecx, DWORD PTR _y$[esp-4]
mov eax, DWORD PTR _x$[esp-4]
lea edx, DWORD PTR [eax+ecx]
imul eax, ecx
mov ecx, DWORD PTR _product$[esp-4]
push esi
mov esi, DWORD PTR _sum$[esp]
mov DWORD PTR [esi], edx
mov DWORD PTR [ecx], eax
pop esi
ret 0
_f1 ENDP
_main PROC
push OFFSET _product
push OFFSET _sum
push 456 ; 000001c8H
push 123 ; 0000007bH
call _f1
mov eax, DWORD PTR _product
mov ecx, DWORD PTR _sum
push eax
push ecx
push OFFSET $SG2803
call DWORD PTR __imp__printf
add esp, 28 ; 0000001cH
xor eax, eax
ret 0
_main ENDP
讓我們在OD中檢視:圖9.1。首先全域性變數地址被傳遞進f1()。我們在堆疊元素點選“資料視窗跟隨”,可以看到資料段上分配兩個變數的空間。這些變數被置0,因為未初始化資料(BSS1)在程式執行之前被清理為0。這些變數屬於資料段,我們按Alt+M可以檢視記憶體對映fig. 9.5.
讓我們跟蹤(F7)到f1()fig. 9.2.在堆疊中為456 (0x1C8) 和 123 (0x7B),接著是兩個全域性變數的地址。
讓我們跟蹤到f1()結尾,可以看到兩個全域性變數存放了計算結果。
現在兩個全域性變數的值被載入到暫存器傳遞給printf(): fig. 9.4.
Figure 9.1: OllyDbg: 全域性變數地址被傳遞進f1()
Figure 9.2: OllyDbg: f1()開始
Figure 9.3: OllyDbg: f1()完成
Figure 9.4: OllyDbg: 全域性變數被傳遞進printf()
Figure 9.5: OllyDbg: memory map
9.2 Local variables example
讓我們修改一下例子:
Listing 9.2: 區域性變數
#!bash
void main()
{
int sum, product; // now variables are here
f1(123, 456, &sum, &product);
printf ("sum=%d, product=%d
", sum, product);
};
f1()函式程式碼沒有改變。僅僅main()程式碼作了修改。
Listing 9.3: Optimizing MSVC 2010 (/Ox /Ob0)
#!bash
_product$ = -8 ; size = 4
_sum$ = -4 ; size = 4
_main PROC
; Line 10
sub esp, 8
; Line 13
lea eax, DWORD PTR _product$[esp+8]
push eax
lea ecx, DWORD PTR _sum$[esp+12]
push ecx
push 456 ; 000001c8H
push 123 ; 0000007bH
call _f1
; Line 14
mov edx, DWORD PTR _product$[esp+24]
mov eax, DWORD PTR _sum$[esp+24]
push edx
push eax
push OFFSET $SG2803
call DWORD PTR __imp__printf
; Line 15
xor eax, eax
add esp, 36 ; 00000024H
ret 0
我們在OD中檢視,區域性變數地址在堆疊中是0x35FCF4和0x35FCF8。我們可以看到是如何圧棧的fig. 9.6.
f1()開始的時候,隨機棧地址為0x35FCF4和0x35FCF8 fig. 9.7.
f1()完成時結果0xDB18和0x243存放在地址0x35FCF4和0x35FCF8。
Figure 9.6: OllyDbg: 區域性變數地址被圧棧
Figure 9.7: OllyDbg: f1()starting
Figure 9.8: OllyDbg: f1()finished
9.3 小結
f1()可以返回結果到記憶體的任何地方,這是指標的本質和特性。順便提一下,C++引用的工作方式和這個類似。詳情閱讀相關內容(33)。
Chapter 10
條件跳轉
現在我們來了解條件跳轉。
#!cpp
#include <stdio.h>
void f_signed (int a, int b)
{
if (a>b)
printf ("a>b
");
if (a==b)
printf ("a==b
");
if (a<b)
printf ("a<b
");
};
void f_unsigned (unsigned int a, unsigned int b)
{
if (a>b)
printf ("a>b
");
if (a==b)
printf ("a==b
");
if (a<b)
printf ("a<b
");
};
int main()
{
f_signed(1, 2);
f_unsigned(1, 2);
return 0;
};
10.1 x86
10.1.1 x86 + MSVC
f_signed() 函式:
Listing 10.1: 非最佳化MSVC 2010
#!bash
_a$ = 8
_b$ = 12
_f_signed PROC
push ebp
mov ebp, esp
mov eax, DWORD PTR _a$[ebp]
cmp eax, DWORD PTR _b$[ebp]
jle SHORT [email protected]_signed
push OFFSET $SG737 ; ’a>b’
call _printf
add esp, 4
[email protected]_signed:
mov ecx, DWORD PTR _a$[ebp]
cmp ecx, DWORD PTR _b$[ebp]
jne SHORT [email protected]_signed
push OFFSET $SG739 ; ’a==b’
call _printf
add esp, 4
[email protected]_signed:
mov edx, DWORD PTR _a$[ebp]
cmp edx, DWORD PTR _b$[ebp]
jge SHORT [email protected]_signed
push OFFSET $SG741 ; ’a<b’
call _printf
add esp, 4
[email protected]_signed:
pop ebp
ret 0
_f_signed ENDP
第一個指令JLE意味如果小於等於則跳轉。換句話說,第二個運算元大於或者等於第一個運算元,控制流將傳遞到指定地址或者標籤。否則(第二個運算元小於第一個運算元)第一個printf()將被呼叫。第二個檢測JNE:如果不相等則跳轉。如果兩個運算元相等控制流則不變。第三個檢測JGE:大於等於跳轉,當第一個運算元大於或者等於第二個運算元時跳轉。如果三種情況都沒有發生則無printf()被呼叫,事實上,如果沒有特殊干預,這種情況幾乎不會發生。
f_unsigned()函式類似,只是JBE和JAE替代了JLE和JGE,我們來看f_unsigned()函式
Listing 10.2: GCC
#!bash
_a$ = 8 ; size = 4
_b$ = 12 ; size = 4
_f_unsigned PROC
push ebp
mov ebp, esp
mov eax, DWORD PTR _a$[ebp]
cmp eax, DWORD PTR _b$[ebp]
jbe SHORT [email protected]_unsigned
push OFFSET $SG2761 ; ’a>b’
call _printf
add esp, 4
[email protected]_unsigned:
mov ecx, DWORD PTR _a$[ebp]
cmp ecx, DWORD PTR _b$[ebp]
jne SHORT [email protected]_unsigned
push OFFSET $SG2763 ; ’a==b’
call _printf
add esp, 4
[email protected]_unsigned:
mov edx, DWORD PTR _a$[ebp]
cmp edx, DWORD PTR _b$[ebp]
jae SHORT [email protected]_unsigned
push OFFSET $SG2765 ; ’a<b’
call _printf
add esp, 4
[email protected]_unsigned:
pop ebp
ret 0
_f_unsigned ENDP
幾乎是相同的,不同的是:JBE-小於等於跳轉和JAE-大於等於跳轉。這些指令(JA/JAE/JBE/JBE)不同於JG/JGE/JL/JLE,它們使用無符號值。
我們也可以看到有符號值的表示(35)。因此我們看JG/JL代替JA/JBE的用法或者相反,我們幾乎可以確定變數的有符號或者無符號型別。
main()函式沒有什麼新的內容:
Listing 10.3: main()
#!bash
_main PROC
push ebp
mov ebp, esp
push 2
push 1
call _f_signed
add esp, 8
push 2
push 1
call _f_unsigned
add esp, 8
xor eax, eax
pop ebp
ret 0
_main ENDP
10.1.2 x86 + MSVC + OllyDbg
我們在OD裡允許例子來檢視標誌暫存器。我們從f_unsigned()函式開始。CMP執行了三次,每次的引數都相同,所以標誌位也相同。
第一次比較的結果:fig. 10.1.標誌位:C=1, P=1, A=1, Z=0, S=1, T=0, D=0, O=0.標誌位名稱為OD對其的簡稱。
當CF=1 or ZF=1時JBE將被觸發,此時將跳轉。
接下來的條件跳轉:fig. 10.2.當ZF=0(zero flag)時JNZ則被觸發
第三個條件跳轉:fig. 10.3.我們可以發現14當CF=0 (carry flag)時,JNB將被觸發。在該例中條件不為真,所以第三個printf()將被執行。
Figure 10.1: OllyDbg: f_unsigned(): 第一個條件跳轉
Figure 10.2: OllyDbg: f_unsigned(): 第二個條件跳轉
Figure 10.3: OllyDbg: f_unsigned(): 第三個條件跳轉
現在我們在OD中看f_signed()函式使用有符號值。
可以看到標誌暫存器:C=1, P=1, A=1, Z=0, S=1, T=0, D=0, O=0.
第一種條件跳轉JLE將被觸發fig. 10.4.我們可以發現14,當ZF=1 or SF≠OF。該例中SF≠OF,所以跳轉將被觸發。
下一個條件跳轉將被觸發:如果ZF=0 (zero flag): fig. 10.5.
第三個條件跳轉將不會被觸發,因為僅有SF=OF,該例中不為真: fig. 10.6.
Figure 10.4: OllyDbg: f_signed(): 第一個條件跳轉
Figure 10.5: OllyDbg: f_signed(): 第二個條件跳轉
Figure 10.6: OllyDbg: f_signed(): 第三個條件跳轉
10.1.3 x86 + MSVC + Hiew
我們可以修改這個可執行檔案,使其無論輸入的什麼值f_unsigned()函式都會列印“a==b”。
在Hiew中檢視:fig. 10.7.
我們要完成以下3個任務:
1. 使第一個跳轉一直被觸發;
2. 使第二個跳轉從不被觸發;
3. 使第三個跳轉一直被觸發。
我們需要使程式碼流進入第二個printf(),這樣才一直列印“a==b”。
三個指令(或位元組)應該被修改:
1. 第一個跳轉修改為JMP,但跳轉偏移值不變。
2. 第二個跳轉有時可能被觸發,我們修改跳轉偏移值為0後,無論何種情況,程式總是跳向下一條指令。跳轉地址等於跳轉偏移值加上下一條指令地址,當跳轉偏移值為0時,跳轉地址就為下一條指令地址,所以無論如何下一條指令總被執行。
3. 第三個跳轉我們也修改為JMP,這樣跳轉總被觸發。
修改後:fig. 10.8.
如果忘了這些跳轉,printf()可能會被多次呼叫,這種行為可能是我們不需要的。
Figure 10.7: Hiew: f_unsigned() 函式
Figure 10.8: Hiew:我們修改 f_unsigned() 函式
10.1.4 Non-optimizing GCC
GCC 4.4.1非最佳化狀態產生的程式碼幾乎一樣,只是用puts() (2.3.3) 替代 printf()。
10.1.5 Optimizing GCC
細心的讀者可能會問,為什麼要多次執行CMP,如果標誌暫存器每次都相同呢?可能MSVC不會做這樣的最佳化,但是GCC 4.8.1可以做這樣的深度最佳化:
Listing 10.4: GCC 4.8.1 f_signed()
#!bash
f_signed:
mov eax, DWORD PTR [esp+8]
cmp DWORD PTR [esp+4], eax
jg .L6
je .L7
jge .L1
mov DWORD PTR [esp+4], OFFSET FLAT:.LC2 ; "a<b"
jmp puts
.L6:
mov DWORD PTR [esp+4], OFFSET FLAT:.LC0 ; "a>b"
jmp puts
.L1:
rep ret
.L7:
mov DWORD PTR [esp+4], OFFSET FLAT:.LC1 ; "a==b"
jmp puts
我們可以看到JMP puts替代了CALL puts/RETN。稍後我們介紹這種情況11.1.1.。
不用說,這種型別的x86程式碼是很少見的。MSVC2012似乎不會這樣做。其他情況下,彙編程式能意識到此類使用。如果你在其它地方看到此類程式碼,更可能是手工構造的。
f_unsigned()函式程式碼:
Listing 10.5: GCC 4.8.1 f_unsigned()
#!bash
f_unsigned:
push esi
push ebx
sub esp, 20
mov esi, DWORD PTR [esp+32]
mov ebx, DWORD PTR [esp+36]
cmp esi, ebx
ja .L13
cmp esi, ebx ; instruction may be removed
je .L14
.L10:
jb .L15
add esp, 20
pop ebx
pop esi
ret
.L15:
mov DWORD PTR [esp+32], OFFSET FLAT:.LC2 ; "a<b"
add esp, 20
pop ebx
pop esi
jmp puts
.L13:
mov DWORD PTR [esp], OFFSET FLAT:.LC0 ; "a>b"
call puts
cmp esi, ebx
jne .L10
.L14:
mov DWORD PTR [esp+32], OFFSET FLAT:.LC1 ; "a==b"
add esp, 20
pop ebx
pop esi
jmp puts
因此,GCC 4.8.1的最佳化演算法並不總是完美的。
10.2 ARM
10.2.1 Keil + ARM mode最佳化後
Listing 10.6: Optimizing Keil + ARM mode
#!bash
.text:000000B8 EXPORT f_signed
.text:000000B8 f_signed ; CODE XREF: main+C
.text:000000B8 70 40 2D E9 STMFD SP!, {R4-R6,LR}
.text:000000BC 01 40 A0 E1 MOV R4, R1
.text:000000C0 04 00 50 E1 CMP R0, R4
.text:000000C4 00 50 A0 E1 MOV R5, R0
.text:000000C8 1A 0E 8F C2 ADRGT R0, aAB ; "a>b
"
.text:000000CC A1 18 00 CB BLGT __2printf
.text:000000D0 04 00 55 E1 CMP R5, R4
.text:000000D4 67 0F 8F 02 ADREQ R0, aAB_0 ; "a==b
"
.text:000000D8 9E 18 00 0B BLEQ __2printf
.text:000000DC 04 00 55 E1 CMP R5, R4
.text:000000E0 70 80 BD A8 LDMGEFD SP!, {R4-R6,PC}
.text:000000E4 70 40 BD E8 LDMFD SP!, {R4-R6,LR}
.text:000000E8 19 0E 8F E2 ADR R0, aAB_1 ; "a<b
"
.text:000000EC 99 18 00 EA B __2printf
.text:000000EC ; End of function f_signed
ARM下很多指令只有某些標誌位被設定時才會被執行。比如做數值比較時。
舉個例子,ADD實施上是ADDAL,這裡的AL是Always,即總被執行。判定謂詞是32位ARM指令的高4位(條件域)。無條件跳轉的B指令其實是有條件的,就行其它任何條件跳轉一樣,只是條件域為AL,這意味著總是被執行,忽略標誌位。
ADRGT指令就像和ADR一樣,只是該指令前面為CMP指令,並且只有前面數值大於另一個數值時(Greater Than)時才被執行。
接下來的BLGT行為和BL一樣,只有比較結果符合條件才能出發(Greater Than)。ADRGT把字串“a>b ”的地址寫入R0,然後BLGT呼叫printf()。因此,這些指令都帶有GT字尾,只有當R0(a值)大於R4(b值)時指令才會被執行。
然後我們看ADREQ和BLEQ,這些指令動作和ADR及BL一樣,只有當兩個運算元對比後相等時才會被執行。這些指令前面是CMP(因為printf()呼叫可能會修改狀態標識)。 然後我們看LDMGEFD,該指令行為和LDMFD指令一樣1,僅僅當第一個值大於等於另一個值時(Greater Than),指令才會被執行。
“LDMGEFD SP!, {R4-R6,PC}”恢復暫存器並返回,只是當a>=b時才被觸發,這樣之後函式才執行完成。但是如果a<b,觸發條件不成立是將執行下一條指令LDMFD SP!, {R4-R6,LR},該指令儲存R4-R6暫存器,使用LR而不是PC,函式並不返回。最後兩條指令是執行printf()(5.3.2)。
f_unsigned與此一樣只是使用對應的指令為ADRHI, BLHI及LDMCSFD,判斷謂詞(HI = Unsigned higher, CS = Carry Set (greater than or equal))請類比之前的說明,另外就是函式內部使用無符號數值。
我們來看一下main()函式:
Listing 10.7: main()
#!bash
.text:00000128 EXPORT main
.text:00000128 main
.text:00000128 10 40 2D E9 STMFD SP!, {R4,LR}
.text:0000012C 02 10 A0 E3 MOV R1, #2
.text:00000130 01 00 A0 E3 MOV R0, #1
.text:00000134 DF FF FF EB BL f_signed
.text:00000138 02 10 A0 E3 MOV R1, #2
.text:0000013C 01 00 A0 E3 MOV R0, #1
.text:00000140 EA FF FF EB BL f_unsigned
.text:00000144 00 00 A0 E3 MOV R0, #0
.text:00000148 10 80 BD E8 LDMFD SP!, {R4,PC}
.text:00000148 ; End of function main
這就是ARM模式如何避免使用條件跳轉。
這樣做有什麼好處呢?因為ARM使用精簡指令集(RISC)。簡言之,處理器流水線技術受到跳轉的影響,這也是分支預測重要的原因。程式使用的條件或者無條件跳轉越少越好,使用斷言指令可以減少條件跳轉的使用次數。
x86沒有這也的功能,透過使用CMP設定相應的標誌位來觸發指令。
10.2.2 Optimizing Keil + thumb mode
Listing 10.8: Optimizing Keil + thumb mode
#!bash
.text:00000072 f_signed ; CODE XREF: main+6
.text:00000072 70 B5 PUSH {R4-R6,LR}
.text:00000074 0C 00 MOVS R4, R1
.text:00000076 05 00 MOVS R5, R0
.text:00000078 A0 42 CMP R0, R4
.text:0000007A 02 DD BLE loc_82
.text:0000007C A4 A0 ADR R0, aAB ; "a>b
"
.text:0000007E 06 F0 B7 F8 BL __2printf
.text:00000082
.text:00000082 loc_82 ; CODE XREF: f_signed+8
.text:00000082 A5 42 CMP R5, R4
.text:00000084 02 D1 BNE loc_8C
.text:00000086 A4 A0 ADR R0, aAB_0 ; "a==b
"
.text:00000088 06 F0 B2 F8 BL __2printf
.text:0000008C
.text:0000008C loc_8C ; CODE XREF: f_signed+12
.text:0000008C A5 42 CMP R5, R4
.text:0000008E 02 DA BGE locret_96
.text:00000090 A3 A0 ADR R0, aAB_1 ; "a<b
"
.text:00000092 06 F0 AD F8 BL __2printf
.text:00000096
.text:00000096 locret_96 ; CODE XREF: f_signed+1C
.text:00000096 70 BD POP {R4-R6,PC}
.text:00000096 ; End of function f_signed
僅僅Thumb模式下的B指令可能需要條件程式碼輔助,所以thumb程式碼看起來更普通一些。
BLE通常是條件跳轉小於或等於(Less than or Equal),BNE—不等於(Not Equal),BGE—大於或等於(Greater than or Equal)。
f_unsigned函式是同樣的,只是使用的指令用來處理無符號數值:BLS (Unsigned lower or same) 和BCS (Carry Set (Greater than or equal)).
相關文章
- 羽夏逆向——逆向基礎2021-11-19
- 逆向基礎(六)2020-08-19
- 逆向基礎(五)2020-08-19
- 逆向基礎(四)2020-08-19
- 逆向基礎(十一)2020-08-19
- 逆向基礎(十)2020-08-19
- 逆向基礎(十二)2020-08-19
- 逆向基礎(一)2020-08-19
- 逆向基礎(二)2020-08-19
- 逆向基礎(九)2020-08-19
- 逆向基礎(八)2020-08-19
- 逆向基礎(七)2020-08-19
- 逆向工程核心原理(1)逆向基礎2023-03-16
- 逆向基礎(十三) JAVA (一)2020-08-19Java
- iOS逆向-彙編基礎(一)2018-11-05iOS
- iOS逆向與安全:基礎篇2018-10-31iOS
- iOS逆向之旅(基礎篇) — 彙編(一)— 彙編基礎2018-10-25iOS
- iOS逆向之旅(基礎篇) — 彙編(三) — 彙編下的 Switch語句2018-10-25iOS
- iOS逆向之旅(基礎篇) — Macho檔案2018-10-26iOSMac
- 逆向基礎 Finding important/interesting stuff in the code (二)2020-08-19ImportREST
- 20192204-exp1-逆向與Bof基礎2022-03-17
- Java逆向基礎之靜態變數存取2021-09-09Java變數
- NIO(三)基礎2018-12-26
- c 基礎三2018-07-10
- Kotlin基礎三2018-06-12Kotlin
- Dart基礎(三)2019-07-11Dart
- 逆向WeChat(三)2024-05-23
- Golang 基礎之基礎語法梳理 (三)2022-03-20Golang
- DataBinding基礎使用三2018-08-10
- ADT基礎(三)—— HashMap2021-02-28HashMap
- 逆向基礎——軟體手動脫殼技術入門2020-08-19
- Python (三) 基礎資訊2018-11-29Python
- python 基礎語法(三)2018-12-15Python
- Python基礎(三)數字2018-07-17Python
- 前端基礎(三):函式2018-06-21前端函式
- python基礎(三)——操作列表2020-11-11Python
- Three.js基礎(三)2020-09-28JS
- typeScript 基礎型別 (三)2024-11-21TypeScript型別