二进制拆弹专家实验(lab3 Phase_5)

 

汇编代码分析

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,继续循环
  • 功能
    1. 遍历输入字符串的每个字符。
    2. 提取字符的低 4 位(0-15)作为索引。
    3. 从地址 0x4024b0 处的字符表中查找对应字符。
    4. 将查找结果存入栈上缓冲区(从 0x10(%rsp) 开始)。
    5. 循环 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                     ; 返回
  • 功能:验证栈完整性(金丝雀值),释放栈空间,恢复寄存器,返回。

关键数据结构

  1. 字符表(地址 0x4024b0

    • 内容:"maduiersnfotvbyl"
    • 作用:根据输入字符的低 4 位索引,查找对应的转换字符。
  2. 目标字符串(地址 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;
}

问题分析总结(勿抄主包 防重)

  1. 忘记的陌生的汇编指令

push 指令

push 是汇编语言中的常用指令,主要用于将数据压入堆栈(Stack)。堆栈是一种遵循 后进先出(LIFO) 原则的内存区域,常用于函数调用、保存寄存器值、传递参数等场景。

一、基本语法与功能

语法

push 操作数  ; 将操作数压入堆栈顶部

功能

  1. 将指定的操作数(如寄存器、内存数据、立即数等)存入堆栈的顶部。
  2. 堆栈指针(SP,Stack Pointer)会自动调整:
    • 在 x86 架构 中(如 32 位或 64 位模式),堆栈向低地址增长,因此执行 push 后,SP 会减少相应的字节数(如 32 位下减 4,64 位下减 8)。

寄存器

  • 通用寄存器:如 eax(32 位)、rax(64 位)、ebxrsp 等。
    push eax  ; 将eax的值压入堆栈
    
  • 段寄存器(如 dses):仅在特定模式下可用。

内存操作数

  • 通过地址访问内存中的数据并压栈。
    push [ebp+8]  ; 将ebp+8地址处的值压入堆栈(假设为32位)
    

立即数

  • 直接压入常量值(部分架构支持,如 x86 的 32 位 / 64 位模式)。
    push 0x1234  ; 将十六进制数0x1234压入堆栈
    

函数调用时保存现场

在调用函数前,压入当前寄存器的值(如 ebpesi 等),防止被函数修改。

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 位寄存器(如 eaxedx)。

功能

  1. 读取源操作数的 8 位数据。
  2. 在高位补 0,扩展为 32 位值。
  3. 将扩展后的 32 位值存入目标寄存器。

三、示例

  1. 从寄存器加载并扩展

    mov bl, 0xAB      ; bl = 1010 1011 (8位)
    movzbl bl, eax    ; eax = 0000 0000 0000 0000 0000 0000 1010 1011 (32位)
    
  2. 从内存加载并扩展

    movzbl [var], edx ; 假设[var]地址处的字节为0xCD
                      ; edx = 0000 0000 0000 0000 0000 0000 1100 1101
    

四、与其他指令的对比

  1. movzbw

    • 将 8 位扩展为 16 位(Zero-extend Byte to Word)。
    mov bl, 0xFF
    movzbw bl, cx     ; cx = 0x00FF (16位)
    
  2. 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)
    

五、应用场景

  1. 处理字节数据

    • 当需要将单个字节作为无符号整数参与 32 位运算时。
    ; 计算数组中第一个字节与第二个字节的和(无符号)
    movzbl [array], eax    ; 加载第一个字节并零扩展
    movzbl [array+1], ebx  ; 加载第二个字节并零扩展
    add eax, ebx           ; 32位加法(避免符号扩展带来的错误)
    
  2. 访问结构体字段

    • 当结构体包含 8 位字段,需要将其作为 32 位值处理时。
    ; 假设struct_addr指向一个结构体,其中第一个字节是count
    movzbl [struct_addr], ecx  ; 将count字段作为无符号32位值加载
    

