workspace/tags/pwn/

Pwn

  • 1-0 Stack_overflow

    栈溢出基础

    前面的文章分别介绍了寄存器、汇编、栈帧与调用约定 现在我们把它们串起来:如果程序存在栈缓冲区越界,攻击者能把程序带到哪里去?

    这篇文章的目标是让你亲手完成一次最简栈溢出——覆盖返回地址,让程序跳到不该执行的地方。读完本文后,你将能够自己编译漏洞程序、用 GDB 定位偏移、用 Python 构造输入,为后续的 ret2text、ret2shellcode 等专项文章做好准备

    什么是栈溢出

    当程序向栈上的局部数组写入超过其容量的数据时,多出的部分会覆盖相邻内存。如果覆盖范围到达保存的返回地址,ret 指令就会取到一个由攻击者控制的值,从而跳转到任意地址。

    回顾计算机组成中的栈帧布局:

    高地址
    +-------------------------+
    | 返回地址                 |  由 call 压入
    +-------------------------+
    | 调用者保存的 RBP         |
    +-------------------------+
    | 局部变量、数组、填充      |  ← 数组从这里开始,向高地址方向写入
    +-------------------------+
    低地址
    

    C 语言不检查数组边界。如果 buffer[32] 的写入没有长度限制,多出的字节就会沿栈"向上"覆盖,依次破坏局部变量、旧 RBP,最终触及返回地址。一旦 ret 执行,CPU 就会跳到攻击者选定的位置。

    演示程序

    这里提供一个demo

    点击查看
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    /*
     * win — 正常情况下不会被调用的函数
     * 溢出利用的目标就是让程序"意外"进入这里。
     */
    void win(void)
    {
        printf("\n=================================\n");
        printf("  你成功了!程序跳转到了 win 函数。\n");
        printf("=================================\n\n");
        exit(0);
    }
    
    /*
     * vulnerable — 包含栈溢出漏洞的函数
     *
     * gets 不检查目标缓冲区大小,超出 buffer[32] 的输入
     * 会持续写入栈上更高地址,可能覆盖:
     *   - 其他局部变量
     *   - 保存的 RBP
     *   - 返回地址
     */
    void vulnerable(void)
    {
        char buffer[32];
    
        printf("请输入一些文本:");
    
        /* gets 不会限制读取长度 */
        gets(buffer);
    
        printf("你输入了:%s\n", buffer);
    }
    
    /*
     * print_info — 打印可用于偏移计算的信息
     */
    void print_info(void)
    {
        printf("-----------------\n");
        printf("win() 地址:%p\n", (void *)win);
        printf("-----------------\n\n");
    }
    
    int main(int argc, char **argv)
    {
        print_info();
        vulnerable();
    
        printf("\n程序正常退出。\n");
        return 0;
    }
    

    vulnerable 中 gets 会一直读取直到换行,完全不管 buffer 只有 32 字节。win 是一个正常情况下永远不会被调用的函数,我们的目标就是通过溢出覆盖返回地址,让程序"意外"进入 win

  • 0-0 计算机组成初步

    从 C 语言到汇编:二进制安全需要的计算机组成基础

    我们平时编写 C 语言程序时,习惯用变量、循环、判断和函数来描述程序逻辑:

    int add(int a, int b) {
        return a + b;
    }
    

    但是 CPU 并不认识 intreturn 或函数名。源代码经过编译器处理后,会被翻译成机器指令;机器指令本质上是一串二进制数据。汇编语言则为这些机器指令提供了便于人类阅读的写法。上面的函数可能对应如下汇编逻辑:

    add:
        lea eax, [rdi + rsi]
        ret
    

    对于普通软件开发,我们未必需要关心翻译后的结果;但在逆向工程、漏洞分析和恶意代码分析中,拿到的往往只有编译后的程序。此时,我们必须从寄存器、内存和汇编指令出发,重新理解程序做了什么。

    本文不会完整讲授数字电路、流水线或缓存一致性等计算机组成原理课程内容,而是围绕二进制安全所需的主线展开:

    硬件如何协作 → CPU 如何保存数据 → 汇编指令如何操作数据和改变控制流 → 函数如何使用栈与寄存器。

    文中的汇编代码采用 x86-64 Intel 语法,运行环境默认为 Linux 用户态,函数调用遵循 System V AMD64 ABI。示例以讲清原理为第一目标,实际编译结果会随着编译器版本、优化等级和源代码写法发生变化。

    计算机由哪些主要硬件组成

    一台计算机包含很多部件。对于分析普通用户态程序,我们首先需要认识 CPU、内存、外部存储、总线和输入输出设备。

    部件主要作用与二进制分析的关系
    CPU解释并执行机器指令,完成运算和控制汇编指令最终在 CPU 中执行
    内存(RAM)临时保存正在运行的程序、代码和数据代码、栈、堆和全局变量都位于进程的虚拟地址空间中
    硬盘或固态硬盘长期保存程序和文件,断电后数据仍存在ELF 可执行文件最初存放在这里,运行时再由系统装入内存
    总线在 CPU、内存和设备之间传输地址、数据和控制信息决定各硬件如何交换信息,但初学阶段不必深入电气细节
    输入输出设备让计算机与用户和外界交换数据程序通常通过操作系统提供的接口访问这些设备

    1. CPU 内部有什么

    为了便于理解,可以把 CPU 内部的重要部分概括为:

Terminal

C0nvR3 Lab terminal ready. Type help for commands.