欢迎来到 嗅灵易学

零基础也能上手的脚本技术课,一对一答疑带你入门

[翻译]使用递归攻击未初始化的变量

[翻译]使用递归攻击未初始化的变量

概要

递归是一种计算机编程技术,它是指某个程序,子程序,函数或算法不断调用自身一次或多次,直到满足某个条件。人们一直在争论递归应该怎么使用,以及是否应该使用它。递归是每个程序员必备的技能。然而,它的不正确使用可能会导致使程序流变复杂,以及使代码产生漏洞。这也使得某些编程标准明确地禁止它的使用,以及禁止其他复杂的流程结构,比如说 goto 。总而言之,递归代码难以理解,从而导致程序出错。在这篇文章中,我会介绍递归,以及如何在漏洞研究中利用它。此外,我还会分享一个我使用中级中间语言写的跨平台的Binary Ninja 插件,用于在汇编代码中定位递归位置。

递归概要

递归可以分为两类,直接递归和间接递归。直接递归出现在程序调用自身的时候。直接递归的例子很多,比如数学算法,列表排序和二叉搜索树。直接递归的例子如下所示,这个例子返回某个整数的阶乘(一系列递减整数的乘积)。

uint32_t factorial(uint32_t number)

{

    if (number <= 1)

    {

        return 1;

    }

    return number * factorial(number -1);

}

间接递归是提程序调用另一个程序,并最终调用原始程序。我发现的间接递归最实际的使用是在目录遍历程序中, 在这样的程序里,程序在树中导航,另一个程序处理文件。如果这个文件是目录,就调用原始程序。另一个例子是,判断某个数是偶数还是奇数,如下所示。

int is_odd(int number)

{

    return number == 0 ? 0 : is_even(abs(number) - 1);

}

int is_even(int number)

{

    return number == 0 ? 1 : is_odd(abs(number) - 1);

}

漏洞研究中的递归

## 栈溢出导致拒绝服务
递归的问题通常出现在没有合理的估计递归调用的次数时。当函数由 x86 处理器执行时,返回地址和函数参数被压入栈中。在递归调用中,栈以指数级增长。如果递归层数太深,就会在分配的栈大小超出时发生栈溢出。递归引发的栈溢出非常难以利用,通常会导致拒绝服务。这是因为,随着递归调用的进行,栈持续增长,使得栈溢出超出了栈顶。在栈上面(大多数时候)有一个防护页,用于预防栈破坏其他内存区域。当然也有技术能够跳过这个防护页,溢出到堆中。这一技术要求递归函数申请足够大的栈变量以及堆内存靠近防护页的另一端。关于防护页的更多相关资料请戳这里

使用递归攻击未初始化的栈变量

递归在漏洞利用中的另一个作用是攻击未初始化的栈变量。正如前面提到的,x86 调用约定中规定函数参数由调用者压入栈中。如果攻击者能够控制递归中函数参数的值,他们就能通过初始化未初始化的栈变量来控制栈。编译器假设当你申明一个变量时,你会初始化它(在使用之前)。
基于这个假设,编译器会在栈中为栈变量分配一个虚拟地址。在未初始化的栈变量中,编译器会在栈中为栈变量分配空间。因为函数调用的栈帧会有重叠(会被重用),一个未初始化的栈变量通常包含上次使用过后留下来的垃圾数据,直到它被初始化。为了证明这一过程是怎么发生的,我写了一个非常好的程序,我想我能凭此得到下一次的图灵奖。

#include <stdio.h>

#include <stdlib.h>

#include <stdint.h>

void take_int(int j)

{

    if (j == 1337)

        printf("How could this be? j was never initialized!\n");

}

void recursive_func(int n)

{

    static int i = 0;

    i++;

    if (i == 10)

        return;

    else

        recursive_func(n);

}

void func()

{

    int j;

    take_int(j);

}

int main(int argc, char **argv)

{

    int n = atoi(argv[1]);

    recursive_func(n);

    func();

    return 0;

}

上面这个程序接收一个命令行整数参数并调用 recursive_function,调用了 10 次。然后程序调用 func 函数,func 里会传递一个 j 作为变量给 take_int 函数并调用它。这一代码的漏洞存在于 func 函数中。变量 j 被申明之后,在传给 take_int 前没有初始化。让我们编译并运行这个程序,并在命令中把1337作为参数传给这个程序。

$ gcc recursion_demo.c -m32 -o recursion_demo

$ ./recursion_demo 1337

How could this be? j was never initialized!

尽管 j 没有初始化,take_int 中的条件(检查 j 是否等于1337)明显为真,但是为什么呢?在 recursive_func 函数运行结束返回 main 后,栈中的情况像下图所示。在重复递归调用时,栈指针被不断修改成更低的内存地址,而 n(0x00000539)和返回地址一起被压入栈中。函数每递归一次,我们就有更大的可能用我们提供的值在 take_int 中访问到我们的未初始化的变量。在返回地址和函数参数之外,栈中也会有 recursive_func生成的其他的数据。这是在实际应用中攻击未初始化变量的困难之处。使用你的数据覆盖到未初始化变量的偏移地址非常困难,即使在没有其他指令清除它的值的情况下,你的变量想要被引用也非常困难。

递归调用之后的栈布局

注意:上传附件及图片大小不得大于30M。

⚠️ 版权声明:
本博客所有内容(含教程、源码、工具)仅供个人技术学习与研究交流使用,严禁商用、倒卖、二次分发及非法用途
未经作者书面授权,任何组织或个人不得转载、复制或用于其他平台,违者将追究相关责任。

0 0 0 举报
复制成功