逆向 time.h 函数库 time、gmtime 函数

article/2025/10/14 15:04:56

0x01 time 函数

  • 函数原型time_t time(time_t *t)
  • 函数功能:返回自纪元 Epoch(1970-01-01 00:00:00 UTC)起经过的时间,以秒为单位。如果 seconds 不为空,则返回值也存储在变量 seconds
  • C\C++ 实现
#include <stdio.h>
#include <time.h>int main ()
{time_t seconds;seconds = time(NULL);printf("自 1970-01-01 起的小时数 = %ld\n", seconds/3600);return(0);
}
  • 上述程序的功能是通过 time 函数获取自 1970-01-01 00:00:00 后经过的时间,之后打印出经过的小时数,程序的运行结果如下图所示:表示自 1970-01-01 00:00:00 之后经过了 432971 个小时在这里插入图片描述
  • 逆向分析:首先进入 main 函数,由于 time 函数传入的参数为 NULL,所以将 0 压入栈之后调用 time 函数在这里插入图片描述
  • 进入函数后进行栈顶和栈底的操作,之后直接通过 jmp 跳转到 msvcrt._time32 的地址,然后继续向下调试在这里插入图片描述
  • 单步到这个位置可以发现在 time 函数中直接调用了 GetSystemTimeAsFileTime() 这个 API 函数,这个函数属于底层函数,是操作系统直接提供的接口函数在这里插入图片描述
  • 看一下微软文档中给出的定义,从函数功能上可以看出这个 API 函数可以实时的获取系统时间,且获取到的时间是 UTC 格式。从参数上来看,传入的参数是一个指向 FILETIME 结构体的指针在这里插入图片描述
  • 再来看一下 FILETIME 结构体,有两个数据成员都是 DWORD 格式(4 个字节),dwLowDateTime 表示低位时间,而 dwHighDateTime 表示高位的时间,关于高位时间和低位时间的区别会在下面说到,值得注意的是时间的单位是 100 纳秒在这里插入图片描述
  • 再来看一下调用 GetSystemTimeAsFileTime API 函数的例子,lea eax,[local] 这个命令是取函数中第二个局部变量的地址并且存放到 eax 当中,再将 eax 压入栈中之后调用函数,结合上面 GetSystemTimeAsFileTime 函数的文档的分析可以知道 eax 其实就是 FILETIME 结构体在这里插入图片描述
  • 调用完 GetSystemTimeAsFileTime 函数之后,会将 FILETIME 结构体的 dwLowDateTime 储存在 ecx 当中,将 dwHighDateTime 储存在 eax 当中在这里插入图片描述
  • 还记得上面的文档吗 ? GetSystemTimeAsFileTime 函数返回的时间格式是 UTC 时间格式,且是从 1601-01-01 开始计时的,单位为 100 纳秒,而 time 函数返回的时间则是从 1970-01-01 开始计时的,单位为秒,所以下面会进行 UTC 格式的时间转换。首先会将时间的高位加上 0xfe624e21,低位加上 2AC18000,这一步的目的就是将 1601-01-01 调整到 1970-01-01在这里插入图片描述在这里插入图片描述
  • 以高位为例子,调整前为 0x01d5122f 而调整后为 0x00376050,用前减去后结果为 0x19db1df在这里插入图片描述在这里插入图片描述
  • 转换为 10 进制在这里插入图片描述
  • 由于是以 100 纳秒为单位,所以乘以 100 得出为 369 年,而 1970 减去 1601 刚刚为 369在这里插入图片描述
  • 时间转换之后,调用如下函数,这个函数的作用是将纳秒转换为秒在这里插入图片描述
  • 进入这个函数看一下,首先取出第四个参数判断是否为 0,之后取出第三个参数 10000000,然后将低位和高位的时间分别处以 100000000 即可转换为秒单位在这里插入图片描述

