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

2022-10-28,,,,

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 函数到此结束,如有错误,欢迎指正

逆向 time.h 函数库 time、gmtime 函数的相关教程结束。

《逆向 time.h 函数库 time、gmtime 函数.doc》

下载本文的Word格式文档,以方便收藏与打印。