夜雨聆风学习资料网

ARTICLE · 1077202

ELF 二进制真的安全吗?从源码编译到 strings、objdump、Ghidra 的完整实战

ELF 二进制真的安全吗?从源码编译到 strings、objdump、Ghidra 的完整实战
做 Linux、嵌入式、Edge AI 开发时,我们每天都会接触各种二进制文件:
appcamera_servicelibxxx.soalgorithmlicense_manager

很多人会下意识认为:

源码已经编译成二进制了,别人应该看不到里面的东西了。

但实际上,编译不等于加密。

Linux 下最常见的可执行文件格式就是 ELF,全称:

Executable and Linkable Format

它的主要作用不是隐藏程序,而是告诉 Linux:

代码在哪里数据在哪里程序从哪里开始执行依赖哪些动态库哪些区域可读、可写、可执行

一个普通 C 程序:

main.c  ↓预处理  ↓编译  ↓汇编  ↓main.o  ↓链接  ↓ELF 可执行文件

最终虽然没有了我们熟悉的 C 源码,但:

机器指令字符串常量动态库依赖程序控制流部分符号信息

仍然可能存在。

下面不空讲理论,直接编译一个 ELF,然后一步一步看看:拿到一个二进制文件后,到底能分析出多少东西。


01|准备一个真实测试程序

先写一个非常简单的密码校验程序:

#include<stdio.h>#include<string.h>staticintverify_password(constchar *input){    const char *secret = "EdgeAI-2026!";    return strcmp(input, secret) == 0;}intmain(int argc, char **argv){    if (argc != 2) {        printf("Usage: %s <password>\n", argv[0]);        return 1;    }    if (verify_password(argv[1])) {        puts("Access granted");        return 0;    }    puts("Access denied");    return 2;}

保存为:

elf_demo.c

先编译一个带调试信息的版本:

gcc -O0 -g \    -fno-omit-frame-pointer \    -o elf_demo_dbg \    elf_demo.c

再复制一个 Strip 版本:

cp elf_demo_dbg elf_demo_strippedstrip elf_demo_stripped

最后再准备一个更接近正式产品的 Release 版本:

gcc -O2 -s \    -fPIE -pie \    -fstack-protector-strong \    -D_FORTIFY_SOURCE=2 \    -Wl,-z,relro,-z,now \    -o elf_demo_hardened \    elf_demo.c

现在有三个文件:

elf_demo_dbgelf_demo_strippedelf_demo_hardened

先运行一下:

./elf_demo_dbg wrong

输出:

Access denied

正确密码:

./elf_demo_dbg 'EdgeAI-2026!'

输出:

Access granted

接下来我们假设:

源码已经没有了,手里只有 ELF 文件。


02|file:先看看这个二进制到底是什么

拿到一个陌生程序,我一般第一步就是:

file elf_demo_dbgfile elf_demo_stripped

典型输出:

elf_demo_dbg:ELF 64-bit LSB pie executable,x86-64,dynamically linked,with debug_info,not stripped

Strip 版本:

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

这一条命令已经告诉我们:

文件格式:ELF位数:64-bitCPU:x86-64链接方式:动态链接是否 PIE是否 Strip是否带 Debug 信息

如果是在嵌入式设备上,通常也能直接看到:

ARMAArch64MIPSRISC-V

所以 ELF 并不是一个完全不可读的二进制黑盒。


03|readelf:把 ELF 的结构拆开

继续:

readelf -h elf_demo_stripped

可以看到:

ELF Header:Magic:   7f 45 4c 46 ...Class:                             ELF64Data:                              little endianType:                              DYNMachine:                           Advanced Micro Devices X86-64Entry point address:               0x1070

最经典的就是:

7f 45 4c 46

后面三个字节对应:

E L F

再看看 ELF 里面有哪些 Section:

readelf -S elf_demo_stripped

常见会看到:

.text.rodata.data.bss.dynsym.dynstr

简单理解:

.text程序机器指令.rodata字符串、常量、错误信息.data已初始化全局数据.bss未初始化全局数据.dynsym / .dynstr动态链接相关信息

这一步已经能看出整个程序的大体布局。


04|strings:密码一条命令就出来了

下面是最值得注意的一步。

执行:

strings -a elf_demo_stripped

为了快速过滤:

strings -a elf_demo_stripped | \grep -E "EdgeAI|Access|Usage"

结果很可能直接出现:

EdgeAI-2026!Usage: %s <password>Access grantedAccess denied

这里最关键的是:

EdgeAI-2026!

密码直接出来了。

甚至还没用:

IDAGhidraGDBobjdump

一个 strings 就够了。

这也是很多嵌入式项目最常见的问题。

如果代码里这样写:

const char *wifi_password = "12345678";const char *aes_key =    "0123456789abcdef0123456789abcdef";const char *server =    "https://api.company.com";const char *token =    "abcdef123456";

这些内容只要最终以明文进入 ELF:

strings application

就可能直接被找到。

