我正在尝试测试来自 StackOverflow 的这个示例(),但没有成功。

两周前,我也直接在帖子上(通过评论)寻求澄清,但仍然没有答案(可能太旧了,2010 年)。


我要求一种简约的方式使其工作:编译器选项、操作系统配置,
如果有必要更改代码以使其符合当今的进程/内存布局,或者至少能够超越当今的安全操作系统保护

我尝试了自己的猜测,但似乎没有任何效果。我想避免继续做迷信的尝试(无关的编译器选项、无关的操作系统修补),并选择在这里询问是否有专家或消息灵通的人提出建议或至少指出一条有希望的道路。

我的结果:

$ gcc overflow.c
$ ./a.out  
now inside f()!

应该发生的结果:

nils@doofnase:~$ gcc overflow.c
nils@doofnase:~$ ./a.out
now inside f()!
now inside g()!
now inside g()!
now inside g()!
now inside g()!
now inside g()!
now inside g()!
Segmentation fault

代码:

#include <stdio.h> 
#include <stdlib.h> 


void g()  
{ 
       printf("now inside g()!\n"); 
} 


void f()  
{ 
       int i; 
       void * buffer[1]; 
       printf("now inside f()!\n"); 

       // can only modify this section 
       // cant call g(), maybe use g (pointer to function) 

       // place the address of g all over the stack: 
       for (i=0; i<10; i++) 
               buffer[i] = (void*) g; 

       // and goodbye... 
} 


int main (int argc, char *argv[]) 
{ 
       f(); 
       return 0; 
}

我的机器:

x86_64 GNU/Linux 
6.10.9-amd64

1

  • 1
    另一种更简单但不同的方法是将一个函数指针数组和一个指向 的函数指针并排放置f()在内存中,例如作为 中的局部变量main。用指向 的指针填充数组g(),将越界写入 1(根据实现,是高于还是低于 ),这应该会覆盖指向 的指针f()。然后调用指针指向的函数应该会调用g()


    – 


最佳答案
2

仅仅因为你写的内容超出了结尾buffer并不一定意味着代码会崩溃。这是未定义行为的一部分。

i这里最有可能发生的事情是,你在循环的第二次迭代中覆盖了 的值。当我运行此代码时,我观察到 的堆栈布局如下f

rsp+0  | buffer[0]
rsp+8  | (padding)
rsp+12 | i
rsp+16 | main

我还注意到本地函数的地址在 0x400000-0x400600 范围内,这意味着这些地址的高 4 个字节为 0。

因此,当您写入 时buffer[1],地址的低 4 个字节最终会位于填充的位置,而值为 0 的高 4 个字节将覆盖i。因此,i在循环的每次迭代结束时都会重置为 0,从而导致buffer[1]在无限循环中反复写入。

如果你在循环中这样做:

if (i<2) {
    buffer[i] = (void *)((uintptr_t)g | ((uintptr_t)i << 32));
} else {
    buffer[i] = g;
}

这会将的高位 4 个字节设置为 的buffer[i]当前值,其中i值为 0 和 1。这样, 的值i将在循环过程中保留,然后main可以覆盖堆栈上的 返回地址。

并且仅对前几次迭代执行此操作以保护的值,堆栈上i的地址将被的地址覆盖,从而允许代码返回并执行该函数。maingg

11

  • 我对鼓励利用 UB 的 SO 答案持悲观看法。


    – 

  • 4
    @DevSolar 这个问题的关键在于缓冲区溢出漏洞。简单地声明“UB”并不适用于这种情况。


    – 

  • 正确答案依然是“不要”。


    – 

  • 4
    @DevSolar 当问题不是编写符合标准的程序时,情况并非如此。OP 知道代码有 UB,并询问如何利用它。有时间和地点可以对 C 标准吹毛求疵。这不是。


    – 

  • 1
    @DevSolar 这个问题是从 Security 迁移过来的。这不是关于依赖 UB,而是关于研究漏洞。


    – 


程序调用了未定义的行为。因此,询问“如何让它以 X 的方式运行”是愚蠢的行为。

您的预期行为

nils@doofnase:~$ gcc overflow.c
nils@doofnase:~$ ./a.out
now inside f()!
now inside g()!
now inside g()!
now inside g()!
now inside g()!
now inside g()!
now inside g()!
Segmentation fault

…只是可能发生的件事。只有当 1) 编译器不检查缓冲区溢出,2) 操作系统实际上允许缓冲区溢出发生而不终止程序,3) 您所处的平台上堆栈排列正确且增长方向正确,才会发生这种情况。

随着下一次编译器更新、下一次操作系统更新或者切换到新平台,这两个先决条件都可能发生变化。

观察到的行为…

$ gcc overflow.c
$ ./a.out  
now inside f()!

… 绝不能保证一定会发生,也不能依赖。也许你很幸运。也许如果你添加另一个函数,行为就会改变。谁知道呢。这是 UB。灵魂已经离开了你的程序。

至于“成功测试”,您应该测试的是UB 不会在您的程序中发生。静态代码分析(编译器警告)或动态代码分析(例如,在您的构建/测试链中使用)可以帮助您实现这一点。

至于您链接到的“家庭作业”问题,“无法调用g(),也许使用 g(指向函数的指针)”,我会选择显而易见的方法……

void (*x)() = g;
x();

…并明确地告诉任何展示实际使用缓冲区溢出解决方案的人,在走出课程并将此类内容设置为“作业”之前,我对这种编码“实践”的看法。

6

  • 1
    你说的都是真的,这实际上是真理除了最不熟练的新手之外,所有人都清楚这一点,这实际上是这项作业的基础。此外,虽然描述所有实现的标准无法对 UB 做出预测,但在大多数给定的实际系统(架构、编译器、选项)中,你确实会得到可重复的结果,而且人们确实会使用它,通常是出于非法目的。这是一项专门要求利用特定系统的特定行为的作业。说标准没有定义行为完全偏离了重点。


    – 


  • @Peter-ReinstateMonica 真有趣。我并没有在我的回答中提到“标准”。反问应该是,由谁来“分配” ?原帖者赞同我的回答,并称其“在许多方面都澄清了”,所以你可以停止报复性地将其否决为“无用”,而它显然是有用的。


    – 


  • 1
    我不确定你为什么说报复。我确实投了反对票,并留下了评论。我真的不认为这个答案有帮助。这不是世界末日,我也没有恶意;这就是网站的工作方式。我的一些答案被投了反对票。至于原帖:也许原帖经验不足,或者觉得给写得好的、没有错的答案投赞成票就足够了(比如你的)。


    – 


  • @Peter-ReinstateMonica 因为它应该是显而易见的,它为那些可能看到 dbush 的答案的“最绿色的新手”提供了平衡,该答案没有任何警告信号,并且可能认为这是在他们的程序中调用函数的一个很好的技巧。


    – 

  • 这会给 dbush 的答案带来很好的评论。这正是评论的用途。此外,它在那里会更有用:人们可能永远不会读到被接受的、得票最高的答案。


    –