注:1 秒等于十亿纳秒,而上述时间单位为 100 纳秒,所以转换为秒只需要除以 1 千万即可

  • 最后返回自 1970 年以来的秒数在这里插入图片描述
  • time 函数的最后会判断传入的参数是否为 0,如果不为 0,则将结果放入传入的变量内在这里插入图片描述

0x02 gmtime 函数

  • 函数原型struct tm *gmtime(const time_t *timer)
  • 函数功能:C 库函数 struct tm *gmtime(const time_t *timer) 使用 time 函数返回的值来填充 tm 结构,并用协调世界时(UTC)也被称为格林尼治标准时间(GMT)表示
  • C\C++ 实现
#include <stdio.h>
#include <time.h>#define BST (+1)
#define CCT (+8)int main ()
{time_t rawtime;struct tm *info;time(&rawtime);/* 获取 GMT 时间 */info = gmtime(&rawtime);printf("当前的世界时钟:\n");printf("伦敦:%2d:%02d\n", (info->tm_hour+BST)%24, info->tm_min);printf("中国:%2d:%02d\n", (info->tm_hour+CCT)%24, info->tm_min);return(0);
}
  • 上述程序的作用主要是获取由 time 函数返回的时间(从 1970.1.1 开始的小时数),之后放入 gmtime 函数转换成更为详细的时间单位。tm 结构体如下图所示,这个就是更为精确的时间细分:
struct tm {int tm_sec;         /* 秒,范围从 0 到 59                */int tm_min;         /* 分,范围从 0 到 59                */int tm_hour;        /* 小时,范围从 0 到 23                */int tm_mday;        /* 一月中的第几天,范围从 1 到 31                    */int tm_mon;         /* 月份,范围从 0 到 11                */int tm_year;        /* 自 1900 起的年数                */int tm_wday;        /* 一周中的第几天,范围从 0 到 6                */int tm_yday;        /* 一年中的第几天,范围从 0 到 365                    */int tm_isdst;       /* 夏令时                        */    
};
  • 程序运行的步骤:(1) 获取纤程局部储存 fls (2)申请堆空间储存 tm 结构体(3)对传入的 time 返回的参数开始转换,转换的结果放入 tm 结构体当中(4)返回 tm 结构体的指针,函数调用结束
  • 运行结果如下图所示:在这里插入图片描述
  • 下面开始逆向分析,由于计算机处理数据和人的计算方式有很大的不同,所以逆向其中的算法还是比较爽的。首先找到 main 函数的入口点,这里用的是 Cfree-5 编译所以 main 入口点比较好找,如果是微软的 VS 编译的话就需要别的方法了,因为在 main 函数之前的初始化工作太复杂了。在 main 函数的入口处可以很清楚的看到首先调用了 time 函数,返回值储存在 eax 当中,之后通过 push eax 将其压入栈中,然后调用 gmtime 函数,最后调用打印函数 printf在这里插入图片描述
  • 查询一下 eax 中的值为 0x240FF1C在这里插入图片描述
  • 由于传入的是一个地址,所以根据 eax 查询其指向的地址,可以发现值为 0x5CEA203A,需要注意的是单位是秒,为什么是倒过来读呢,因为 time 函数返回的是数字类型,所以是以小尾的方式储存在内存空间中,其大小为 4 个字节在这里插入图片描述
  • 0x5CEA203A 转换成年单位,得到 49.4307314 年,刚刚说了这个时间是从 1970-01-01 开始算的,以年为单位加上 49 的结果刚好是 2019在这里插入图片描述
  • 接下来 F7 进入 gmtime 函数看看,开始的时候主要操作栈顶和栈底,这个对分析函数没什么用处,直接跳转到 mscrt._gmtime32 即可在这里插入图片描述
  • 跳转过后发现会调用两个子函数,逆向之后发现第一个函数主要功能是获取纤程局部储存 FLS,并且申请堆空间用于存放 tm 结构体;而第二个函数则是核心函数,主要负责时间的转换在这里插入图片描述
  • 首先看一下第一个函数把,由于功能比较简单就不单步调试了。如图和注释所示,有两个子函数,第一个是获取纤程局部储存 FLS,而第二个函数是申请堆空间,而 msvcrt,_error 函数主要用作错误处理在这里插入图片描述

