iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0

前幾天我們開始慢慢把 Reverse 的流程串起來:

Control Flow
    ↓
Xref
    ↓
Function Analysis

在前面的範例裡,我們常常可以很舒服地看到:

main
check_password
verify
strcmp

但實際拿到 Binary 時,事情通常不會這麼順利。

有時候把 Binary 丟進 IDA / Ghidra,看到的可能不是:

check_password()

而是:

sub_401176
sub_4011A3
sub_401210

原因之一就是:

Stripped Binary

今天就來看看:

當 Function Name 都消失之後,我們還能怎麼 Reverse?


0x00 Symbol 是什麼?

假設我們今天寫一個簡單的程式:

#include <stdio.h>
#include <string.h>

int check_password(char *input)
{
    return strcmp(input, "rev_is_fun") == 0;
}

int main()
{
    char input[32];

    printf("Password: ");
    scanf("%31s", input);

    if (check_password(input))
        puts("Correct!");
    else
        puts("Wrong!");

    return 0;
}

Compile:

gcc chall.c -o chall

接著可以使用:

nm chall

可能看到:

0000000000001159 T check_password
0000000000001185 T main

這些:

check_password
main

就是 Symbol。

它可以幫助 Debugger、Linker 或分析工具知道:

這個 Address 對應到哪個 Function

對 Reverse Engineering 來說,有 Symbol 當然非常舒服。

因為:

check_password

光看名字就已經告訴我們:

這個 Function 八成跟 Password Validation 有關。


0x01 Strip

但 Release Binary 通常不一定會留下這些資訊。

我們可以自己試試:

strip chall

接著再:

file chall

可能會看到:

ELF 64-bit LSB pie executable, x86-64, dynamically linked, stripped

最後面的:

stripped

就是今天的主角。

再執行:

nm chall

可能會得到:

nm: chall: no symbols

原本的:

main
check_password

都不見了。


0x02 丟進 IDA / Ghidra

這時候再把 Binary 丟進 IDA。

原本可能看到:

main
check_password

現在可能變成:

sub_1159
sub_1185

Ghidra 則可能看到:

FUN_00101159
FUN_00101185

因為分析工具不知道:

這個 Function 原本叫什麼

所以只能用:

Address

幫它暫時命名。

例如:

sub_1159

基本上可以理解成:

位於 0x1159 的 Function

但這不代表我們不能分析。

只是代表:

Function Name 不再能直接告訴我們答案。


0x03 String 還在

第一件事情還是可以從最便宜的方法開始。

strings chall

我們可能還是可以找到:

Password:
Correct!
Wrong!
rev_is_fun

這裡要注意:

Strip != 刪除所有 String

Strip 主要處理的是 Symbol 等資訊。

程式真正執行時需要使用的:

Password:
Correct!
Wrong!

通常還是得存在 Binary 裡。

所以即使 Binary 被 Strip:

strings

依然可能提供非常多線索。


0x04 從 String 找回 Function

假設我們在 IDA 裡看到:

"Correct!"

這時就可以使用上一篇介紹的:

Xref

查看:

誰使用了這個 String?

例如:

"Correct!"
    ↑
    Xref
    ↑
sub_1185

那我們就知道:

sub_1185

很可能是負責處理 Password 驗證結果的地方。

接著進去看。

可能看到:

undefined8 FUN_00101185(void)
{
    char input[32];

    printf("Password: ");
    scanf("%31s", input);

    if (FUN_00101159(input) != 0)
        puts("Correct!");
    else
        puts("Wrong!");

    return 0;
}

雖然名字變成:

FUN_00101159

但程式邏輯其實沒有消失。

我們依然可以知道:

User Input
    ↓
FUN_00101159
    ↓
Return Value
    ↓
Correct / Wrong

因此我們可以合理推測:

FUN_00101159

就是某種:

Validation Function

0x05 Function Name 不重要,Behavior 才重要

再進入:

FUN_00101159

可能看到:

int FUN_00101159(char *param_1)
{
    int result;

    result = strcmp(param_1, "rev_is_fun");

    return result == 0;
}

這時候即使原本的:

check_password

已經消失,其實也完全不影響我們理解它。

因為:

strcmp

已經告訴我們這個 Function 在做什麼。

我們可以自己把它 Rename:

FUN_00101159

改成:

check_password

再把:

FUN_00101185

改成:

main

Reverse 過程中非常重要的一個習慣就是:

