前言
通过本文介绍怎么对一个 windows
程序进行安全分析。分析的软件版本为 2018-10-9
, 所有相关文件的链接
链接:https://pan.baidu.com/s/1l6BuuL-HPFdkFsVNOLpjUQ
提取码:erml
逆向分析
定位核心代码
拿到一个软件首先需要进行攻击面的探测,即找到尽可能多的可以与程序进行交互的入口点,有数据交互的地方就有可能会出现漏洞。首先对软件的功能做一个大概的了解,发现搜狗输入法能够安装用户自定义的皮肤,这是一个比较好的入口点,于是下面分析分析处理皮肤文件的逻辑。
先从官网随便下个皮肤,然后拿 010editor
简单看看能不能拿到一些有用的信息。
使用 binwalk
也没有识别出文件格式,于是猜测应该是输入法自己实现的格式。
后来在对皮肤相关的功能进行浏览的时候发现有皮肤编辑器这个软件
https://pinyin.sogou.com/skins/design.php
下载下来随便创建一个皮肤,发现此时的皮肤格式为 zip
格式,双击也能正常安装。皮肤编辑器的最近更新在 13 年,估计输入法是为了做兼容,同时支持两种格式的皮肤文件。
下载下来的皮肤双击就可以安装,这样的安装方式我们不好定位具体安装皮肤的程序,这时我们可以使用 api montor
监控当双击皮肤文件时系统所执行的命令,以便进行下一步的分析。
打开 api monitor
, 然后打开皮肤文件可以监控到搜狗输入法处理皮肤文件执行的命令
"C:\Program Files (x86)\SogouInput\SogouExe\SogouExe.exe" "C:\Program Files (x86)\SogouInput\9.1.0.2657\SGTool.exe"-line 0 -border --appid=skinreg -list "C:\Users\XinSai\Desktop\test.ssf"
通过使用 Procmon.exe
分析,其实最后调用
"C:\Program Files (x86)\SogouInput\9.1.0.2657\SGTool.exe" -line 0 -border --appid=skinreg -install -c "C:\Users\XinSai\Desktop\test.ssf" -q -ef
通过执行这条命令就可以把皮肤安装到输入法内部。
下面把 SGTool.exe
拖到 IDA 里面, 使用命令行选项来搜索字符串的交叉引用去找到相关的处理代码。通过对命令行参数的交叉引用逐步向上追溯,在 0x07A04D0
发现程序会根据 appid
参数的值,决定下一步进行处理的函数
然后调试发现一直断不到这,于是用 drrun
看看程序到底走了哪些路径
drrun.exe -t drcov -- "C:\Program Files (x86)\SogouInput\9.1.0.2657\SGTool.exe" -line 0 -border --appid=skinreg -install -c "C:\Users\XinSai\Desktop\test.ssf" -q -ef
发现直接点击 ssf
文件还是没有进入这个分支。
经过不断的尝试 + 使用一些监控软件,发现在关闭所有搜狗输入法相关进程的情况下,双击 .ssf
文件,会首先使用
"C:\Program Files (x86)\SogouInput\9.1.0.2657\SGTool.exe" -daemon
开启一个类似于服务器的进程,然后在使用
"C:\Program Files (x86)\SogouInput\9.1.0.2657\SGTool.exe" -line 0 -border --appid=skinreg -install -c "C:\Users\XinSai\Desktop\test.ssf" -q -ef
另外再起一个 SGTool.exe
的进程向刚刚启动服务进程发送消息,后续的皮肤处理在服务进程进行。猜测可能是使用了 windows
本地通信机制实现 C/S 架构
后续在瞎试的时候,将一个非皮肤文件命名成 .ssf
后缀,然后双击会出现报错信息。
根据这些字符串在 od
里面找,可以找到一些信息,最后通过对
皮肤解压失败:skin.ini不存在
交叉引用, 然后不断回溯, 发现了一个有趣的函数 0x07A72D0
这个函数首先 调用 0x7A84F0
检测了某些参数。
从我们的参数进行对比,猜测这里校验的应该是最后那两个参数
然后猜测 -q
应该是安静模式,于是删掉 -q
试了一试
发现居然会有提示框,那这个提示框的代码附件应该离处理 皮肤文件的代码更加近了。
对字符串交叉引用,发现其实就是上面的那个代码(0x007A75DC
)
所以从这里开始,应该就开始对皮肤文件进行处理了。
分析皮肤处理相关的代码
经过不断的调试以及查看函数调用的参数,发现当我们点击 确认 的时候,会调用位于0x914980
的函数。
这个函数传入的参数是一个对象指针,对象内部有我们皮肤文件的路径,这个函数会对传入的 皮肤文件 进行第一次的判断。
函数首先会打开文件, 然后取出开头的4
个字节作为文件类型,判断是不是Skin。
如果是的话就认为是最新的皮肤格式然后进入后续的操作。
如果不是就认为是第一代皮肤格式 ,即用zip
格式打包的皮肤。
调试时,可以看到 type
的值。 下面是打开的官网下载的皮肤文件,所以 type
为 Skin
.
当皮肤文件的type
值不为Skin
时,程序会进入decompress_skin_ini (0x063F340)
, 这个函数里面会调用ziplib.dll
里面的函数对皮肤文件进行解压,提取并解析skin.ini
文件。
总的来说,0x914980
函数其实只是校验了皮肤文件的版本信息,对于 文件头 不是 Skin
的文件,则认为是第一代皮肤格式文件,然后会使用 ziplib.dll
里面的函数提取 skin.ini
文件并尝试解析它。
在进行完第一次的校验后,会回到 0x7A72D0
, 将文件拷贝到用户的皮肤保存目录,然后对皮肤文件进行解析,提取出里面的文件。
其中 0x7A6230 deal_skin
就是解析皮肤文件的入口,它会调用 0x63E3F0
完成具体文件解析流程。
这个函数首先判断文件头 , 如果是 Skin
, 表示为最新格式的皮肤文件
则通过 decompress_skinv3 0x053B320
进行解析并提取出皮肤包里面包含的文件。否则就认为是第一代皮肤文件, 使用 ziplib
里面的函数, 把皮肤包里面的文件解压出来。
for ( i = v25 - 3; v28 < v27; ++v28 )
{
..................................................................
..................................................................
..................................................................
..................................................................
if ( decompress_file_from_zip(v46, v65, &path, target, &len, &buf) )
{
v47 = len;
if ( len <= 0x80000000 && len )
{
len = target;
(*v51)(&v51, &len, 0);
v48 = buf;
v54 = v47;
v53 = buf;
sub_53ACE0(obj1, &v50, 0);// 做一些析构操作
sub_49F5C0(ziplib_obj, v65, v47, v48);
}
v24 = v62;
}
..................................................................
..................................................................
..................................................................
..................................................................
}
下面对decompress_skinv3(0x053B320)
进行分析,以便理解最新皮肤包的格式
首先打开皮肤包,读取文件内容到内存,然后把文件内容,大小传给 handle_skinv3 0x0053A8C0
进行处理。
通过对这个函数的逆向,可以明白.ssf
文件开头 8个字节的结构为 4字节的 Skin 和 4 字节的 version 字段
校验完版本信息后, 会对文件头部后的数据使用程序自己实现的算法进行解码,然后对解码之后的数据使用zlib
再次解码。
zlib
解码完成后,把解码后的数据传入 0x53bf50
(调试时确认)对文件进行提取
这个函数对传入的数据进行解析,提取出相关的文件
// 调用者 0x53AAE1
unsigned int __thiscall skin_v3_step2(int this, unsigned int *buf, unsigned int size, int a4)
{
len = *buf; // 取出 buf 开始的 4 个字节, 表示数据的长度
if ( *buf > size )
return -1;
new_size = size - 4;
sizea = size - 4;
if ( *buf )
{
if ( (**(this + 4))(this + 4, buf + 1, new_size, a4) < 0 )// 调用0x53c110L,第 2, 3个参数为 buf + 4, size-4
// 就是忽略掉头4个字节
// 函数作用复制 zlib 解码后的数据 0x8-0x40 到对象里面
do // 循环的从 zlib 解压过的文件里面提取出文件
{
offset = *(v12 + 4 * v9);
if ( offset < data_len )
{
v17 = (**v15)(cur, sizea, a4); // 调用 0x923f70L, 提取文件名
if ( v17 < 0 )
goto LABEL_19;
v18 = &cur[v17];
v19 = (*v22[3])(v18, sizea - v17, a4);// 调用的是 0x53c1c0L, 提取文件内容
}
++v9;
}
while ( v9 < v11 );
文件格式汇总
老版本的皮肤格式
其实就是一个 zip
包, 里面有配置文件和一些图片。
新版本的皮肤格式
首先是 .ssf
文件
开头 8个字节为 4字节的 Skin
和 4 字节的 version
- 然后调用
0x0639610
对 文件偏移 8开始进行解码, 解码后的数据A
偏移 4 字节开始 为zlib
压缩的数据。 - 然后对
A + 4
使用zlib
解码,得到zlib
解码后的数据B
- 然后把
B
和B
的长度传入0x53bf50
继续处理
B
的结构为
开头4个字节为数据的总长度
然后根据 0x53C110
, 后面紧跟着的 4 个字节为文件映射表的长度,即 0x38
字节。每一个表项4个字节,代表表项指示其所表示的文件在整个文件中的偏移地址。
如图所示, 第一个表项的值为 0x40
, 所以第一个文件应该在 0x40
处。
经过一定的观察发现一个文件的表示方式为
- 4字节: 文件名的长度
- 文件名(
unicode
编码) - 4字节: 文件数据的长度
- 文件数据
发现漏洞
分析完整个皮肤处理的代码后发现整个代码的逻辑还是不怎么复杂的。于是可以直接读反编译的代码来找找看是否存在什么漏洞。在读代码找漏洞时重点关注缓冲区的操作, 内存的分配大小以及对文件中表示长度的字段的使用是否合理。
经过一番仔细的走查发现在对皮肤文件第一步用自己实现的解密算法解密后的开始 4 个字节为 deocded_data_size
, 之后会把它加上 8
然后去分配内存。
deocded_data_size
是从解密后的文件中取出的,当把 deocded_data_size
改成 0xffffffff
时,在分配内存时会整数溢出导致分配比较小的内存块,然后后续的代码在使用这个缓冲区时会造成一个堆溢出。漏洞已于3个月前提交并修复。
总结
在分析软件功能实现时,可以采用一些监控软件比如 api monitor
来辅助定位关键代码。一些程序中的提示,报错信息也可以用来定位。最重要的就是多调试,多调试。程序从文件内容中取 size
时要注意校验。