注:调用一些比较复杂的系统 API 函数需要非常小心,因为容易出错,所以 error 函数用的非常多。但是需要注意的是 error 的使用要注意多线程问题,防止多个线程对用一个 error 变量进行争抢

  • 下面就是获取纤程局部储存的函数,其中调用了系统 API 函数 FlsGetValue,并且使用 GetLastError 函数设置错误信息。当时逆向的时候也是查阅了很多的资料但没有 FLSFlsGetValue 的资料可供了解,所以尚不清楚这个调用这个函数的目在哪里。在这里插入图片描述
  • 由于 FlsGetValue 调用成功了,所以直接跳转到如下位置,之后通过 SetLastError 设置错误码为最后一次获取到的错误码,也就是刚刚 GetLastError 函数获取到的错误码,最后返回 FlsGetValue 的返回值,也就是获取到的局部储存 FLS 的地址在这里插入图片描述在这里插入图片描述
  • 调用完获取纤程局部储存的函数,之后看看获取堆空间的函数,可以看出调用这个函数只有一个参数 0x24,应该是申请堆空间的大小在这里插入图片描述
  • 进入申请堆空间函数,从函数的运行流程可以大致的得出这个函数主要是通过循环的方式调用 malloc 申请堆空间,申请堆空间的大小就是传入的第一个参数 0x24,如果 malloc 调用失败的话就通过 sleep 函数隔段时间后再次调用,直到超出了某些限制值。如果 malloc 调用成功,那么该函数则返回申请堆空间的首地址在这里插入图片描述
  • 运行完申请堆空间的函数后将堆空间的首地址储存在 FLS 偏移 44 个字节的地方,之后再次返回堆空间的首地址在这里插入图片描述
  • 这样一来第一个函数就分析完了,下面来到第二个函数,这个函数就是转换时间的核心函数。从图中可以看出,这个函数传入了两个参数,第一个参数是申请的堆空间的首地址,第二个参数是 time 函数返回的时间,两个参数的作用就不再多述了在这里插入图片描述
  • F7 进入这个函数,开始单步调试。首先从参数中取出堆空间的首地址,之后判断是否申请成功,如果申请成功的话就跳过设置错误信息的步骤在这里插入图片描述
  • 然后初始化堆空间,其实就是将堆空间覆盖为 FFFF...,成功之后再次跳转,目的是忽略设置异常的步骤在这里插入图片描述在这里插入图片描述
  • 之后从参数中取出 time 函数的返回值,并且和 0xFFFF5740 做比较,说明该时间不能大于 136 年,成功之后再次跳转在这里插入图片描述
  • 还记得 tm 结构体吗,首先做的转换就是将 time 函数返回的秒数转换成年,具体算法:(1)通过 0x5CEA203A / 0x7861F80 得到多少年,且余数 edx 约在 1 - 4 年之间(2)使用 5CEA203A / 7861F80 * F879E080 + 5CEA203A 公式计算出余数(3)根据余数加上固定的年数得到一共多少年,如果是 1.3 年就加上 1 年;2.4 年就加上 2
  • 机器的 CPU 计算方法和人的计算方法有很大的不同,最大的难点就是为何使用 5CEA203A / 7861F80 * F879E080 + 5CEA203A 公式去计算余数,直接取出余数不行吗在这里插入图片描述
  • 来分析一下 5CEA203A / 7861F80 * F879E080 + 5CEA203A 取余公式:
原式等于:  5CEA203A / 7861F80 * F879E080 + 5CEA203A = C * F879E080 + 5CEA203A = BA5B68600 + 5CEA203A = A5B68600 + 5CEA203A = 102A0A63A= 02A0A63A
  • 可能有点难理解,转换一下就行了:
原式等于:  原数 / 7861F80 * F879E080 + 原数 =* F879E080 + 原数 = BA5B68600 + 原数 = A5B68600 + 原数 = 102A0A63A= 余数
  • 之后还需要考虑到溢出:
原式等于:  原数 / 7861F80 * F879E080 + 原数 - B00000000 - 100000000=* F879E080 + 原数- B00000000 - 100000000= BA5B68600 + 原数 - B00000000 - 100000000= A5B68600 + 原数 - 100000000= 102A0A63A - 100000000= 02A0A63A= 余数
  • 而人的计算方式是这样的:
原数/ 7861F80 =... 余数 =>  余数 = 原数 -* 7861F80
  • 所以将上面的式子化简之后,和 商 * 7861F80 + 余数 = 原数 其实是一样的:
原数/ 7861F80 * F879E080 + 原数 - B00000000 - 100000000 = 余数原数 / 7861F80 * F879E080 + 原数 - C00000000 = 余数C * F879E080 + 原数 - C00000000 = 余数C * (F879E080 - 100000000) + 原数 = 余数C * -7861F80 + 原数 = 余数原数 -* 7861F80 = 余数
  • eax 当中储存的就是年数在这里插入图片描述
  • 之后将 eax 存入 tm 结构体偏移 0x14 的位置,也就是 int tm_year 在结构体 tm中的位置,其中 ebx 指向的就是 tm 结构体的地址,而且用的是类似数组的寻址在这里插入图片描述
  • 既然知道了 ebxtm 结构体的地址,那么下面逆向起来就快了,因为理解时间格式便于逆向其中的算法。完成了年的转换之后接下来根据 tm 结构体成员变量的位置就可以推出下面转换的是天数,首先将 0x15180 放入 ecx 中,接着将余下的年数除以 0x15180 得出一年当中的第几天(余下的年数就是上面转换年数的余数,以秒表示),将商存入 tm 结构体偏移 0x1C 的位置,余数存入 esi在这里插入图片描述

0x15180 十进制表示为 86400 秒,刚好为 1在这里插入图片描述

  • 接下来转换月份,就是处于一年当中的第几个月,范围是 0 - 110 表示 1 月,怎么转换的呢:通过循环比较 ecx + 4 地址往后的值进行比较,如果大于就跳转。最后将月份储存在 tm 结构体偏移 0x10 的地方在这里插入图片描述
  • ecx + 4 之后的值其实就是月份叠加起来的值,比如 1 月就是 1E(30天),2 月就是 3A(58天=1月+2月)edi 中记录着月份的值,且每次循环加 1。那为什么 1 月是 30 天,2 月是 58 天,怎么都少了一天呢,因为 edi 初始值就为 1,所以是在 1 月的基础上加的在这里插入图片描述
  • 然后转换的是一月当中的第几天,这个比较简单,只需要将一年当中第几天减去 ecx + 4 的数组中表示的最大月数即可,计算结果为 1A(26天)。结果储存在 tm 结构体偏移 0xC 的位置在这里插入图片描述在这里插入图片描述
  • 完成了一月当中的第几天的转换后,下面转换的是一周当中的第几天,算法很简单:首先取出 time 函数返回的秒数,之后除以 0x15180(1天) 得到 1 年当中第几天,之后再除以 7,余数 edx 就是一周当中第几天。结果储存在 tm 结构体偏移 0x18 的位置在这里插入图片描述
  • 最后就是转换时分秒了,由于算法比较简单就统一说了:(1)小时的转换是用上面余下的天数除以 0x1E0(3600秒) (2)分的转换是使用余下的小时数除以 3C(60秒) (3)秒的转化就是余下的秒数,这个不需要计算(4)最后将这三个值分别存入 tm 结构体偏移 0x8、0x4、0x0 的地方在这里插入图片描述
  • 最后返回堆空间的首地址,也就是 tm 结构体的地址。如图所示 tm 结构体的所有变量都已经被覆盖成转换后的值。需要注意的是返回值通过 esi 返回,而不是一般的 eax在这里插入图片描述在这里插入图片描述在这里插入图片描述