所以:

不要把密码、Token、AES Key 直接写死在普通 C/C++ 程序里,然后认为“编译以后别人就看不到”。


05|继续定位:密码其实就在 .rodata 里

继续执行:

objdump -s \    -j .rodata \    elf_demo_stripped

可能看到:

Contents of section .rodata:2000 01000200 45646765 41492d32 303236212010 00557361 67653a20 2573203c 706173732020 776f7264 3e0a0041 63636573 732067722030 616e7465 64004163 63657373 2064656e2040 69656400

看这一段:

45 64 67 65 41 49 2d 32 30 32 36 21

对应 ASCII:

E d g e A I - 2 0 2 6 !

也就是:

EdgeAI-2026!

甚至还能直接找到它在文件中的偏移:

grep -oba \    'EdgeAI-2026!' \    elf_demo_stripped

例如输出:

8196:EdgeAI-2026!

也就是说:

这个密码就在 ELF 某个 Offset 附近明文躺着。

所以有一个很实用的安全判断:

如果一个 Secret 能在普通 ELF 的 .rodata 中直接找到,就不要再把它当成真正受保护的 Secret。


06|nm:没 Strip 时,函数名字都可能直接暴露

先看调试版本:

nm -C elf_demo_dbg | \grep -E 'verify_password| main$'

可能看到:

000000000000118d T main0000000000001159 t verify_password

函数名字:

verify_password

直接暴露。

如果真实产品中出现:

decrypt_modelverify_licensecheck_passwordgenerate_keydecrypt_firmwareverify_signature

这对于逆向分析人员来说非常有价值。

因为函数名字本身就在告诉他:

关键代码在哪里。

这也是为什么 Release 版本通常应该执行:

strip application

Strip 以后:

nm elf_demo_stripped

可能得到:

nm: elf_demo_stripped: no symbols

到这里确实好了很多。

但并没有结束。


07|Strip 以后,仍然能知道程序调用了什么

继续:

nm -D elf_demo_stripped

仍然可能看到:

U printf@GLIBC_2.2.5U puts@GLIBC_2.2.5U strcmp@GLIBC_2.2.5

最值得注意的是:

strcmp

这已经告诉我们:

程序里有字符串比较。

再结合之前找到的:

EdgeAI-2026!Access grantedAccess denied

程序逻辑几乎已经能够猜出来:

输入 Password     ↓   strcmp     ↓与固定字符串比较   ↓         ↓正确        错误 ↓           ↓granted     denied

这就是逆向分析很典型的过程。

不是一上来就“恢复整个源码”,而是通过:

String+Symbol+Import+Control Flow

逐渐把程序逻辑拼回来。


08|objdump:真正进入机器指令

下面开始反汇编:

objdump -d -Mintel \    elf_demo_dbg

定位:

verify_password

可能看到:

0000000000001159 <verify_password>:1159: push   rbp115a: mov    rbp,rsp1165: lea    rax,[rip+0xe98]1178: mov    rsi,rdx117b: mov    rdi,rax117e: call   1050 <strcmp@plt>1183: test   eax,eax1185: sete   al118b: ret

不看每条指令,直接看核心:

取某个字符串地址       ↓准备函数参数       ↓调用 strcmp       ↓判断返回值       ↓返回 True / False

已经基本可以推断出原始逻辑:

return strcmp(input, secret) == 0;

这就是一个很重要的事实:

函数名可以 Strip,变量名可以没有,但 CPU 真正需要执行的逻辑不能消失。

因为程序最终还要执行:

LoadCompareCallBranchReturn

这些机器指令。


09|Ghidra / IDA:进一步恢复接近 C 的逻辑

如果直接看汇编太累,就可以使用:

GhidraIDABinary Ninjaradare2

例如把:

elf_demo_stripped

导入 Ghidra。

典型分析过程:

Import ELF   ↓识别 x86-64   ↓Analyze   ↓找到字符串   ↓查看 XREF   ↓找到引用函数   ↓Decompiler

最终可能看到类似:

intFUN_00101159(char *param_1){    int ret;    ret = strcmp(        param_1,        "EdgeAI-2026!"    );    return ret == 0;}

当然,这不是原始源码。

比如原来的:

verify_password

可能已经变成:

FUN_00101159

原来的:

inputsecret

可能变成:

param_1local_10

原来的:

注释宏代码格式

基本恢复不了。

但最重要的:

程序在干什么

已经能够分析出来。

所以逆向工程真正追求的很多时候不是:

100% 恢复原始 C 代码。

而是:

理解核心算法和业务逻辑。


10|静态分析之外,还可以动态调试

前面的:

filereadelfstringsnmobjdumpGhidra

基本都属于:

静态分析。

也就是程序不用真正运行。

另一条路是:

动态分析。

常见工具:

GDBLLDBstraceltrace

比如 GDB 可以:

下断点单步运行查看寄存器查看栈查看内存查看函数参数

这又带来一个很现实的问题。

