0
点赞
收藏
分享

微信扫一扫

逆向 stdio.h 函数库 fopen 函数(调试版本)

0x01 fopen

  • 函数原型:​​FILE *fopen(const char *filename, const char *mode)​​​ 返回值为​​FILE​​ 类型
  • 函数功能:使用给定的模式​​mode​​​ 打开​​filename​​ 所指向的文件
  • 动态链接库:​​ucrtbased.dll​
  • C\C++ 实现

#define
#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​​ 文件,若返回值不为空,则表示打开文件成功。运行结果如下图所示:
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_局部变量

  • 调试工具:x32dbg
  • 逆向分析:这一次分析的函数和其他函数有很大的不同,​​fopen​​​ 属于操作型函数,不同于算法型函数,操作型函数没有复杂的算法,但是函数调用及传参比较复杂,尤其是​​fopen​​ 函数需要对操作系统底层进行操作,下面来进行逆向分析
  • 首先利用字符串定位的老办法定位​​main​​​ 函数的位置,用来绕过​​main​​​ 函数之前复杂的初始化过程。如图所示可以看到​​fopen​​​ 函数的调用过程,其中第一个参数是打开文件的路径,第二个参数是​​w+​​ 符号
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_局部变量_02

  • 直接进入​​fopen​​​ 函数,可以看出在里面有调用了一个子函数​​ucrtbased.sub_560A560​​​,且压入的参数和​​fopen​​ 函数的参数是一样的
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_sed_03

  • 进入​​ucrtbased.sub_5A8AA560​​ 函数,单步向下调试:首先判断传入的第一个参数和第二个参数是否为空
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_sed_04


  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_初始化_05

  • 其次判断第二个参数的第一和第二个字节是否为空,同时判断第一个参数的第一个字节是否为空(不知道为什么需要重复的判断)
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_局部变量_06


  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_sed_07

  • 参数判断完成之后调用​​ucrtbased.sub_5A8D6ED0​​​ 函数,函数的功能是对​​w+​​ 符号对象的操作进行初始化
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_初始化_08

  • 进入​​ucrtbased.sub_5A8D6ED0​​​ 函数看看,发现此函数会使用关键段对一些数据进行初始化操作。里面涉及了一系列的​​API​​​ 函数,包括​​_calloc_dbg​​​、​​free_dbg​​​、​​_unlock_file​​​、​​_lock_file​​​ 等。需要注意的是在​​ucrtbased.sub_5A8D6ED0​​​ 函数的最后进行了​​_lock_file​​​ 锁文件操作,并且将文件的​​FILE​​​ 类型的句柄储存在局部变量​​[ebp-1c]​​​ 当中。由于并不是​​fopen​​ 函数的核心功能,所以不多叙述。
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_初始化_09