逆向 timegmtime 函数到此结束,如有错误,欢迎指正


http://chatgpt.dhexx.cn/article/U3XEmkUY.shtml

相关文章

python gmtime_在Python中操作日期和时间之gmtime()方法的使用

在Python中操作日期和时间之gmtime()方法的使用 这篇文章主要介绍了在Python中操作日期和时间之gmtime()方法的使用,是Python入门学习中的基础知识,需要的朋友可以参考下 gmtime()方法转换历元到一struct_time以UTC其中dst的标志值始终为0以秒表示时间。如果不设置秒时或None&a…

gmtime与localtime的区别

目录 gmtime函数 linux环境下&#xff1a; window环境下 localtime函数 gmtime函数 gmtime转换的时间是UTL时间&#xff0c;与北京时间相差了8个小时 如果你想要得到北京时间&#xff0c;不建议你将gmtime转换后的时间直接加上八个小时 linux环境下&#xff1a; 执行结…

【C库函数】strerror函数详解

目录 strerror 函数原型 参数详解 返回值详解 函数讲解 strerror 返回错误码&#xff0c;所对应的错误信息 函数原型 char *strerror( int errnum ); 参数详解 参数errnum解析错误码信息(errno) 返回值详解 strerror函数就是返回这些错误码所对应错误信息的字符串起始地…

详解:strerror函数:将错误码转化为错误信息

对于大家在浏览网页的时候&#xff0c;或多或少的会见识过不少的错误信息&#xff1a;比如&#xff1a;最常见的就是&#xff1a;404 但是&#xff0c;使用strerror函数&#xff0c;可以将错误码转化为错误信息&#xff01;不知道偶然间看见的读者是否有兴趣进行深入研究一下&…

Strerror函数和Perror函数的介绍及使用

Strerror 通过标准错误的标号&#xff0c;获得错误的描述字符串 &#xff0c;将单纯的错误标号转为字符串描述&#xff0c;方便用户查找错误。 需要引用的头文件 #include <errno.h> #include <string.h> 用法&#xff1a;如果调用函数失败&#xff0c;会产生错误码…

strerror函数使用

。 char *strerror(int errnum); 功能&#xff1a;通过标准错误的标号&#xff0c;获得错误的描述字符串 &#xff0c;将单纯的错误标号转为字符串描述。 参数&#xff1a;errnum&#xff1a;最新的错误标号。 返回值&#xff1a;指向错误信息的指针。#include <stdio.h>…

Linux 应用编程之strerror函数

在 Linux 系统下对常见的错误做了一个编号&#xff0c;每一个编号都代表着每一种不同的错误类型&#xff0c;当函数执行发生错误的时候&#xff0c;操作系统会将这个错误所对应的编号赋值给 errno 变量&#xff0c;每一个进程&#xff08;程序&#xff09;都维护了自己的 errno…

C语言函数: 字符串函数及模拟实现strtok()、strstr()、strerror()

C语言函数&#xff1a; 字符串函数及模拟实现strtok()、strstr()、strerror() strstr()函数: 作用&#xff1a;字符串查找。在一串字符串中&#xff0c;查找另一串字符串是否存在。 形参: str2在str1中寻找。返回值是char*的指针 原理&#xff1a;如果在str1中找到了str2&…

【strerror函数的使用】

strerror函数的使用 一&#xff0c;说明二&#xff0c;具体使用场景 一&#xff0c;说明 strerror会返回错误码&#xff0c;我们可以将其翻译成所对应的错误信息&#xff1b; c语言的库函数在运行的时候&#xff0c;如果发生错误&#xff0c;就会将错误码存放在一个变量中&…

strerror 函数

收藏 75 23 strerror编辑 本词条缺少 名片图&#xff0c;补充相关内容使词条更完整&#xff0c;还能快速升级&#xff0c;赶紧来 编辑吧&#xff01; 通过标准错误的标号&#xff0c;获得错误的描述字符串 &#xff0c;将单纯的错误标号转为字符串描述&#xff0c;方便用户查找…

