0x01 fopen 函数
- 函数原型:
FILE *fopen(const char *filename, const char *mode)
返回值为FILE
类型 - 函数功能:使用给定的模式
mode
打开filename
所指向的文件 - 动态链接库:
ucrtbased.dll
- CC++ 实现:
#define _CRT_SECURE_NO_WARNINGS
#include <Windows.h>
#include <iostream>
using namespace std;
int main(int argc, char** argv)
{
char str[] = "AAAAAAAAAA"; // 字符串只是为了定位程序的 main 函数
FILE* fp;
fp = fopen("D:\1.txt", "w+");
if (fp == NULL)
{
cout << "文件打开失败" << endl;
}
else
{
cout << "文件打开成功" << endl;
fclose(fp);
}
return true;
}
- 上述程序功能主要是打开
D
盘下的1.txt
文件,若返回值不为空,则表示打开文件成功。运行结果如下图所示:
- 调试工具:x32dbg
- 逆向分析:这一次分析的函数和其他函数有很大的不同,
fopen
属于操作型函数,不同于算法型函数,操作型函数没有复杂的算法,但是函数调用及传参比较复杂,尤其是fopen
函数需要对操作系统底层进行操作,下面来进行逆向分析 - 首先利用字符串定位的老办法定位
main
函数的位置,用来绕过main
函数之前复杂的初始化过程。如图所示可以看到fopen
函数的调用过程,其中第一个参数是打开文件的路径,第二个参数是w+
符号
- 直接进入
fopen
函数,可以看出在里面有调用了一个子函数ucrtbased.sub_560A560
,且压入的参数和fopen
函数的参数是一样的
- 进入
ucrtbased.sub_5A8AA560
函数,单步向下调试:首先判断传入的第一个参数和第二个参数是否为空
- 其次判断第二个参数的第一和第二个字节是否为空,同时判断第一个参数的第一个字节是否为空(不知道为什么需要重复的判断)
- 参数判断完成之后调用
ucrtbased.sub_5A8D6ED0
函数,函数的功能是对w+
符号对象的操作进行初始化
- 进入
ucrtbased.sub_5A8D6ED0
函数看看,发现此函数会使用关键段对一些数据进行初始化操作。里面涉及了一系列的API
函数,包括_calloc_dbg
、free_dbg
、_unlock_file
、_lock_file
等。需要注意的是在ucrtbased.sub_5A8D6ED0
函数的最后进行了_lock_file
锁文件操作,并且将文件的FILE
类型的句柄储存在局部变量[ebp-1c]
当中。由于并不是fopen
函数的核心功能,所以不多叙述。
需要注意的是以上 API 具有调试方面的功能,调试版本和运行版本可能会有区别
- 初始化完成之后,会调用
ucrtbased.sub_55EFAA0
函数用于判断局部变量[ebp-1c]
否为0
,由于在初始化的过程中传入了[ebp-1c]
的地址,所以该局部变量可能是为了判断初始化是否赋值成功
- 继续向下调试,发现会调用
ucrtbased.sub_55DC550
函数获取[ebp-1c]
地址的值作为调用ucrtbased.sub_6256AA20
函数的第4
个参数。其他三个参数如图中的注释所示(第一个参数和第二个参数就是文件的路径和打开的方式)。从参数中可以看出ucrtbased.sub_560AA20
函数才是fopen
函数的核心处理函数,下面主要看这个函数就可以了
F7
单步进入ucrtbased.sub_560AA20
这个函数,发现其会对传入的参数稍作处理,之后会调用ucrtbased.sub_5619C60
这个子函数,继续F7
跟进ucrtbased.sub_5619C60
这个函数
- 在
ucrtbased.sub_5619C60
函数中发现其会调用两个函数:ucrtbased.sub_55EF510
函数和ucrtbased.sub_5619900
函数
- 传入
ucrtbased.sub_55EF510
函数的参数包括局部变量[ebp-4]
和第四个参数[ebp+14]
。函数的功能很简单,主要是将[ebp+14]
地址的值赋值到[ebp-4]
地址中去
- 而传入
ucrtbased.sub_62579900
函数的参数有4
个,如上图所示,其中第四个参数传递的局部变量[ebp-4]
地址的值,也就是祖父函数[ebp-1c]
的值。进入ucrtbased.sub_62579900
函数单步调试,首先会判断传入的第一个参数是否为空,第一个参数是文件的路径;其次判断第二个参数是否为空,之后调用ucrtbased.sub_55EFAA0
函数判断传入的第四个参数地址的值是否为0
- 其次判断传入的第
4
个参数是否为0
,其实这个[ebp+14]
的值就是祖父函数的局部变量[ebp-1c]
传进来的,通过参数溯源可以很容易的看出来
- 判断完成之后调用
ucrtbased.sub_5606F60
函数,该函数的功能根据传入的参数w+
初始化一些数据,之后需要用到
- 进入
ucrtbased.sub_5606F60
函数调试看看,首先会初始化一些变量。接着判断传入的W+
参数第一个字节是否为0
- 接下来就比较重要了,该判断会根据传入
fopen
的第二个参数来对[ebp-10]
和[ebp-c]
这个两个局部变量赋值。举个例子:如果传入的参数为w+
,则将[ebp-10]
赋值为301
,[ebp-c]
赋值为2
- 接着对该函数中的其他变量进行赋值,以位为单位。其中
w+
的第二个字节储存在局部变量[ebp-18]
中
- 之后将
+
号对应的Ascii
码减去0x20
,配合0xF3E7770
做地址取值,取出来的值做为索引进行调用,jmp
就相当于call
,该功能主要是为了判别传入fopen
函数的第二个参数后面是否带了加号跳转对应的地址
- 紧接着将
302、4、1
放入传入的第一个参数的地址中去
- 之后经过 GS 验证之后函数返回第一个参数的地址
- 函数调用完成之后,将返回的值赋值到局部变量当中去,如下图所示:
- 之后调用
ucrtbased.sub_5619c00
函数,压入的参数如图所示
- 单步进入
ucrtbased.sub_5619c00
函数,发现其会调用底层API
函数_sopen_s
函数,由该函数完成对文件的共享访问
- _fopen_s 底层函数如图所示:
- 然后将传入的第四个参数的值做为地址,如图所示清空其地址中的值,需要注意的是在地址
+0x10
的地方赋值为3
,这个三就是传入_sopen_s
函数的第一个参数,也就是文件的句柄
还记得第四个参数吗,就是上面说的祖父函数的
[ebp-1c]
变量,其实根据函数的功能可以判断出此变量就是用于返回fopen
函数的返回值,类型是 FILE 类型
- 最后进行
GS
验证后返回第四个参数的地址
- 继续返回,返回值和上面一样不变
- 经过两次返回之后,将返回值储存在
[ebp-20]
当中,之后调用ucrtbased.sub_560AA20
函数
ucrtbased.sub_560AA20
函数的功能主要是通过底层API
函数_unlock_file
来解锁文件,压入的参数为父函数的[ebp-1c]
这个局部变量
- 最后取出
[ebp-20]
的值放入eax
中,fopen
函数调用完毕
总结
fopen
函数总体来说还是比较复杂的,这里简单说一下流程:首先会对传入的第一个和第二个参数进行过滤处理,之后使用局部变量[ebp-1c]
做为fopen
函数的返回值,然后对[ebp-1c]
局部变量(FILE
结构)进行初始化操作,并使用_lock_file
锁住文件,之后调用_sopen_s
函数获取文件的共享权限,并且将句柄放入[ebp-1c]
中,最后使用_unlock_file
函数解锁文件并返回,返回值的类型为FILE
。- 需要注意的是本次逆向分析是调试状态下的逆向分析,和真正运行状态下的程序流程可能会有一定的误差。另外就是调试器的注释中可能会有一些错误,还请谅解
逆向
stdio.h
库的fopen
函数到此结束,如有错误,欢迎指正