假设程序文件本身经过了加密:

Encrypted Data      ↓程序运行      ↓Decrypt      ↓Plaintext      ↓CPU 使用

那么在某个时刻:

明文 Key明文模型明文数据明文代码

就有可能需要出现在:

CPU RegisterRAMStackHeapProcess Address Space

所以:

静态文件看不到,不代表运行时也绝对看不到。

这也是为什么软件安全通常只能强调:

提高攻击成本。

而不是简单说:

绝对无法逆向。


11|PIE、Canary、RELRO 能不能防逆向?

前面我们还有一个:

elf_demo_hardened

编译方式:

gcc -O2 -s \    -fPIE -pie \    -fstack-protector-strong \    -D_FORTIFY_SOURCE=2 \    -Wl,-z,relro,-z,now \    -o elf_demo_hardened \    elf_demo.c

这里用了:

PIEStack CanaryFORTIFYRELROBIND_NOW

这些都是非常重要的 Linux Hardening。

但要分清楚:

这些东西主要解决的是:

内存破坏栈溢出地址预测GOT 修改漏洞利用

而不是:

隐藏密码隐藏字符串隐藏算法隐藏代码逻辑

继续执行:

strings -a elf_demo_hardened | \grep -E 'EdgeAI|Access|Usage'

很多情况下依然可以看到:

EdgeAI-2026!Access grantedAccess denied

所以应该把安全目标分成三类。

运行时攻击防护:

PIEASLRNXCanaryRELROFORTIFY

逆向分析防护:

StripObfuscationAnti-DebugPackingCode Encryption

Secret 保护:

Private KeyAES KeyModel KeyDevice Credential

这三类东西不要混在一起。


12|真实嵌入式产品最容易犯的几个错误

第一种:直接硬编码 Key。

例如:

static const char *aes_key =    "0123456789abcdef0123456789abcdef";

这是最常见的问题之一。

第二种:认为 Strip 以后就安全。

不是。

Strip=减少信息泄露

而不是:

Strip=无法逆向

第三种:认为“二进制别人看不懂”。

准确一点应该说:

二进制比源码难读,但仍然可以分析。

第四种:安全完全依赖一个本地判断。

例如:

if (license_valid())    run();else    exit();

如果整个产品安全性完全依赖:

攻击者找不到这个判断

那这个设计本身就不够稳。

第五种:把 Hardening 当成代码保密。

CanaryPIENXRELRO

非常重要。

但它们不是用来隐藏算法的。


13|真实产品应该怎么保护 ELF?

我更倾向于把二进制安全分成几层。

第一层:最基本的 Release 处理

至少做到:

去除 Debug 信息Strip-O2 / -O3减少敏感日志避免明显测试字符串不要硬编码 Secret

第二层:运行时 Hardening

继续做好:

PIEASLRNXStack CanaryRELROFORTIFY

这些是正常 Linux 产品很值得做的基础防护。

第三层:提高逆向成本

对于真正敏感的算法,可以考虑:

代码混淆字符串混淆控制流混淆Anti-DebugAnti-TamperPackingCode Encryption

但是这里要有一个现实认识:

这些手段主要是提高逆向成本,而不是让逆向绝对不可能。

第四层:真正的 Secret 不要只放普通 ELF

比如:

设备私钥模型解密 Key固件解密 Key长期身份凭据设备认证材料

最好不要简单写进普通应用。

更加安全的方向包括:

Secure ElementTPMTEEHSMHardware Root Key硬件 Key Ladder

真正的敏感材料应该尽量离开普通 Linux 用户空间。


14|最后总结:一个 ELF 到底能被看到多少?

把这次实验完整串起来:

C Source   ↓gcc   ↓ELF   ↓file   ↓知道 CPU / 位数 / ELF 类型   ↓readelf   ↓知道 ELF 内部结构   ↓strings   ↓直接找到密码   ↓nm   ↓找到函数名和动态调用   ↓objdump   ↓看到机器指令   ↓Ghidra / IDA   ↓恢复核心程序逻辑

整个过程没有什么特别神秘的地方。

对于一个交付出去的 ELF,我觉得更合理的安全假设应该是:

攻击者迟早能够拿到它,并且有能力理解其中相当一部分代码。

所以不要把安全建立在:

“别人看不到我的程序。”

这个前提上。

更应该考虑:

即使别人看到了字符串怎么办?即使别人知道算法怎么办?即使函数逻辑被分析出来怎么办?真正的 Key 放在哪里?License 是否只是一个简单本地判断?程序被修改后能不能发现?

最后可以把 ELF 二进制安全浓缩成几句话:

编译,不等于加密。

Strip,不等于不可逆向。

签名,主要解决防篡改,不解决内容保密。

Hardening,主要解决漏洞利用,不等于算法隐藏。

真正需要重点保护的,始终应该是:

密钥、身份凭据、核心数据和真正的安全边界。

而不是寄希望于:

“我已经把源码编译成 ELF 了。”

相关学习资料