需要注意的是以上 API 具有调试方面的功能,调试版本和运行版本可能会有区别

  • 初始化完成之后,会调用​​ucrtbased.sub_55EFAA0​​​ 函数用于判断局部变量​​[ebp-1c]​​​ 否为​​0​​​,由于在初始化的过程中传入了​​[ebp-1c]​​ 的地址,所以该局部变量可能是为了判断初始化是否赋值成功
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_局部变量_10

  • 继续向下调试,发现会调用​​ucrtbased.sub_55DC550​​​ 函数获取​​[ebp-1c]​​​ 地址的值作为调用​​ucrtbased.sub_6256AA20​​​ 函数的第​​4​​​ 个参数。其他三个参数如图中的注释所示(第一个参数和第二个参数就是文件的路径和打开的方式)。从参数中可以看出​​ucrtbased.sub_560AA20​​​ 函数才是​​fopen​​ 函数的核心处理函数,下面主要看这个函数就可以了
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_初始化_11

  • ​F7​​​ 单步进入​​ucrtbased.sub_560AA20​​​ 这个函数,发现其会对传入的参数稍作处理,之后会调用​​ucrtbased.sub_5619C60​​​ 这个子函数,继续​​F7​​​ 跟进​​ucrtbased.sub_5619C60​​ 这个函数
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_sed_12

  • 在​​ucrtbased.sub_5619C60​​​ 函数中发现其会调用两个函数:​​ucrtbased.sub_55EF510​​​ 函数和​​ucrtbased.sub_5619900​​ 函数
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_初始化_13

  • 传入​​ucrtbased.sub_55EF510​​​ 函数的参数包括局部变量​​[ebp-4]​​​ 和第四个参数​​[ebp+14]​​​。函数的功能很简单,主要是将​​[ebp+14]​​​ 地址的值赋值到​​[ebp-4]​​ 地址中去
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_初始化_14

  • 而传入​​ucrtbased.sub_62579900​​​ 函数的参数有​​4​​​ 个,如上图所示,其中第四个参数传递的局部变量​​[ebp-4]​​​ 地址的值,也就是祖父函数​​[ebp-1c]​​​ 的值。进入​​ucrtbased.sub_62579900​​​ 函数单步调试,首先会判断传入的第一个参数是否为空,第一个参数是文件的路径;其次判断第二个参数是否为空,之后调用​​ucrtbased.sub_55EFAA0​​​ 函数判断传入的第四个参数地址的值是否为​​0​
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_初始化_15


  • 其次判断传入的第​​4​​​ 个参数是否为​​0​​​,其实这个​​[ebp+14]​​​ 的值就是祖父函数的局部变量​​[ebp-1c]​​ 传进来的,通过参数溯源可以很容易的看出来
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_局部变量_16

  • 判断完成之后调用​​ucrtbased.sub_5606F60​​​ 函数,该函数的功能根据传入的参数​​w+​​ 初始化一些数据,之后需要用到
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_sed_17

  • 进入​​ucrtbased.sub_5606F60​​​ 函数调试看看,首先会初始化一些变量。接着判断传入的​​W+​​​ 参数第一个字节是否为​​0​
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_局部变量_18


  • 接下来就比较重要了,该判断会根据传入​​fopen​​​ 的第二个参数来对​​[ebp-10]​​​ 和​​[ebp-c]​​​ 这个两个局部变量赋值。举个例子:如果传入的参数为​​w+​​​,则将​​[ebp-10]​​​ 赋值为​​301​​​,​​[ebp-c]​​​ 赋值为​​2​
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_初始化_19


  • 接着对该函数中的其他变量进行赋值,以位为单位。其中​​w+​​​ 的第二个字节储存在局部变量​​[ebp-18]​​ 中
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_sed_20

  • 之后将​​+​​​ 号对应的​​Ascii​​​ 码减去​​0x20​​​,配合​​0xF3E7770​​​ 做地址取值,取出来的值做为索引进行调用,​​jmp​​​ 就相当于​​call​​​,该功能主要是为了判别传入​​fopen​​ 函数的第二个参数后面是否带了加号跳转对应的地址
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_sed_21

  • 紧接着将​​302、4、1​​ 放入传入的第一个参数的地址中去
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_sed_22

  • 之后经过 GS 验证之后函数返回第一个参数的地址
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_局部变量_23

  • 函数调用完成之后,将返回的值赋值到局部变量当中去,如下图所示:
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_局部变量_24

  • 之后调用​​ucrtbased.sub_5619c00​​ 函数,压入的参数如图所示
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_初始化_25

  • 单步进入​​ucrtbased.sub_5619c00​​​ 函数,发现其会调用底层​​API​​​ 函数​​_sopen_s​​ 函数,由该函数完成对文件的共享访问
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_局部变量_26

  • ​​_fopen_s​​ 底层函数如图所示:
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_sed_27

  • 然后将传入的第四个参数的值做为地址,如图所示清空其地址中的值,需要注意的是在地址​​+0x10​​​ 的地方赋值为​​3​​​,这个三就是传入​​_sopen_s​​ 函数的第一个参数,也就是文件的句柄
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_初始化_28

还记得第四个参数吗,就是上面说的祖父函数的 ​​[ebp-1c]​​​ 变量,其实根据函数的功能可以判断出此变量就是用于返回 ​​fopen​​ 函数的返回值,类型是 FILE 类型

  • 最后进行​​GS​​ 验证后返回第四个参数的地址
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_sed_29

  • 继续返回,返回值和上面一样不变
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_初始化_30


  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_sed_31

  • 经过两次返回之后,将返回值储存在​​[ebp-20]​​​ 当中,之后调用​​ucrtbased.sub_560AA20​​ 函数
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_sed_32

  • ​ucrtbased.sub_560AA20​​​ 函数的功能主要是通过底层​​API​​​ 函数​​_unlock_file​​​ 来解锁文件,压入的参数为父函数的​​[ebp-1c]​​ 这个局部变量
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_初始化_33

  • 最后取出​​[ebp-20]​​​ 的值放入​​eax​​​ 中,​​fopen​​ 函数调用完毕
  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_sed_34


  • 逆向 stdio.h 函数库 fopen 函数(调试版本)_初始化_35

总结

  • ​fopen​​​ 函数总体来说还是比较复杂的,这里简单说一下流程:首先会对传入的第一个和第二个参数进行过滤处理,之后使用局部变量​​[ebp-1c]​​​ 做为​​fopen​​​ 函数的返回值,然后对​​[ebp-1c]​​​ 局部变量(​​FILE​​​ 结构)进行初始化操作,并使用​​_lock_file​​​ 锁住文件,之后调用​​_sopen_s​​​ 函数获取文件的共享权限,并且将句柄放入​​[ebp-1c]​​​ 中,最后使用​​_unlock_file​​​ 函数解锁文件并返回,返回值的类型为​​FILE​​。
  • 需要注意的是本次逆向分析是调试状态下的逆向分析,和真正运行状态下的程序流程可能会有一定的误差。另外就是调试器的注释中可能会有一些错误,还请谅解

逆向 ​​stdio.h​​​ 库的 ​​fopen​​ 函数到此结束,如有错误,欢迎指正



举报

相关推荐

0 条评论