strerror函数介绍

认识strerror 库函数调用失败的时候会产生错误码&#xff0c;而每一个错误码对应着一条错误信息&#xff0c;strerror函数的作用就是将错误码给转化成错误信息。 在C语言中有一条全局的错误码errno&#xff0c;在程序运行过程中&#xff0c;只要库函数调用失败&#xff0c;我们…

strerror perror

strerror这个函数把错误码转化为错误信息,把错误信息的起始地址返回 X86下的代码 #define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<string.h> int main() { printf("%s\n", strerror(0)); printf("%s\n", strerror(1…

关于strerror

1功能 strerror用于返回错误码信息的首字符的地址。 可以这样理解&#xff0c;函数运行过程中如果失败的话&#xff0c;会返回一个错误码放在errno中&#xff0c;&#xff08;原先设计中就已经存在的&#xff09;调用strerror函数的话&#xff0c;可以打印对应的错误信息 2 …

Multipass虚拟机入门教程

目录 一、Multipass介绍 二、Multipass的安装 1. 系统版本 2. 下载地址和官网教程 3. 安装 4. 查看版本并测试是否安装成功 5. 若启动失败 三、Multipass的简单使用 1. 设置虚拟化提供方式&#xff08;忽略该步骤&#xff09; 2. 创建名为test虚拟机&#xff0c;分配…

服务器vmware新建虚拟机教程,如何创建虚拟机教程全解

这部分教程我们将学习的是如何创建虚拟机。在创建虚拟机之前&#xff0c;vSphere Client是必要的软件之一&#xff0c;它用于访问ESX主机或vCenter的图形管理用户界面。 vSphere Client安装在Windows计算机上&#xff0c;它是与虚拟基础架构进行交互的主要方法。 1.安装vSphere…

专为折腾而生!老旧电脑安装PVE虚拟机保姆教程

专为折腾而生&#xff01;老旧电脑安装PVE虚拟机保姆教程 这几天玩VMware虚拟机上瘾&#xff0c;感觉特别有意思。然而我其实并不满足于只是在这种软件层面上玩玩&#xff0c;而想挑战更高级的玩法&#xff0c;比如说玩玩可以安装在实体机上的虚拟机系统~~ 说直接点就是我想在…

虚拟机Hyper-V的安装以及使用教程

目录 前言 一、什么是虚拟机Hyper-V&#xff1f; 二、使用步骤 虚拟机的安装 前言 Hyper-V是Windows操作系统提供的虚拟机管理平台&#xff0c;所有提供Hyper-V功能的Windows操作系统都可以根据本论文的内容安装并管理虚拟机。具体来说&#xff0c;Hyper-V 提供硬件虚拟化…

VirtualBox基础使用教程

选择VirtualBox而不是VMware Workstation的原因:VirtualBox是开源软件,对于个人的Linux学习来说,既免费又够用,且相较于VMware Workstation来说更为小巧,也没有那么多的自启动服务,不会在你不使用虚拟机的时候也有那么多服务在后台运行。 下载并安装VirtualBox 注:以下…

VMWare安装Linux虚拟机详细教程

使用VMware创建Linux并配置网络 1. 创建虚拟机&#xff0c;选择自定义 2. 选择硬件兼容性 3. 选择稍后安装操作系统 4. 选择Linux系统 5. 创建虚拟机名称和位置 6. 根据需求选择处理器 8. 选择虚虚拟机内存 9. 选择虚拟机的网络类型 10. 选择I/O控制器类型 11. 选择磁盘类型 1…

华为虚拟机服务器怎么使用教程,虚拟机装服务器教程

虚拟机装服务器教程 内容精选 换一换 应用容器化改造有三种方式,您可单击这里查看。本教程以某游戏为例,将该游戏进行微服务的架构改造,再进行容器化。本教程不对改造细节做深度讲解,仅讲解大致的建议。如需要详细了解容器化改造的过程,请单击服务咨询。本章节主要讲解游戏…