不要被工具產生的名字綁住。

看到:

sub_401234

不代表你要一路記著:

401234

當你理解它的功能之後,就可以直接 Rename:

sub_401234
    ↓
parse_input

或:

sub_401500
    ↓
check_password

慢慢把 Binary 重新整理成:

自己看得懂的程式

0x06 Library Function 為什麼還看得到?

這時可能會有一個問題。

既然 Binary 已經 Strip:

為什麼 strcmp、puts、printf 還看得到?

因為這些 Function 很多是:

Dynamic Linking

需要使用的 External Symbol。

例如:

strcmp@plt
puts@plt
printf@plt

程式執行時還需要知道怎麼找到這些外部 Function。

因此 Stripped Binary 裡:

自己寫的 Function Name

通常比較容易消失。

但:

Imported Function

仍然可能留下非常重要的線索。

這也是 Reverse 時很常看的東西。

例如看到:

strcmp

可能想到:

字串比較
Password / Token Validation

看到:

malloc
free

可能想到:

Heap
Dynamic Memory

看到:

open
read
write

可能想到:

File / IO

Function Name 本身就是一種線索。


0x07 沒有 main 怎麼辦?

Stripped Binary 裡還有另一個很常遇到的問題:

main 不見了

但程式執行時:

main

當然還是存在。

只是:

main 這個名字

消失了。

Linux ELF 執行時通常不是直接:

Entry Point
    ↓
main

而比較接近:

Entry Point
    ↓
_start
    ↓
Runtime Initialization
    ↓
main

因此如果真的完全不知道 main 在哪裡,我們也可以從:

readelf -h chall

找到:

Entry point address

再從 Entry Point 開始往下追。

不過在 CTF 的簡單題目中,通常不需要每次都從 _start 慢慢追。

我們還是可以優先利用:

Strings
Xref
Imported Functions
Control Flow

找到真正重要的 Function。


0x08 Reverse Stripped Binary 的思路

所以今天的流程可以整理成:

Stripped Binary
      ↓
file
      ↓
strings
      ↓
找有意義的 String
      ↓
Xref
      ↓
找到使用 String 的 Function
      ↓
觀察 Caller / Callee
      ↓
分析 Function Behavior
      ↓
Rename
      ↓
慢慢重建程式邏輯

例如今天:

"Correct!"
      ↓
Xref
      ↓
FUN_00101185
      ↓
呼叫 FUN_00101159
      ↓
FUN_00101159 呼叫 strcmp
      ↓
理解它在驗證 Password
      ↓
Rename

最後就可以重新得到:

main
    ↓
check_password
    ↓
strcmp

也就是說:

Symbol 被拿掉,不代表程式邏輯被拿掉。


0x09 Rename 是 Reverse 的一部分

剛開始學 Reverse 的時候,很容易一直看到:

FUN_00101234
FUN_00101453
FUN_00101842

然後看到最後自己也不知道誰是誰。

所以我自己覺得 Rename 是一個非常重要的習慣。

例如分析過程中:

FUN_00101234

發現它負責讀 User Input。

就改成:

read_input

另一個:

FUN_00101453

發現負責比對 Password。

改成:

check_password

另一個:

FUN_00101842

負責輸出結果。

改成:

print_result

原本:

FUN_00101234
FUN_00101453
FUN_00101842

很快就會變成:

read_input
    ↓
check_password
    ↓
print_result

Binary 的結構也會慢慢變得比較容易理解。


0x0A 小結

今天介紹的 Stripped Binary,其實只是把 Reverse 再往真實世界推進一步。

前面的 Binary 很友善:

main
check_password
verify

但當 Symbol 消失之後:

sub_1159
FUN_00101185
sub_401234

我們就不能再依賴 Function Name。

而是開始依賴:

String
Imported Function
Xref
Control Flow
Caller / Callee
Function Behavior

這也是前面幾天一直在介紹這些東西的原因。

因為真正 Reverse 一個 Binary 時,我們做的事情其實就是:

找到線索
    ↓
追蹤 Reference
    ↓
理解 Function
    ↓
Rename
    ↓
建立更多線索
    ↓
逐漸還原整個 Program

到了這裡,我們已經差不多把 Reverse Engineering 最基本的一套 Workflow 串起來了。

下一篇就來做一次比較完整的 Reverse 實戰!


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

尚未有邦友留言

立即登入留言