六、注意事项

  1. 目标操作数必须是 32 位寄存器

    • 不能直接存入内存地址或其他大小的寄存器。
  2. 零扩展与符号扩展的选择

    • 若源数据是有符号数,需使用 movsbl 保留符号位。
    • 若源数据是无符号数,必须使用 movzbl 避免符号扩展导致的错误。
  3. 64 位模式下的变体

    • 在 64 位模式下,可使用 movzbq 将 8 位零扩展为 64 位。
    mov bl, 0xAB
    movzbq bl, rax    ; rax = 0x00000000000000AB (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 函数中,这一步的目的是:

  1. 从输入字符中提取索引值
    假设输入的字符是 '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. **实现字符到字符表的映射**,这是解密炸弹密码的关键逻辑之一
  1. 金丝雀值

金丝雀值(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 终止程序。

 

  1. 寄存器结构详解

在 x86-64 架构中,通用寄存器(如 RAXRBXRCX 等)的结构分层如下:

寄存器名称位数描述
RCX64 位完整的 64 位寄存器
ECX32 位RCX 的低 32 位(RCX[31:0]
CX16 位ECX 的低 16 位(ECX[15:0]
CH8 位CX 的高 8 位(CX[15:8]
CL8 位CX 的低 8 位(CX[7:0]

与其他寄存器的对比

寄存器位数用途
%al8 位RAX 的低 8 位(常用于单字节操作)
%ah8 位RAX 的高 8 位(已较少使用)
%ax16 位RAX 的低 16 位
%eax32 位RAX 的低 32 位
%rax64 位完整的 64 位寄存器

例如,mov %al,%bl 表示将 RAX 的低 8 位复制到 RBX 的低 8 位。

 

  1. 步骤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 位整数,避免符号扩展带来的错误。

  1. 步骤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 指令的对比

指令形式编码长度说明
nop1 字节最基本的空操作指令,编码为 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) 是一种 多字节空操作指令,通过复杂的内存寻址语法实现,但实际不执行任何操作。其主要目的是 填充代码空间,确保指令对齐或预留调试位置。在逆向分析中,这类指令通常可被忽略,不影响对程序核心逻辑的理解。

 

  1. 步骤   0x0(%rax,%rax,1)  ; //空操作(占位)

  2. 在汇编代码中,0x0(%rax,%rax,1) 是一种内存寻址方式,但在此处它被用作 占位指令(NOP)。这种写法虽然看似指向内存访问,但实际上是编译器或开发者刻意构造的 无实际操作的指令,主要用于以下目的:

    1. 指令格式与内存寻址

    寻址方式拆解

    assembly

    0x0(%rax,%rax,1)
    
  3. 计算规则:有效地址 = 0x0 + %rax + %rax*1 = 2*%rax
  4. 正常用途:若用于内存访问(如 mov (%rax,%rax,1), %rbx),则从地址 2*%rax 读取数据。
  5. 但在此处nopl 0x0(%rax,%rax,1) 中的 nopl 是 空操作指令,不会真正访问内存。
  6. 普通 NOP:标准的 nop 指令通常编码为单字节 0x90
  7. 多字节 NOP:复杂寻址方式会生成更长的指令编码(如 8 字节),用于填充特定长度的内存空间。
  8. 代码对齐:某些架构或编译器要求函数起始地址按 16 字节对齐,用多字节 NOP 填充可满足对齐要求。
  9. 调试占位:预留空间以便后续插入代码,而无需重新编译整个程序。
  10. 反调试 / 混淆:用看似有意义的指令(如内存寻址)伪装成空操作,增加逆向工程难度。
  11. 位置:位于 callq explode_bomb 之后,函数返回前。
  12. 作用:可能是为了保持代码块的对齐,或在调试版本中预留空间,方便后续插入断点或日志代码。
  13. 无害的空操作:不影响程序逻辑,可在分析时忽略。
  14. 代码结构标记:可能暗示此处曾有代码修改,或为未来扩展预留。
  15. 编译优化产物:编译器为满足对齐或指令流水线要求而插入的填充指令。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值