汇编代码分析
1. 函数初始化与前置检查
401062: 53 push %rbx ; 保存rbx寄存器(压栈)
401063: 48 83 ec 20 sub $0x20,%rsp ; 分配32字节栈空间
401067: 48 89 fb mov %rdi,%rbx ; 将第一个参数(输入字符串)存入rbx
40106a: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax ; 加载金丝雀值(用于栈保护)
401073: 48 89 44 24 18 mov %rax,0x18(%rsp) ; 保存金丝雀值到栈上
401078: 31 c0 xor %eax,%eax ; 清空eax寄存器(初始化为0)
40107a: e8 9c 02 00 00 callq 40131b <string_length> ; 调用函数计算输入字符串长度
40107f: 83 f8 06 cmp $0x6,%eax ; 比较长度是否为6
401082: 74 4e je 4010d2 <phase_5+0x70> ; 若等于6,跳转到4010d2
401084: e8 b1 03 00 00 callq 40143a <explode_bomb> ; 否则触发炸弹
401089: eb 47 jmp 4010d2 <phase_5+0x70> ; 跳转到4010d2
- 功能:初始化函数环境,检查输入字符串长度是否为 6,否则爆炸。
2. 主循环(核心逻辑)
40108b: 0f b6 0c 03 movzbl (%rbx,%rax,1),%ecx ; 加载输入字符串的第rax个字符(零扩展为32位)
40108f: 88 0c 24 mov %cl,(%rsp) ; 将字符存入栈顶
401092: 48 8b 14 24 mov (%rsp),%rdx ; 读取字符到rdx
401096: 83 e2 0f and $0xf,%edx ; 取字符的低4位(掩码0xF),即为1-15的索引值
401099: 0f b6 92 b0 24 40 00 movzbl 0x4024b0(%rdx),%edx ; 以低4位为索引,从0x4024b0表中查找对应字符
4010a0: 88 54 04 10 mov %dl,0x10(%rsp,%rax,1) ; 将查表结果存入栈上缓冲区(0x10(%rsp)+rax)
4010a4: 48 83 c0 01 add $0x1,%rax ; rax加1(循环计数器)
4010a8: 48 83 f8 06 cmp $0x6,%rax ; 比较计数器是否达到6
4010ac: 75 dd jne 40108b <phase_5+0x29> ; 若不等于6,继续循环
- 功能:
- 遍历输入字符串的每个字符。
- 提取字符的低 4 位(0-15)作为索引。
- 从地址
0x4024b0
处的字符表中查找对应字符。 - 将查找结果存入栈上缓冲区(从
0x10(%rsp)
开始)。 - 循环 6 次,处理完所有字符。
3. 结果校验
4010ae: c6 44 24 16 00 movb $0x0,0x16(%rsp) ; 在缓冲区末尾添加字符串结束符('\0')
4010b3: be 5e 24 40 00 mov $0x40245e,%esi ; 将目标字符串地址(0x40245e)存入esi
4010b8: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi ; 将转换后的字符串地址存入rdi
4010bd: e8 76 02 00 00 callq 401338 <strings_not_equal> ; 调用函数比较两字符串
4010c2: 85 c0 test %eax,%eax ; 检查比较结果
4010c4: 74 13 je 4010d9 <phase_5+0x77> ; 若两字符串相等,跳转到4010d9(通过)
4010c6: e8 6f 03 00 00 callq 40143a <explode_bomb> ; 否则触发炸弹
4010cb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1) ; 空操作(占位)
4010d0: eb 07 jmp 4010d9 <phase_5+0x77> ; 跳转到4010d9
- 功能:比较转换后的字符串与目标字符串(地址
0x40245e
)是否相同,不同则爆炸。
4. 收尾工作
4010d2: b8 00 00 00 00 mov $0x0,%eax ; 将eax置0(可能是循环初始化)
4010d7: eb b2 jmp 40108b <phase_5+0x29> ; 跳转到循环开始处(40108b)
4010d9: 48 8b 44 24 18 mov 0x18(%rsp),%rax ; 恢复金丝雀值到rax
4010de: 64 48 33 04 25 28 00 xor %fs:0x28,%rax ; 验证金丝雀值(防栈溢出)
4010e7: 74 05 je 4010ee <phase_5+0x8c> ; 若验证通过,继续
4010e9: e8 42 fa ff ff callq 400b30 <__stack_chk_fail@plt> ; 否则调用栈检查失败函数
4010ee: 48 83 c4 20 add $0x20,%rsp ; 释放栈空间
4010f2: 5b pop %rbx ; 恢复rbx寄存器
4010f3: c3 retq ; 返回
- 功能:验证栈完整性(金丝雀值),释放栈空间,恢复寄存器,返回。
关键数据结构
-
字符表(地址
0x4024b0
)- 内容:
"maduiersnfotvbyl"
- 作用:根据输入字符的低 4 位索引,查找对应的转换字符。
- 内容:
-
目标字符串(地址
0x40245e
)- 内容:
"flyers"
- 作用:校验转换后的字符串是否匹配。
- 内容:
答案
gdb查x/s 0x40245e
等效代码(C)
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// 模拟炸弹爆炸函数
void explode_bomb() {
printf("BOOM!\n");
exit(1);
}
// 字符串长度函数
size_t string_length(const char* str) {
size_t len = 0;
while (str[len]) len++;
return len;
}
// 字符串比较函数
int strings_not_equal(const char* s1, const char* s2) {
return strcmp(s1, s2) != 0;
}
void phase_5(const char* input) {
// 栈保护 - 模拟金丝雀值
unsigned long canary = 0x12345678; // 实际值由系统随机生成
// 检查输入长度是否为6
if (string_length(input) != 6) {
explode_bomb();
}
// 字符表 - 对应地址0x4024b0
const char* char_table = "maduiersnfotvbyl";
// 目标字符串 - 对应地址0x40245e
const char* target = "flyers";
// 用于存储转换后的字符
char buffer[7]; // 6个字符 + 终止符
// 主循环 - 处理每个字符
for (int i = 0; i < 6; i++) {
// 提取字符的低4位作为索引
int index = input[i] & 0xF;
// 从字符表中查找对应字符
buffer[i] = char_table[index];
}
// 添加字符串终止符
buffer[6] = '\0';
// 比较转换后的字符串与目标字符串
if (strings_not_equal(buffer, target)) {
explode_bomb();
}
// 验证金丝雀值(栈保护)
if (canary != 0x12345678) {
explode_bomb();
}
}
int main() {
char input[100];
printf("Enter the password: ");
scanf("%99s", input);
phase_5(input);
printf("Congratulations! You defused the bomb!\n");
return 0;
}
问题分析总结(勿抄主包 防重)
-
忘记的陌生的汇编指令
push 指令
push
是汇编语言中的常用指令,主要用于将数据压入堆栈(Stack)。堆栈是一种遵循 后进先出(LIFO) 原则的内存区域,常用于函数调用、保存寄存器值、传递参数等场景。
一、基本语法与功能
语法:
push 操作数 ; 将操作数压入堆栈顶部
功能:
- 将指定的操作数(如寄存器、内存数据、立即数等)存入堆栈的顶部。
- 堆栈指针(SP,Stack Pointer)会自动调整:
- 在 x86 架构 中(如 32 位或 64 位模式),堆栈向低地址增长,因此执行
push
后,SP 会减少相应的字节数(如 32 位下减 4,64 位下减 8)。
- 在 x86 架构 中(如 32 位或 64 位模式),堆栈向低地址增长,因此执行
寄存器
- 通用寄存器:如
eax
(32 位)、rax
(64 位)、ebx
、rsp
等。push eax ; 将eax的值压入堆栈
- 段寄存器(如
ds
、es
):仅在特定模式下可用。
内存操作数
- 通过地址访问内存中的数据并压栈。
push [ebp+8] ; 将ebp+8地址处的值压入堆栈(假设为32位)
立即数
- 直接压入常量值(部分架构支持,如 x86 的 32 位 / 64 位模式)。
push 0x1234 ; 将十六进制数0x1234压入堆栈
函数调用时保存现场
在调用函数前,压入当前寄存器的值(如 ebp
、esi
等),防止被函数修改。
call func ; 调用函数前,自动压入返回地址(IP/EIP/RIP)
传递参数
通过堆栈向函数传递参数(如 C 语言的栈传递方式)。
push arg2 ; 先压入参数2
push arg1 ; 再压入参数1(堆栈逆序存储)
call func ; 函数中通过栈指针访问参数
临时存储数据
在程序中临时保存变量值,后续通过 pop
指令取出。
push ebx ; 保存ebx原值
; 执行其他操作
pop ebx ; 恢复ebx原值
xor 异或 相同为1不同为0
XOR
指令是计算机底层编程中的基础操作,具有高效、灵活的特点。其核心应用包括清 0 操作、值交换、位操作及简单加密。理解异或运算的数学特性(如自反性)是掌握这些应用的关键。在性能敏感的代码中,合理使用 XOR
可提升程序效率
movzbl
movzbl
是处理字节到 32 位无符号整数转换的关键指令,通过零扩展确保高位补 0。它在处理无符号数据、结构体访问和避免符号扩展错误等场景中广泛应用。理解其与 movsbl
的差异是正确进行数据类型转换的基础
一、指令名称解析
- mov:基本的数据传送操作
- z:Zero-extend(零扩展)
- b:Byte(字节,8 位)
- l:Long(长字,32 位)
完整含义:将一个字节(8 位)数据零扩展为 32 位后存入目标寄存器。
二、语法与功能
语法:
movzbl 源操作数, 目标操作数
- 源操作数:必须是8 位寄存器或内存地址(如
al
,[ebp+8]
)。 - 目标操作数:必须是32 位寄存器(如
eax
,edx
)。
功能:
- 读取源操作数的 8 位数据。
- 在高位补 0,扩展为 32 位值。
- 将扩展后的 32 位值存入目标寄存器。
三、示例
-
从寄存器加载并扩展
mov bl, 0xAB ; bl = 1010 1011 (8位) movzbl bl, eax ; eax = 0000 0000 0000 0000 0000 0000 1010 1011 (32位)
-
从内存加载并扩展
movzbl [var], edx ; 假设[var]地址处的字节为0xCD ; edx = 0000 0000 0000 0000 0000 0000 1100 1101
四、与其他指令的对比
-
movzbw
- 将 8 位扩展为 16 位(Zero-extend Byte to Word)。
mov bl, 0xFF movzbw bl, cx ; cx = 0x00FF (16位)
-
movsbl
- 符号扩展(保留符号位)而非零扩展。
mov bl, 0xFF ; bl = 1111 1111 (-1的补码) movsbl bl, eax ; eax = 1111 1111 1111 1111 1111 1111 1111 1111 (-1) movzbl bl, ebx ; ebx = 0000 0000 0000 0000 0000 0000 1111 1111 (255)
五、应用场景
-
处理字节数据
- 当需要将单个字节作为无符号整数参与 32 位运算时。
; 计算数组中第一个字节与第二个字节的和(无符号) movzbl [array], eax ; 加载第一个字节并零扩展 movzbl [array+1], ebx ; 加载第二个字节并零扩展 add eax, ebx ; 32位加法(避免符号扩展带来的错误)
-
访问结构体字段
- 当结构体包含 8 位字段,需要将其作为 32 位值处理时。
; 假设struct_addr指向一个结构体,其中第一个字节是count movzbl [struct_addr], ecx ; 将count字段作为无符号32位值加载
六、注意事项
-
目标操作数必须是 32 位寄存器
- 不能直接存入内存地址或其他大小的寄存器。
-
零扩展与符号扩展的选择
- 若源数据是有符号数,需使用
movsbl
保留符号位。 - 若源数据是无符号数,必须使用
movzbl
避免符号扩展导致的错误。
- 若源数据是有符号数,需使用
-
64 位模式下的变体
- 在 64 位模式下,可使用
movzbq
将 8 位零扩展为 64 位。
mov bl, 0xAB movzbq bl, rax ; rax = 0x00000000000000AB (64位)
- 在 64 位模式下,可使用
按位与(and)
在汇编代码中,and $0xf,%edx
这一步执行的是按位与运算,其作用是提取 %edx
寄存器中低 8 位的最后 4 位(即二进制的低 4 位),同时将高 4 位清零。这是一个常见的位操作技巧,用于截取或过滤特定位置的二进制值。
按位与运算的原理
按位与(AND)运算的规则是:对应位都为 1 时结果为 1,否则为 0。
例如:
0110 1101 (数值109)
AND 0000 1111 (掩码0xF,即15)
------------
0000 1101 (结果13,即0xD)
掩码 0xF
的二进制形式为 0000 1111
,它的作用是:
- 保留低 4 位:无论原数的低 4 位是什么,与
0xF
与运算后都会保留原值。 - 清零高 4 位:无论原数的高 4 位是什么,与
0xF
与运算后都会变为 0。
在代码中的具体作用
在炸弹游戏的 phase_5
函数中,这一步的目的是:
- 从输入字符中提取索引值:
假设输入的字符是'A'
(ASCII 码为0x41
,即二进制0100 0001
),执行and $0xf,%edx
后:0100 0001 ('A'的ASCII码)
AND 0000 1111 (掩码 0xF)
0000 0001 (结果为 1)
此时 `%edx` 的值变为 `1`,表示从字符表中取第1个字符(索引从0开始)。
2. **限制索引范围**:
通过掩码 `0xF`,确保最终的索引值始终在 **0~15** 之间(对应字符表的有效范围)。即使输入的字符ASCII码超过15,经过与运算后也会被映射到合法索引。
### **为什么要提取低4位?**
在这个炸弹游戏中,程序设计的规则是:
- **输入的每个字符**通过低4位映射到一个**字符表索引**。
- **字符表**(地址 `0x4024b0`)包含16个字符:`"maduiersnfotvbyl"`。
- 例如:
- 字符 `'A'`(低4位为 `0001`)→ 映射到索引1 → 对应字符表中的 `'a'`。
- 字符 `'9'`(低4位为 `1001`)→ 映射到索引9 → 对应字符表中的 `'f'`。
通过这种方式,程序将输入的字符串转换为另一个字符串(如 `"flyers"`),并进行校验。
### **总结**
`and $0xf,%edx` 这一步的核心作用是:
1. **提取输入字符的低4位**,将其转换为0~15之间的索引值。
2. **确保索引合法**,避免访问越界。
3. **实现字符到字符表的映射**,这是解密炸弹密码的关键逻辑之一
-
金丝雀值
金丝雀值(Canary Value) 是一种用于防御缓冲区溢出攻击的安全机制,常见于计算机程序的内存保护中。其核心思想是在栈帧中关键位置(如返回地址之前)插入一个特殊的检测值,当发生缓冲区溢出时,该值会被篡改,从而触发程序报错或终止,避免恶意代码执行。
起源与名称由来
- 名称来源:灵感来自煤矿工人用金丝雀检测瓦斯泄漏的做法。金丝雀对瓦斯敏感,一旦环境危险会先死亡,作为预警信号。
- 技术目的:在缓冲区溢出攻击中,攻击者常通过覆盖栈中的返回地址来劫持程序执行流。金丝雀值的作用是在返回地址被篡改前发出 “预警”,阻止攻击。
- 插入位置:
在编译阶段,编译器会在函数的栈帧中,位于缓冲区(如数组)和返回地址之间插入金丝雀值。例如:
栈内存布局(示例):
┌───────────────┐
│ 局部变量 │ (缓冲区,如char buf[16])
├───────────────┤
│ 金丝雀值 │ (Canary Value)
├───────────────┤
│ 返回地址 │ (Return Address)
└───────────────┘
运行时检测:
- 函数返回前,编译器会自动生成代码检查金丝雀值是否被修改。
- 如果值被篡改(说明发生了缓冲区溢出),程序会立即终止(如调用
__stack_chk_fail
函数),防止攻击者利用溢出执行恶意代码。
随机性:
- 金丝雀值通常由操作系统在进程启动时随机生成(如基于
fs
寄存器或gs
寄存器的值),每次运行程序时值不同,避免攻击者预测或猜测。
与栈保护的关系:
- 金丝雀值是 栈保护技术(Stack Guard,又名 Canary Protection) 的核心实现。在 GCC 编译器中,可通过
-fstack-protector
系列选项启用(如-fstack-protector-all
强制为所有函数插入保护)。
局限性:
- 无法防御所有类型的攻击(如精确计算溢出长度绕过金丝雀值)。
- 对堆溢出(Heap Overflow)无效,需结合其他防护机制(如 ASLR、DEP/NX 等)。
压入金丝雀值:
40106a: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax ; 从fs段寄存器读取金丝雀值
401073: 48 89 44 24 18 mov %rax,0x18(%rsp) ; 将值存入栈帧(通常位于返回地址附近)
%fs:0x28
是 Linux 中存储金丝雀值的常见位置(Windows 常用 %gs:0x14
)。
通过异或操作检查金丝雀值是否被篡改,若结果不为零(值被修改),则调用 __stack_chk_fail
终止程序。
-
寄存器结构详解
在 x86-64 架构中,通用寄存器(如 RAX
, RBX
, RCX
等)的结构分层如下:
寄存器名称 | 位数 | 描述 |
---|---|---|
RCX | 64 位 | 完整的 64 位寄存器 |
ECX | 32 位 | RCX 的低 32 位(RCX[31:0] ) |
CX | 16 位 | ECX 的低 16 位(ECX[15:0] ) |
CH | 8 位 | CX 的高 8 位(CX[15:8] ) |
CL | 8 位 | CX 的低 8 位(CX[7:0] ) |
与其他寄存器的对比
寄存器 | 位数 | 用途 |
---|---|---|
%al | 8 位 | RAX 的低 8 位(常用于单字节操作) |
%ah | 8 位 | RAX 的高 8 位(已较少使用) |
%ax | 16 位 | RAX 的低 16 位 |
%eax | 32 位 | RAX 的低 32 位 |
%rax | 64 位 | 完整的 64 位寄存器 |
例如,mov %al,%bl
表示将 RAX
的低 8 位复制到 RBX
的低 8 位。
-
步骤movzbl 0x4024b0(%rdx),%edx ; //以低4位为索引,从0x4024b0表中查找对应字符
这一步是通过 内存间接寻址 和 零扩展 实现查表操作的。
指令拆解
movzbl 0x4024b0(%rdx),%edx
关键组成部分:
movzbl
:
- 功能:从内存读取 1 字节数据,并 零扩展为 32 位 后存入目标寄存器。
- 示例:若内存值为
0xAB
(8 位),则扩展后%edx = 0x000000AB
(32 位)。
0x4024b0(%rdx)
:
- 内存寻址方式:基址 + 偏移量。
- 计算逻辑:有效地址 = 基址
0x4024b0
+ 偏移量%rdx
。 - 这里的
%rdx
存储的是 前一步通过and $0xf,%edx
计算得到的索引值(范围 0~15)。 - 字符表基址:
0x4024b0
(存储字符串"maduiersnfotvbyl"
)。 - 索引值:
%rdx = 9
(对应字符'f'
)。 - 地址
0x4024B9
处存储的字符是 字符表中的第 9 个字符(索引从 0 开始)。
字符表内容:
索引 0 1 2 3 4 5 6 7 8 9 ...
字符 'm''a''d''u''i''e''r''s''n''f'...
因此,0x4024B9
处的值为 'f'
(ASCII 码 0x66
)。
movzbl
将读取的 1 字节数据 0x66
零扩展为 32 位 0x00000066
。
最终 %edx = 0x00000066
(即字符 'f'
的 ASCII 码)。
输入字符处理:
例如,输入字符 '9'
(ASCII 码 0x39
)。
通过 and $0xf,%edx
提取低 4 位,得到 索引值 9。
查表转换:
索引 9 对应字符表中的 'f'
。
因此,输入字符 '9'
被转换为 'f'
。
基址 0x4024b0
指向字符表的起始位置。
偏移量 %rdx
是输入字符的低 4 位(0~15)。
movzbl
确保读取的单字节数据被正确扩展为 32 位整数,避免符号扩展带来的错误。
-
步骤movb $0x0,0x16(%rsp) ; //在缓冲区末尾添加字符串结束符
movb $0x0,0x16(%rsp)
这行汇编代码的作用是在栈上的字符串缓冲区末尾手动添加一个 空字符(NULL,ASCII 码为 0),作为字符串的结束标志。这是 C 语言风格字符串的重要特性(以\0
结尾),用于后续的字符串比较操作。
2. 为什么用复杂的寻址方式实现 NOP?
指令编码与对齐需求:
示例场景:
3. 汇编代码中的实际用途
在你分析的代码中:
4010cb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1) ; 空操作(占位)
4. 与其他 NOP 指令的对比
指令形式 | 编码长度 | 说明 |
---|---|---|
nop | 1 字节 | 最基本的空操作指令,编码为 0x90 。 |
nopl (%eax) | 2 字节 | 使用简单寻址方式的空操作,编码为 0x66 0x90 。 |
nopl 0x0(%rax) | 3 字节 | 更长的空操作,编码包含寄存器和偏移量。 |
nopl 0x0(%rax,%rax,1) | 5 字节 | 本例中的复杂形式,用于填充更多空间。 |
5. 逆向工程中的识别
当看到类似 nopl 0x0(%reg,%reg,scale)
的指令时,可判断为:
总结
nopl 0x0(%rax,%rax,1)
是一种 多字节空操作指令,通过复杂的内存寻址语法实现,但实际不执行任何操作。其主要目的是 填充代码空间,确保指令对齐或预留调试位置。在逆向分析中,这类指令通常可被忽略,不影响对程序核心逻辑的理解。
-
步骤 0x0(%rax,%rax,1) ; //空操作(占位)
-
在汇编代码中,
0x0(%rax,%rax,1)
是一种内存寻址方式,但在此处它被用作 占位指令(NOP)。这种写法虽然看似指向内存访问,但实际上是编译器或开发者刻意构造的 无实际操作的指令,主要用于以下目的:1. 指令格式与内存寻址
寻址方式拆解:
assembly
0x0(%rax,%rax,1)
- 计算规则:有效地址 =
0x0 + %rax + %rax*1
=2*%rax
。 - 正常用途:若用于内存访问(如
mov (%rax,%rax,1), %rbx
),则从地址2*%rax
读取数据。 - 但在此处:
nopl 0x0(%rax,%rax,1)
中的nopl
是 空操作指令,不会真正访问内存。 - 普通 NOP:标准的
nop
指令通常编码为单字节0x90
。 - 多字节 NOP:复杂寻址方式会生成更长的指令编码(如 8 字节),用于填充特定长度的内存空间。
- 代码对齐:某些架构或编译器要求函数起始地址按 16 字节对齐,用多字节 NOP 填充可满足对齐要求。
- 调试占位:预留空间以便后续插入代码,而无需重新编译整个程序。
- 反调试 / 混淆:用看似有意义的指令(如内存寻址)伪装成空操作,增加逆向工程难度。
- 位置:位于
callq explode_bomb
之后,函数返回前。 - 作用:可能是为了保持代码块的对齐,或在调试版本中预留空间,方便后续插入断点或日志代码。
- 无害的空操作:不影响程序逻辑,可在分析时忽略。
- 代码结构标记:可能暗示此处曾有代码修改,或为未来扩展预留。
- 编译优化产物:编译器为满足对齐或指令流水线要求而插入的填充指令。