用一个printf()调用实现一个web服务器

一个小伙伴转发了一个可能我们都知道的Jeff Dean笑话。每次我读到这个列表的时候,这一部分就会跳出来:

 Jeff Dean有次用一句printf()实现了一个web服务器,而其他工程师添加了数千行注释但是仍然不能完全弄清楚它是如何工作的。而这个程序正是如今的Google Search首页。

使用一句printf调用来实现一个web服务器是很有可能的,但是我还没发现其他人做到。所以这次我读到这个列表时,我决定实现它。这里是它的代码,一个纯粹单一的printf调用,没有任何附加的变量或者宏(不用担心,我将会解释这段代码是如何工作的)。

这段代码只能在独有Linux AMD64位编译器(gcc版本是4.8.2Debian 4.8.2-16))的系统上运行,编译命令如下:

可能有些人会这样猜测:我用一个特殊格式的字符串来作弊。这段代码可能不能在你的机器上运行,因为我对两个地址使用了硬编码。

下面这个版本是更加用户友好化的(更容易改变),但是你仍旧要改变两个值:FUNCTION_ADDRDESTADDR,稍后我会解释:

我将解释这段代码如何通过一系列简短的C编码来工作。第一段代码将解释如何不使用函数调用,就能运行另一段代码。看看下面这段简单的代码:

你可以编译它,但是它可能不能在你的系统上运行,你需要按如下步骤来做:

1.编译这段代码:

2.检查fini_array的地址

然后从中找到VMA

你需要一个最新版的GCC来编译才能发现它,旧版本的GCC使用不同的存储终结器原理。

3. 改变代码中ADDR的值为正确的地址。

4.重新编译代码

5.运行它

现在你就会看到你的屏幕上输出“hello world”,而它实际上是如何运行的呢?:

依据Chapter 11 of Linux Standard Base Core Specification 3.1(译注:Linux标准基础核心规范3.111章)

       .fini_array

这部分保存了一个函数指针数组,它贡献出一个终止数组给这个可执行的或可共享的、包含这个部分的对象。

为了让hello函数被调用而不是调用默认的处理函数,我们要重写这个数组。如果尝试编译这个web服务器代码,ADDR的值以同样的方式获取(使用objdump)。

好了,现在我们清楚了如何通过覆盖一个确定的地址来执行一个函数,还需要知道如何使用printf来覆盖一个地址。可以找到很多关于利用格式化字符串漏洞的教程,但是我将给出一个简短的解释。

printf函数有这样一个特性,使用“%n”格式可以让我们知道有多少个字符输出。

可以看到输出如下:

当然我们用任何计数指针的地址来重写这个地址。但是为了用一个大数值来覆盖地址,需要输出大量的文本。幸运的是,有另外一个格式化字符串“%hn”作用于short而不是int。每次可以用2个字节排列成一个我们需要的4字节值来覆盖这个值。

试着用两个printf调用放置我们需要的a¡值(在这个例子中是指“hello”函数的指针)到fini_array

导入的行是:

ab都只是函数地址的一半,可以构造一个ab长度的字符串传入printf但是我选择使用“%*”这个格式,它可以通过参数来控制输出的长度。

例如这段代码:

将会在A后面输出9个空格,所以一共输出10字符。

如果只想用一个printf,就需要考虑到b字节已经被打印,而我们又需要打印另一个b-a字节(这个计数器是累加的)。

目前我们是调用这个“hello”函数,但是其实我们是可以调用任何函数的(或者任何地址)。我写过一个就像web服务器的shellcode(译注:填充数据),但是它只是输出“Hello world”。以下是我写的填充数据:

如果移除hello函数然后插入这个填充数据,这段代码将会被调用。

这段代码其实就是一个字符串,所以可以给它添加“%*c%hn%*c%hn”格式化字符串。这个字符串还未命名,所以需要在编译后找到它的地址,而为了获得这个地址,我们需要编译这段代码,然后反汇编它:

其实只需要关心这行:

这就是我们需要的地址:

+12是非常必要的,因为我们的填充数据是从12个字符长度的“%*c%hn%*c%hn”字符串后面开始的。

如果你的对填充数据很好奇,其实它是由以下的C代码创建的:

我做了额外的工作(即使在这个例子中并不是十分必要的)来移除这个填充数据中的所有NUL字符(因为我没有从X86-64上的Shellcodes数据库中找到一个NUL字符)。

Jeff Dean曾经使用一个printf()调用实现了一个web服务器。其他的工程师添加了数千行的注释,但是仍然没有弄清楚它是如何工作的。而这个程序正是如今的Google Search首页

这给读者留下了一道练习题,如果要评测web服务器,可以处理Google search的负载。

这部分的代码可以从这里获得。

对于认为这样做是无用的人:它确实是没有用的。我只是碰巧喜欢这种挑战,而它为以下主题更新了我的记忆和知识:编写填充代码(已经很多年没有写过了),AMD64装配(调用惯例,寄存器保护等等),系统调用,objdumpfini_array(最近一次我检测的时候,GCC依然使用.dtors),printf格式化利用,gdb技巧(例如将内存块写入文件),还有低阶的socket编程(过去几年中我使用过boost)。

更新Ubuntu增加了一个安全特性,这个特性提供了在最终的ELF表区域中只读重定位,为了能够在ubuntu中运行这个例子,在编译的时候添加以下命令行:

比如:

打赏支持我翻译更多好文章,谢谢!

打赏译者

打赏支持我翻译更多好文章,谢谢!

2 收藏 5 评论

关于作者:欣仔

假装会写代码的伪程序员~ 个人主页 · 我的文章 · 12 ·   

相关文章

可能感兴趣的话题



直接登录
最新评论
  • TERRY_ZJ   2014/04/14

    我感觉这个例子除了告诉大家可以在字符串里写汇编语言和炫耀技巧之外无多大意义,当然就如文章中说的那样,这只是一个笑话

    这让我想到了C语言混乱代码大赛,里面充斥着各式各样的类似这种语言技巧

    • Analyst   2014/04/16

      作者把这件事情的意义写的很清楚了:编写填充代码,AMD64装配,系统调用,objdump,fini_array,printf格式化利用,gdb技巧,还有低阶的socket编程。作者对系统底层的理解已经到了很深入的境界,这哪是你这种小菜鸟所能企及的。

  • C0reFast   2014/04/21

    屌炸天....

  • 程序猿   2014/04/21

    看不懂的,远离计算机底层原理的脚本级程序员,过来点个赞吧。

  • liym   2014/04/22

    很厉害啊,参加某数字公司笔试后心血来潮写了点类似的东西,但远不及作者的深度。不过提醒一下,在windows操作系统下可能无法进行类似的实验

跳到底部
返回顶部