逆向 time.h 函数库 time、gmtime 函数
0x01 time 函數
函數原型:time_t time(time_t *t)
函數功能:返回自紀元 Epoch(1970-01-01 00:00:00 UTC)起經過的時間,以秒為單位。如果 seconds 不為空,則返回值也存儲在變量 seconds 中
CC++ 實現:
#include <stdio.h>
#include <time.h>
int main ()
{
time_t seconds;
seconds = time(NULL);
printf("自 1970-01-01 起的小時數 = %ld
", 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)表示
CC++ 實現:
#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("當前的世界時鐘:
");
printf("倫敦:%2d:%02d
", (info->tm_hour+BST)%24, info->tm_min);
printf("中國:%2d:%02d
", (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 函數設置錯誤信息。當時逆向的時候也是查閱了很多的資料但沒有 FLS 和 FlsGetValue 的資料可供了解,所以尚不清楚這個調用這個函數的目在哪里。
由于 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 結構體的地址,而且用的是類似數組的尋址
既然知道了 ebx 是 tm 結構體的地址,那么下面逆向起來就快了,因為理解時間格式便于逆向其中的算法。完成了年的轉換之后接下來根據 tm 結構體成員變量的位置就可以推出下面轉換的是天數,首先將 0x15180 放入 ecx 中,接著將余下的年數除以 0x15180 得出一年當中的第幾天(余下的年數就是上面轉換年數的余數,以秒表示),將商存入 tm 結構體偏移 0x1C 的位置,余數存入 esi 中
0x15180十進制表示為86400秒,剛好為1天
接下來轉換月份,就是處于一年當中的第幾個月,范圍是 0 - 11,0 表示 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
逆向
time、gmtime函數到此結束,如有錯誤,歡迎指正
總結
以上是生活随笔為你收集整理的逆向 time.h 函数库 time、gmtime 函数的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 在Visual Studio Code里
- 下一篇: C# vb .net图像合成-多图片叠加