iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0

昨天我們實際分析了第一個 Crackme,並把前面學過的 Static Analysis, IDA / Ghidra, GDB 全部串在一起。

但昨天的 Crackme 有一個很大的問題,就是他的驗證邏輯太單純了。

只要找到 strcmp(),甚至直接觀察傳入的 Argument,就很容易知道 Password。

但實際的 Reverse 題目或者實際漏洞研究並不會這麼友善。

我們更常看到的是:

cmp eax, 5
jne loc_401200

test edx, edx
je loc_401250

cmp ecx, 0x539
jg loc_401280

jmp loc_401300

看到這種程式時,如果只是一行一行讀 Assembly,很快就會不知道:

現在到底跳到哪裡去了?

所以今天要開始學 Reverse Engineering 很重要的一個概念:Control Flow


0x00 Control Flow 是什麼?

Control Flow 就是:

程式執行的流程。

正常情況下 CPU 會依序執行:

Instruction 1
Instruction 2
Instruction 3
Instruction 4

但程式不可能永遠從上跑到下。

例如 C Code:

if (password == 1337) {
    puts("Correct");
} else {
    puts("Wrong");
}

程式必須根據條件決定:

password == 1337
       │
   ┌───┴───┐
  Yes      No
   │        │
Correct    Wrong

這就是最基本的 Control Flow。


0x01 從 C Code 看 Control Flow

先看一個簡單的程式:

#include <stdio.h>

int check(int value) {

    if (value == 1337) {
        return 1;
    }

    return 0;
}

int main() {

    int input;

    printf("Number: ");
    scanf("%d", &input);

    if (check(input)) {
        puts("Correct!");
    } else {
        puts("Wrong!");
    }

    return 0;
}

編譯:

gcc chall.c -o chall

如果從 Source Code 看,很容易理解:

Input
  ↓
check()
  ↓
value == 1337 ?
  ↓
Yes / No

但 Reverse Engineering 的時候我們沒有 Source Code。

看到的可能是:

cmp DWORD PTR [rbp-0x4], 0x539
jne loc_wrong

mov eax, 1
jmp loc_end

loc_wrong:
mov eax, 0

loc_end:
ret

其中:

0x539 = 1337

所以其實就是:

if (value == 1337)

0x02 Branch

Control Flow 裡面最重要的一個東西就是:

Branch

也就是「分支」。

前面的 Assembly:

cmp eax, 0x539
jne loc_wrong

可以理解成:

eax == 1337 ?

接著程式可能往兩個方向走:

           cmp eax, 1337
                │
          ┌─────┴─────┐
        Equal       Not Equal
          │             │
          ↓             ↓
       Correct         Wrong

這就是一個 Branch。

前面的 Assembly 文章已經介紹過:

je
jne
jg
jl
jge
jle

真正重要的是:

看到 Conditional Jump 時,要開始思考它把程式切成了哪些路徑。


0x03 Basic Block

接著介紹 Reverse 裡非常常看到的一個詞:

Basic Block

Basic Block 可以先簡單理解成:

一段會按照順序執行,不會在中間突然跳走的 Instruction。

例如:

mov eax, DWORD PTR [rbp-0x4]
cmp eax, 0x539
jne loc_wrong

可以視為一個 Basic Block。

接著:

mov eax, 1
jmp loc_end

可能是另一個 Basic Block。

而以下又是另一個:

loc_wrong:
mov eax, 0

所以原本看起來像:

一大坨 Assembly

其實可以拆成:

Block A
   │
 ┌─┴─┐
 ↓   ↓
B     C
 \   /
   D

當 Binary 越來越大時,這種看法會比逐行閱讀容易很多。


0x04 CFG

把這些 Basic Block 與它們之間的 Jump 關係連起來,就會得到 Control Flow Graph 簡稱 CFG

例如剛才的 check():

        ┌───────────────┐
        │ value == 1337 │
        └───────┬───────┘
                │
          ┌─────┴─────┐
          │           │
        True         False
          │           │
          ▼           ▼
     ┌────────┐   ┌────────┐
     │ return │   │ return │
     │   1    │   │   0    │
     └────────┘   └────────┘

這就是一個非常簡單的 CFG。


0x05 IDA / Ghidra 的 Graph View

前面介紹 IDA / Ghidra 時有稍微看過 Graph View。

現在就可以知道 Graph View 到底在畫什麼了。

它並不是單純把 Assembly 排得比較漂亮,而是在顯示:

