1、不管在windwos、android、ios或其他平台,應用程序在運行時肯定會產生或讀取很多數據(個人觀點:數據才是核心,所有的代碼都是為數據服務的),這些數據肯定要找個地方存放。比如之前破解的xxxx圖片,加密后以bat格式存放在磁盤某個特定的目錄。除了圖片,還是有其他很多數據,比如聊天記錄、語音/視頻消息等。由於這些數據涉及個人隱私,T家官方答復是用戶的這些數據都是點對點傳輸的,從來都沒經過T家的服務器,那么我們平時用xxxx軟件看到的聊天記錄肯定存放在客戶端本地,而不是從服務器下載的!這些數據都存放在本地哪了?以什么形式存放的?
簡單的數據,比如軟件參數配置等,一般都以ini格式存放在文件。但聊天記錄、語音/視頻涉及到大量的非結構化數據,這些是沒法直接存文件的。同時文件的數據組織形式很簡單,就是順序存儲,檢索的時候效率也低,不適合大數據量的查詢。這些數據只能以另一種形式存儲了,這就是: 數據庫!
說起數據庫,IT行業肯定是無人不知、無人不曉!大家聽說和用的最多的就關系型數據庫,比如MySQL、sqlserver、oracle; 近些年大數據和AI火熱,催生了mpp、nosql類的數據庫,比如gbase、vertica、hbase等;這些所有數據庫都有一個同樣的特點,就是很“重”!一般的生產環境,都需要單獨的服務器裝這些數據庫,還會配備專門的運維人員來維護。所以對於普通的客戶端APP,用這些數據庫存放和查詢數據肯定是不行的(不可能讓客戶端的用戶去運維數據庫吧),那么那種數據庫最適合相對輕量的APP來存放和查詢數據了? SQLite孕育而生!
2、SQLite是一個進程內的庫,實現了自給自足的、無服務器的、零配置的、事務性的 SQL 數據庫引擎。它是一個零配置的數據庫,用戶不需要在系統中配置。SQLite 引擎不是一個獨立的進程,可以按應用程序需求進行靜態或動態連接,SQLite 直接訪問其存儲文件,主要優點終結如下:
-
不需要一個單獨的服務器進程或操作的系統(無服務器的)。
-
SQLite 不需要配置,這意味着不需要安裝或管理。
-
一個完整的 SQLite 數據庫(就是.db文件)是存儲在一個單一的跨平台的磁盤文件。
-
SQLite 是非常小的,是輕量級的,完全配置時小於 400KiB,省略可選功能配置時小於250KiB。
-
SQLite 是自給自足的,這意味着不需要任何外部的依賴。
-
SQLite 事務是完全兼容 ACID 的,允許從多個進程或線程安全訪問。
-
SQLite 支持 SQL92(SQL2)標准的大多數查詢語言的功能。
-
SQLite 使用 ANSI-C 編寫的,並提供了簡單和易於使用的 API。
-
SQLite 可在 UNIX(Linux, Mac OS-X, Android, iOS)和 Windows(Win32, WinCE, WinRT)中運行。
對於輕量級的客戶端APP,最需要功能莫過於前4點了:無需單獨服務器、不需要安裝、db文件跨平台、輕量級別不占空間!
3、xxxx存放數據的db文件都在哪了?下面這個目錄:從db文件的字面意思看,有存放聊天記錄的,有存放表情包的,有存放收藏的,有存放媒體的。
先看ChatMsg.db文件,應該是存放聊天記錄的。要不先試試看能不能打開?結果卻是這樣的!
從提示來看,這個文件都不是database文件,這是怎么回事了?難道我們找錯文件了?為了驗證這個想法, 自己先生成一個db文件,里面寫少量數據,比如:
然后和ChatMsg.db比對一下,問題就很明顯了:我自己生成的db文件開頭就是SQLite format 3,這是個明顯的文件頭信息,說明這個文件的格式(其他格式文件諸如jpg、gif等都這樣,在文件頭就標注了文件的格式);而ChatMsg.db頭部都是“亂碼”,其他地方也都是亂碼!這么看就很明顯了:ChatMsg.db是加密的!其實回過頭來想想:xxxx這種裝機量幾十億的國民級的超級APP,怎么可能會明文存儲客戶的關鍵數據了?要是發生了大規模泄露,T廠的股價不得直接腰斬啊!
既然都加密了,為了看到db的內容,肯定是要解密的!解密涉及到兩個關鍵的信息:密鑰和加密方法!目前生產環境下流行的對稱加密方法(非對稱加密算法效率低一些,這里應該不會用的,而且也沒必要保公私鑰,導致維護成本高):XOR和AES;XOR前面加密圖片時用過了,這里大概率會用AES,原因:(1)XOR用明文和密文能得到密鑰,沒有AES安全 (2)聊天記錄這些都是用戶的絕對隱私,安全級別比圖片高多了! 接下來還有一個問題:AES密鑰的長度是多少了?
4、解密數據庫前,先簡單學習一下sqlited的使用接口。在https://www.runoob.com/sqlite/sqlite-c-cpp.html 這里有簡單的C或c++接口的demo代碼,如下:
#include <stdio.h> #include <stdlib.h> #include <sqlite3.h> static int callback(void *data, int argc, char **argv, char **azColName){ int i; fprintf(stderr, "%s: ", (const char*)data); for(i=0; i<argc; i++){ printf("%s = %s\n", azColName[i], argv[i] ? argv[i] : "NULL"); } printf("\n"); return 0; } int main(int argc, char* argv[]) { sqlite3 *db; char *zErrMsg = 0; int rc; char *sql; const char* data = "Callback function called"; /* Open database */ rc = sqlite3_open("test.db", &db); if( rc ){ fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db)); exit(0); }else{ fprintf(stderr, "Opened database successfully\n"); } /* Create SQL statement */ sql = "SELECT * from COMPANY"; /* Execute SQL statement */ rc = sqlite3_exec(db, sql, callback, (void*)data, &zErrMsg); if( rc != SQLITE_OK ){ fprintf(stderr, "SQL error: %s\n", zErrMsg); sqlite3_free(zErrMsg); }else{ fprintf(stdout, "Operation done successfully\n"); } sqlite3_close(db); return 0; }
上面代碼很多,除開各種容錯的代碼,核心代碼就這么幾句:先聲明一個sqlite3的指針對象db,再連接數據庫,並且把連接的相關信息保存到db指針(也有些地方叫句柄)。后續執行各種sql語句(select、delete、update等)都通過這個db指針,所有sql代碼執行完后調用cloesAPI關閉指針對象!在調用cloes函數之前,db這個指針(句柄)一直都有效,通過這個指針能調用exec方法執行任何sql語句!
#include <stdio.h> #include <sqlite3.h> static int callback(void *NotUsed, int argc, char **argv, char **azColName){ for(int i=0; i<argc; i++){ printf("%s = %s\n", azColName[i], argv[i] ? argv[i] : "NULL"); } return 0; } int main(int argc, char **argv){ sqlite3 *db; sqlite3_open(argv[1], &db); sqlite3_exec(db, argv[2], callback, 0, &zErrMsg); sqlite3_close(db); return 0; }
上述代碼經過vs2019編譯后(注意選擇release模式,減少其他代碼的干擾),用IDA打開時main的代碼如下:可以看出call sqlite3_open時push的兩個參數:db是 lea eax, [ebp-8] ;push eax;這個是非常重要的特征:db是個局部變量,本身在棧內,所以可以通過ebp-立即數的方式找到;db本身又是指針,可以保存函數執行的結果,所以可以把地址傳給open函數;后續分析xxxx打開db庫是會用到!
.text:00401090 ; int __cdecl main(int argc, const char **argv, const char **envp) .text:00401090 _main proc near ; CODE XREF: __scrt_common_main_seh+F5↓p .text:00401090 .text:00401090 zErrMsg = dword ptr -0Ch .text:00401090 var_8 = dword ptr -8 .text:00401090 var_4 = dword ptr -4 .text:00401090 argc = dword ptr 8 .text:00401090 argv = dword ptr 0Ch .text:00401090 envp = dword ptr 10h .text:00401090 .text:00401090 push ebp .text:00401091 mov ebp, esp .text:00401093 sub esp, 0Ch .text:00401096 mov eax, ___security_cookie .text:0040109B xor eax, ebp .text:0040109D mov [ebp+var_4], eax .text:004010A0 push esi .text:004010A1 mov esi, [ebp+argv] .text:004010A4 lea eax, [ebp-8] ; sqlite3* db; .text:004010A7 push eax .text:004010A8 mov [ebp+zErrMsg], 0 .text:004010AF push dword ptr [esi+4] ; argv[1] .text:004010B2 call ds:__imp__sqlite3_open .text:004010B8 lea eax, [ebp+zErrMsg] .text:004010BB push eax .text:004010BC push 0 .text:004010BE push offset callback .text:004010C3 push dword ptr [esi+8] .text:004010C6 push [ebp+var_8] .text:004010C9 call ds:__imp__sqlite3_exec .text:004010CF push [ebp+var_8] .text:004010D2 call ds:__imp__sqlite3_close .text:004010D8 mov ecx, [ebp+var_4] .text:004010DB add esp, 20h .text:004010DE xor ecx, ebp ; cookie .text:004010E0 xor eax, eax .text:004010E2 pop esi .text:004010E3 call @__security_check_cookie@4 ; __security_check_cookie(x) .text:004010E8 mov esp, ebp .text:004010EA pop ebp .text:004010EB retn .text:004010EB _main endp
為了進一步熟悉lite相關API,也可以通過調試體驗一下;在IDA中看到main被__scrt_common_main_seh調用了,所以也可以在OD中這里下個斷點方便觀察:
進一步:可以根據call main代碼在這函數內部的偏移,在OD找到main的真正入口后下斷點:
為了方便靜態分析,也可以在edit->segment->rebase program這里把整個exe默認的基址改成OD中實際加載的基址0xBC0000,支持IDA和OD的地址就完全對齊了!
吐槽一下OD這里的寫法: 直接用local.2來表示了,平時都用epb-立即數表示局部變量的,這里都有些不習慣了!
open函數執行完后,ebp-8、也就是db指針的地方出現了一個句柄,這個大概率就是sqlite3的對象了! 這個句柄很重要,后續對db數據庫所有的操作都要通過這個句柄來實現!
繼續順着這個句柄跟蹤內存,這里明顯可以發現單個對象的大小是D4-80=0x54字節;把內存網上滑,還能看到好幾個同樣結構的句柄!
上面鋪墊了這么多,都是為了后續在xxxx中快速找到sqlite3_open函數,這個函數有兩個非常明顯的特征參數:(1)db文件的路徑 (2)sqlite3 *db這個句柄是由lea 的方式保存到寄存器,再傳遞給函數使用的! 這是兩個非常重要的特征,一定要牢記!
5、現在正式開始分析xxxx的sqlite數據庫!數據庫以db文件形式存放在磁盤。xxxx剛開始運行時,肯定會從磁盤讀出來、解密,用戶才能看到聊天記錄等關鍵內容。從磁盤讀文件,win32編程必然涉及到CreateFileW(注意:xxxx全球通用,為了兼容肯定會用w方法),這里先在CreateFileW下斷點:(注意: 本人調試期間失敗了無數次,並非一氣呵成,所以關鍵數據的地址有可能每次都不一樣)
已經開始讀磁盤的文件了,不過這個是log日志,明顯不是我們想要的,直接放過!
用OD的時候出了一些bug,這里換成x32dbg繼續:找到db文件,就是這里了!
從調用堆棧看,有10來個函數。這里沒有啥特殊的技巧,只能挨個都看看,直到第4個函數:
這里非常可疑:lea edx, [ebp-0x14]像極了傳sqlite3 *db的句柄對象;這個句柄經過call處理后,又付給了esi,進一步說明句柄是有用的;先下個斷點試試:
這明顯是這個dll里面的,可以記住偏移:0x7a494cbe-0x79f80000=0x514CBE, 以后調試直接一步到位!
x32dbg出現了異常,繼續換回OD調試:ebp-0x14這里已經生成了句柄,棧上不遠處還有db文件的完整路徑,這里越看越像;
繼續追蹤這個句柄:怎么樣,和我們自己寫的demo的sqlite3 *db句柄是不是很像啊!!! 0x64-0x10=0x54,連長度都是一樣的,這里就可以實錘這就是sqlite3 *db句柄了!后續寫代碼hook的時候,可以直接在0x7A494CC3(也就是call的下一行代碼,已經生成了句柄!寫代碼時需要動態獲取,hook點相對dll基址的偏移是0x7A494CC3 - 0x79f80000 = 0x514CC3)這里下斷點,然后取[ebp-0x14]就是sqlite3的句柄了!還有另一個重要的信息:[ebp-0x24] 數據庫路徑了! 偏移、db句柄的位置、db文件的位置這3個信息非常重要,后續寫代碼要用到!
現在已經確認call 7AA090A0初始化了sqlite3 *db句柄,那么進一步跟蹤進入這個函數瞅瞅:一行一行地跟蹤太累;由於我們需要找到哪些代碼改寫了[ebp-0x14]、也就是sqlite3* db句柄的值,所以對這個地址下個寫入斷點,斷到了這里:這里eax存放了句柄地址,先給地址清零,然后調用7AA0959A函數,所以這個函數有重大“嫌疑”:放過后發現並未改變句柄指針的值,無奈繼續;
當走到這里時:句柄的值被改變了,就是圖中標紅的這兩行代碼!esi存放了句柄的值,此時重點變成了回溯esi是怎么來的了!這里的偏移0x7AA09577-0x79F80000=0xA89577可以記住,下次直接到這里!
繼續往上回溯: 發現好多地方都在讀寫esi,如果下個要搞定出到底是那行代碼生成和句柄,需要繼續逐行分析esi值的改變!
其實到此為止,利用找到的句柄位置完全可以通過調用sqlite3_open、sqlite3_exec函數讀寫db文件了!下次繼續分享怎么通過代碼遠程調用句柄讀寫db的數據!
查找小技巧:
1、右邊是棧視圖:棧本質上也是一塊內存,里面存的都是各種二進制數據,這些數據都有可能是什么數據了?
- 數字從幾到幾萬、幾十萬的:這些數字比較常規,游戲中可能是血量、魔法、距離、坐標、葯品數量等;其他軟件大概率都是常規的數字,具體含義根據軟件業務意義確定;
- 數字很大,4字節至少占用了3個字節表示,這種數字大概率是地址、指針或句柄,怎么區分這3者了?
- 如果是地址:OD會標記返回到xxxxx來自xxxxx:通過棧回溯找call就是這個原理!
- 如果是字符串指針:OD會標時出字符串
- 如果是句柄(或則說結構體/對象指針):OD棧視圖中雙擊這個句柄,內存視圖也會如下標記
心得:
要想逆向做的好,首先要有正向開發的經驗和思維,逆向的時候才知道去哪找關鍵的call!逆向分析目標exe前,最好自己寫個簡單的demo,分析一下核心函數被編譯器翻譯成匯編時的指令,找到這些指令的特征后再去逆向,事半功倍!
參考:
1、https://www.runoob.com/sqlite/sqlite-intro.html SQLIite簡介
2、https://blog.csdn.net/qq_38474570/article/details/96606530 PC xxxx逆向:兩種姿勢教你解密數據庫文件
3、https://bbs.pediy.com/thread-257028.htm PC xxxx逆向分析