写在前面
这个CVE
复现起来很简单的,直接把Orange
大大的两条指令在有漏洞的PHP版本上跑一下就可以看到效果了。
复现之后,大致看了一下代码,感觉没有太多收获…所以决定调试一下PHP
的gd
扩展的源代码,理解一下到底是怎么导致了这个漏洞点的产生。虽然期间踩了不少坑,但确实是一次不错的尝试。
PHP
源码调试新技能get~
Window下PHP源码编译
这部分工作,主要参考了链接:
这中间遇到了很多坑点,在介绍过程中都会列出来。
准备工作
-
首先装好
Visual Studio 2017
,必须要装的是通用Windows平台开发
和使用C++的桌面开发
,其它的不安装也可以。
-
下载
PHP-SDK
和 有漏洞的PHP
版本。我下载的版本分别是PHP-SDK 2.1.10-dev
和PHP 7.1.10
。(github上都有对应的项目,可以直接下载,前面提到的链接中都有写下载地址)
编译
- 进入到
PHP-SDK
所在目录,我的操作系统是64位的,安装了Visual Stadio 2017
,选择目录下的phpsdk-vc15-x64.bat
来编译。 - 在命令行中执行
phpsdk-vc15-x64.bat
,然后执行phpsdk_buildtree phpdev
,之后会在PHP-SDK
所在目录中出现phpdev
文件夹。 - 将之前下载的
php
源码整个文件夹拷贝到php-sdk/phpdev/vc15/x64/
目录下。 - 进入
php-src
目录,执行phpsdk_deps --update --branch master
命令,下载依赖关系组件(需要等待较长时间)。 - 运行
buildconf.bat
生成configure
文件,执行命令configure --disable-all --enable-cli --with-gd --enable-debug
,这一步是要选择安装哪些组件,刚开始的时候,并没有好好看要这条指令是实现什么功能,导致后来没有gd
扩展。使用configure --help
指令可以查看添加某个扩展对应的参数是什么。
这里要说一下,如果编译php
之后还想再添加一些新的扩展,可以通过单独编译一个扩展,或者重新编译php
实现。但是我查了很多,单独添加一个扩展的话,在Linux
下可以使用phpize
实现,Windows
下的方法后来不想找了,所以选择了重新编译。 configure
指令执行之后,再执行nmake
指令来编译php
,最终编译出来的可执行的二进制文件路径为php-sdk\phpdev\vc15\x64\php-src\x64\Debug_TS\php.exe
,命令行切换到php.exe
所在目录,通过php.exe -v
指令可以查看我们自己编译成功的php
的版本号,php.exe -m
可以查看我们都安装了哪些扩展。
到这里我们就编译完了php
了。
这部分踩到的坑就是:网上找到的编译php
源码的帖子都是执行configure --disable-all --enable-cli --enable-debug
指令,导致后来找如何单独添加一个扩展花费了很多时间。不过也都怪我,没有理解清楚指令的作用就直接编译了。
(我是复现之后写的,所以就没有截图了。)
调试环境搭建
我是使用VSCode
来调试源码的,感觉这个工具用着很舒服呀,用到一些扩展,还有配置的一些过程比较烦。
扩展
用到的扩展是 C/C++,安装很简单,就不介绍了:
调试环境
在VSCode
中打开php-src
目录,我在php-src\x64\Debug_TS
目录下新建了一个test.php
文件,文件内容:
<?php
//imagecreatefromgif("48540923dd54564eea74f1d5b8de9c82d0584fcc.gif"); //正常gif文件
imagecreatefromgif("poc.gif"); //poc
?>
·
VSCode
在调试时候会在调试目录下新建一个.vscode
文件夹,其中launch.json
文件用来配置调试相关的信息。
launch.json
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "(Windows) Launch",
"type": "cppvsdbg",
"request": "launch",
"program": "编译后生成的可执行文件php.exe路径",
"args": [
"要调试的文件"
],
"stopAtEntry": false,
"cwd": "编译后生成的可执行文件php.exe所在目录",
"environment": [],
"externalConsole": true,
}
]
}
写好这些之后,我并没有像之前的参考链接中提到的那样,顺利地开始调试…反而…坑了好久。
VSCode
一直提示很多文件链接不到。这是因为还要配置一个文件:c_cpp_properties.cpp
,这个文件用来定义一些文件所在路径、宏定义…
c_cpp_properties.cpp
{
"configurations": [
{
"name": "Win32",
"includePath": [
"可以根据错误提示,把所有的缺失的文件路径都加进来",
"${workspaceFolder}/**"
],
"defines": [
"_DEBUG",
"UNICODE",
"_UNICODE",
"PHP_WIN32"
],
"intelliSenseMode": "msvc-x64",
"browse": {
"path": [
"与includePath对应",
"${workspaceFolder}/**"
],
"limitSymbolsToIncludedHeaders": true,
"databaseFilename": ""
}
}
],
"version": 4
}
在找路径的过程中,用了Everything
软件,真的是搜索起来特别快,强行安利一波…我就把所有缺失的文件的路径找到后都加进去…没有很细看…目标就是不报错,可以调试!!
includePath
和browse
不确定是不是两个都要写…
define
中的PHP_WIN32
,刚开始没有加这个,然后一直提示说没有php_config.h
这个头文件,然后我用Everything
查,发现我的电脑上压根没有这个文件,就别说再添加路径了。后来找包含php_config.h
的地方,是因为没有定义PHP_WIN32
,所以才会包含这个头文件的。想了一下,应该是根据操作系统不同,包含了不同的文件。再网上找啊找,在一些和Visual Studio
调试c
代码相关的帖子中,发现要先预定义一些变量,就突然反应过来要在define
中定义PHP_WIN32
,表明当前系统是Window
。
:-O还有,我在网上找php_config.h
这个文件的时候,所以提到的这个文件的路径都是在Linux
下的,就猜这个文件应该只是在操作系统为Linux
时候才会用,Windows
下不会有。
感觉很多相关的帖子,对操作系统的区别并没有说的很清楚,导致没有经验的我在这些地方卡了很长时间。
调试的时候,Debug Console
里还会输出很多错误信息…但是其实并不会影响调试。知道照样可以调试之后,就没有管这些错误信息了。
应该把坑点都讲全了吧…接下来就可以加断点开始调试了。
调试过程
我用了一个正常的gif
文件和Orange
大大给出的poc.gif
比较着调试,帮助我理解gd
扩展对gif
的处理过程,以及漏洞点产生的原因。
那就直接说这个gd_gif_in.c
对gif
图片的处理过程了。
程序刚进来,读图像是在gdImageCreateFromGifCtx()
函数中。
上述代码应该是根据gif
文件的格式,进行了一些处理,读到 ,
符号之后会执行ReadImage()
函数(这个可以通过单步调试,一步一步地去看,看完之后就觉得很清晰啦)。
在ReadImage()
函数中,会一直循环读取gif
文件的内容。
(跟读文件不相关的处理就没必要看了)
在ReadImage()
中就调用了LWZReadByte()
函数了,接下来程序就会执行到LWZReadByte_()
中。
这个函数最开始执行是进行了一些初始化的操作。
其中,传递进来的参数input_code_size
是从gif
文件中读取到的一个字节。单步调试的话很好找到它的值。
对于poc.gif
来说,这个值是0x03
。
我自己找的正常文件,这个值是0x07
。
这是我找的gif
图片…(突然觉得好魔性)
所以,对于poc.gif
来说,赋值之后:scd->code_size=4, scd->clear_code=8
。对于正常gif
来说,赋值之后:scd->code_size=8, scd->clear_code=128
。这个clear_code
起着很关键的作用。
等下次进来的时候,就会进入到有漏洞的代码部分了。
这部分代码会一直执行,直到scd->firstcode != scd->clear_code
。scd->firstcode
的值就是GetCode()
函数的返回值,理解GetCode()
函数之后可以知道,正常情况下它的返回值读到的gif
数据部分的某个字节的值。
图中是我截的处理正常GIF
文件时候的调试信息,scd->buf
中存储读到的gif
文件的内容,每次会读254
个字节(这个是由0x07
之后的0xfe
来决定的)。
正常文件之所以不会导致服务器宕机,是因为:其中包含了和scd->clear_code
不相等的字节(即0x80
)。不清楚为什么会设定这样的循环截止条件(scd->firstcode != scd->clear_code
),估计跟gif
的格式有关吧。
在不包含截止条件的情况下,会一直执行GetCode_()
函数,并且通过scd->curbit
值的不断改变,会遍历到scd->buf
中后面的字节,也就是说会在读取到的字节中一直找有没有值不为0x80
的字节。
scd->lastbit
存储了当前scd->buf
中存储的比特数,scd->curbit
表示当前遍历了哪些比特了。
当遍历完所有的比特的时候,就会满足第388
行的判断条件。
进入388
行的if
语句之后,会再398
行再次读取文件,从而判断是否已经读到gif
文件的末尾。读到末尾的标志就是count<=0
,因为GetDataBlock()
函数的返回值是读取的字节数。如果判断读到末尾的话,scd->done
就会被置为true
。在下一次执行这个函数的时候,就会在第393
行return -1
,从而使得scd->firstcode != scd->clear_code
,然后跳出循环。
那么问题来了,因为count
的定义是unsigned char
,所以它的值一定是0-255
之间的。所以GetDataBlock_()
函数的返回值一定是个大于0的数(要么返回count
,要么返回-1
,但是因为count
是unsigned char
,返回-1
时,它的值其实是大于0
的)。所以这边不会判断到文件结束,不会在这里返回-1
,从而结束循环。这就是最根源的漏洞点。
让循环结束有两种方式:
- 读完整个
gif
文件,达到文件末尾,这个因为数据类型定义错误,导致不可能执行; - 让文件中包含与
scd->clear_code
相等的字节,满足截止条件,这个正常的gif
文件是可以满足的。
那,我们要是想循环一直执行,从而消耗服务器资源,就需要再破坏第二个循环结束条件。通过构造一个所有字节都与scd->clear_code
相等的gif
文件(对于poc.gif
来说,scd->clear_code
值为8
,所以构造的数据均为0x88
),就这么简单~~
总结
总结来说,就是:
正常文件都是包含截止条件的,所以不会出问题。
而我们恶意构造的文件不包含截止文件,而读到文件末尾的限制又因为数据类型定义出错,而无法起作用。
好像结束的有点突然…
感觉已经讲的很清楚很啰嗦了。这次学习经历应该对我的代码审计能力有提高,多多练习。