Basic Block
+
Branch
+
Jump

形成的:

Control Flow Graph

例如 IDA 裡可能看到:

          ┌─────────────┐
          │ cmp eax,539 │
          │ jne loc_x   │
          └──────┬──────┘
                 │
          ┌──────┴──────┐
          │             │
          ▼             ▼
    ┌──────────┐   ┌──────────┐
    │ mov eax,1│   │ mov eax,0│
    └─────┬────┘   └─────┬────┘
          │              │
          └──────┬───────┘
                 ▼
                ret

這時候就不需要一直追:

0x401163
0x401172
0x401184
0x40119a

而是可以直接看:

程式在哪裡判斷?
判斷後有幾條路?
哪一條是 Success?
哪一條是 Failure?

0x06 怎麼看 Control Flow?

假設今天看到:

cmp eax, 0
je loc_fail

cmp ebx, 10
jl loc_fail

cmp ecx, 0x539
jne loc_fail

call print_flag

不要急著一行一行翻譯。

可以先整理成:

eax == 0 ?
   │
  Yes → Fail
   │
   No
   ↓
ebx < 10 ?
   │
  Yes → Fail
   │
   No
   ↓
ecx == 1337 ?
   │
  No → Fail
   │
  Yes
   ↓
print_flag()

瞬間就會比較清楚。

這也是我覺得 Reverse 很重要的一個觀念:

不一定要把每一條 Assembly 都翻成 C,先把程式的邏輯流程思考出來通常更重要。


0x07 Loop 也是 Control Flow

除了 if / else 之外,Loop 也是 Control Flow。

例如:

for (int i = 0; i < 10; i++) {
    sum += i;
}

Assembly 可能出現:

mov DWORD PTR [rbp-0x4], 0

loop:
cmp DWORD PTR [rbp-0x4], 10
jge end

add DWORD PTR [rbp-0x8], eax
add DWORD PTR [rbp-0x4], 1

jmp loop

可以畫成:

        ┌──────────┐
        │ i < 10 ? │
        └────┬─────┘
             │
       ┌─────┴─────┐
      Yes          No
       │            │
       ▼            ▼
     Loop          End
       │
       └────────────┐
                    │
                    └── 回到判斷

所以看到:

jmp

往前跳時,也可以特別注意:

這裡是不是一個 Loop?


0x08 實際分析時的思路

之後拿到一個 Function,可以先不要試著理解全部 Instruction。

先找:

cmp
test
je
jne
jg
jl
jmp

接著問:

這裡在判斷什麼?
       ↓
True 往哪裡?
False 往哪裡?
       ↓
哪一條會到 Success?
哪一條會到 Failure?

最後再回頭分析:

要滿足什麼條件
才能走到我們想要的那一條路?

這其實就是很多 Crackme 的核心。


0x09 從「讀 Assembly」到「讀程式」

前面幾天我們比較像是在學:

mov 是什麼
cmp 是什麼
call 是什麼
je 是什麼

但到了現在,目標要慢慢變成:

Assembly
   ↓
Basic Block
   ↓
Branch
   ↓
Control Flow
   ↓
Program Logic

真正做 Reverse Engineering 時,我們通常不需要知道每一條 Instruction 的所有細節。

我們真正想知道的是:

Input 從哪裡進來?
↓
經過哪些 Function?
↓
在哪裡被驗證?
↓
有哪些 Branch?
↓
Success Path 在哪裡?

能回答這些問題,就已經開始從看 Assembly 進入 Reverse Engineering 了


0x0A 小結

今天介紹了 Reverse Engineering 中很重要的幾個概念:

Control Flow
Branch
Basic Block
CFG

簡單來說:

Basic Block
    ↓
透過 Jump / Branch 連接
    ↓
Control Flow Graph
    ↓
還原 Program Logic

昨天的 Crackme 還可以直接找到 strcmp()。

但之後遇到比較複雜的驗證邏輯時,我們就必須開始追:

Input
 ↓
Function
 ↓
Branch
 ↓
Branch
 ↓
Success

而當 Binary 越來越大的時候,又會遇到另一個問題:

我知道這個 Function 很重要,但到底有哪些地方會呼叫它?這個字串又在哪裡被使用?

下一篇就來介紹 Reverse Engineering 非常好用的功能:Cross Reference(XREF)


上一篇
Day11|First Crackme
下一篇
Day13|Xref & Function Analysis
系列文
從零開始の Binary CTF:30 天系統化學習 Rev & Pwn 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言