本文是對上一篇記錄《Sublime Text 3143 Win32版本暴力破解過程》的補充之一。
整個破解工作到目前爲止,似乎一切都很順利,隨便輸入一個序列號就能完成註冊了。然而今早起牀刷牙時猛然想起一個細節:Sublime的作者在3126版本中就已經有了對註冊函數進行驗證的環節,故意傳入了一個全0的參數,此時作爲異常情況,註冊函數應該返回代表註冊失敗的值,當初我們第一次破解3126版本的時候就因爲沒發現這點而折戟沉沙。
排查遇坑
一個本就在不斷對抗破解,手段越來越惡劣、套路越來越猥瑣的作者,會在新版本放棄這種方式嗎?我不相信這種奇蹟會發生。於是在IDA中找到註冊函數:0x0044FA0E,Ctrl+X查找交叉引用,可以看到如下地方調用了該函數:
一共五處調用,先看第一處:
嗨呀,看到上面一串全0字符串頓時有種不妙的感覺,嚇得我趕緊打開了OD。在目標位置下斷後運行程序:註冊函數兩次被斷下,第一次傳入了我們之前輸入的key,而第二次,就來到了這裏:
ecx中傳入的是一個字符串對象,其中第一個字段即爲字符串指針:
來看看是何方神聖:
哇,社會是真的險惡。這種驗證碼就算是閉着眼睛也知道是有問題的。調用註冊碼校驗函數完成後,果不其然,作者開始將返回值和1匹配(在Sublime 3143版本中,註冊函數返回1表示註冊成功),果然城裏人套路深啊!
作者的套路1
接着分析,如果我們不問青紅皁白地將註冊函數修改爲無論合適都返回註冊成功,那就中了作者的套路了,下面這段代碼就會執行:
.text:00426DAE 6A 0C push 0Ch ; size_t
.text:00426DB0 E8 6E DF 2F 00 call ??2@YAPAXI@Z ; 對象大小爲12
.text:00426DB5 8B 4D 6C mov ecx, [ebp+6Ch] ; ecx指向全局註冊結構
.text:00426DB8 C7 04 24 00 53+mov [esp+0FCh+var_FC], 75300h
.text:00426DBF 50 push eax
.text:00426DC0 89 48 08 mov [eax+8], ecx ; 新對象第三個成員爲全局對象大小
.text:00426DC3 B9 04 31 8A 00 mov ecx, offset dword_8A3104
.text:00426DC8 89 45 60 mov [ebp+60h], eax ; 新對象保存起來了
.text:00426DCB C7 00 EC E7 7E+mov dword ptr [eax], offset ??_7callback_work_item@@6B@ ; const callback_work_item::`vftable'
.text:00426DD1 C7 40 04 63 6C+mov dword ptr [eax+4], offset sub_426C63
.text:00426DD8 E8 5E 68 17 00 call sub_59D63B ; 壓棧兩個參數,頂到底分別是:新創建對象的地址,0x75300,0xC
.text:00426DD8 ; ecx指向全局地址,疑似爲消息響應
這裏構造的就是一個Handler(我們在第一次嘗試中分析過,其中第三個成員即是參數(全局註冊結構),其中第二個成員即是處理函數),看看其內容:
.text:00426C63 sub_426C63 proc near
.text:00426C63
.text:00426C63 arg_0= dword ptr 4
.text:00426C63
.text:00426C63 8B 44 24 04 mov eax, [esp+arg_0]
.text:00426C67 C6 00 00 mov byte ptr [eax], 0
.text:00426C6A C3 retn
.text:00426C6A sub_426C63
很明顯,被觸發後,該函數會清空全局註冊結構中的已註冊標誌。那麼能不能按照Sublime 3126版本那樣,在註冊函數中判斷edx的值是否爲0或者通過堆棧壓入的參數是否爲0來區分是作者故意測試還是真的在校驗註冊碼呢?
作者的套路2
第三次斷點命中時情況如下:
這時,ECX中字符串地址爲我們之前輸入的驗證碼,EDX爲0,傳入的其他參數情況也是0:
但此時,註冊函數仍然識別出了是正確的註冊碼,應返回1,並且結構偏移18處的整數應不爲0,否則又會註冊一個清空全局註冊標誌的handler上去。
將計就計
偏移18位置的整數是什麼呢?我們並不清楚。但既然放在了全局結構中,想必是與註冊信息有關的。於是對該字段下硬件訪問斷點,然後隨意輸入一串註冊信息:
點擊註冊後,斷在了上述位置。此時edi值爲0,正將0寫入esi+0x10位置,也就是0x00884600位置。在IDA中分析一下:
void __thiscall sub_405EC9(int g_pstRegInfo, char pUnknown, size_t nLength)
{
int _pstRegInfo; // esi
void *v4; // ebx
bool bTrue; // cf
_pstRegInfo = g_pstRegInfo;
if ( pUnknown && *(g_pstRegInfo + 0x14) >= 0x10u )
{
v4 = *g_pstRegInfo;
if ( nLength )
memmove_0(g_pstRegInfo, v4, nLength);
std::_Deallocate(v4, *(_pstRegInfo + 20) + 1, 1u);
}
*(_pstRegInfo + 0x14) = 0xF;
bTrue = *(_pstRegInfo + 0x14) < 0x10u;
*(_pstRegInfo + 0x10) = nLength;
if ( !bTrue )
_pstRegInfo = *_pstRegInfo;
*(_pstRegInfo + nLength) = 0;
}
通過這段我們知道了一個信息:
0x008845E8 | 一字節註冊信息 | |
---|---|---|
0x008845EC | ||
0x008845F0 | 從這開始是pRegInfo,存放用戶名 | 0 |
0x008845F4 | 4 | |
0x008845F8 | 8 | |
0x008845FC | C | |
0x00884600 | 用戶名長度 | 10 |
0x00884604 | 0xF | 14 這裏是否大於16很重要 |
如果0x00884604位置大於等於16,則從0x008845F0位置取出地址,拷貝nLength位到0x008845F0,並且釋放掉原來的內容,然後將0x00884604的值設爲0xF,並將0x00884600的值設置爲nLength,然後將從0x008845F0開始的,第nLength位置爲0。
儘管目前仍然有點迷糊,但很明顯,這裏進行的是字符串操作。如果串長大於等於15,則取其前nLength位複製到0x008845F0,並添加結尾的NULL標識。同時將0x00884600設置爲長度。我高度懷疑這裏是在進行註冊用戶名的提取。如果用戶名很長,那麼0x008845F0存放的指向用戶名的指針,否則就將用戶名直接放在0x008845F0,其最大的容量爲15(預留1位存放結尾的0標誌),然後緊接着0x00884600放其長度。這樣就解釋的通了!
這個猜測可以立即加以驗證,手動修改0x008845F0處的內容如下:
然後查看註冊信息:
哈哈,我們的猜想得到了證實。下面就可以動手改註冊函數的代碼了。
修改註冊函數,實現完美破解
接下來對註冊函數進行修改,實現所謂完美破解,即註冊時輸入用戶名即完成破解,且輸入的用戶名可以在關於窗口查看:
由於程序包含重定位信息,爲了不影響ASLR機制的正常工作,有三點需要注意:
在代碼中不出現絕對地址,要麼需要自己根據某個全局地址(這裏其實是有條件的,因爲註冊信息就是全局存放的)算出偏移,手動修正;否則就得加重定位表項
如果要調用外部API,則要麼直接採用call到IAT的方法(E8+相對偏移),前提是sublime已經導入了該函數
也乾脆直接想辦法找kernel32基址,然後解析其導出表,只要知道LoadLibrary的地址就行,當然,順帶獲取到GetProcAddress的地址就更方便了。但這種方法有被殺軟誤報的風險。
咱們儘量偷懶,註冊函數有幾點需要滿足的:
正常註冊情況下,應該返回1
如果是作者故意傳入的那組錯誤的註冊碼,則應該返回2
正常情況下,獲取用戶輸入,將用戶輸入的前15位作爲註冊用戶名填入(不滿15位的,取相應位數即可)
下面開始修改:
ecx中指向的是用戶輸入的key,edx如果不爲0的話則指向全局註冊信息結構。那麼用戶輸入的key附近有沒有存放其大小呢?如果有的話,咱們就不用自己寫彙編去求了。本着“大海撈針,總不死心”的方針,在周圍轉轉,還真有收穫!
注意到偏移0x10位置就是字符串的長度。此外,在輸入key的過程中,還發現如果輸入的key長度小於等於0xF,則直接存在ECX指向的緩衝區中,否則將其指針存入ECX指向的結構中。
有了上面的信息,咱們就可以來寫一個完美版的註冊函數了。
0044FA0E 55 push ebp
0044FA0F 53 push ebx
0044FA10 56 push esi
0044FA11 57 push edi
0044FA12 B8 FB967600 mov eax,sublime_.007696FB ;這裏有重定位信息,該語句保留不動
0044FA17 8BEC mov ebp,esp
0044FA19 83EC 38 sub esp,0x38
0044FA1C 33C0 xor eax,eax
0044FA1E 40 inc eax ;默認情況下設置返回值爲1(註冊成功)
0044FA1F 85D2 test edx,edx ;是否傳入了全局註冊信息結構
0044FA21 74 1F je short sublime_.0044FA42 ;沒傳入,作者在挖坑,單獨處理
0044FA23 8BFA mov edi,edx ;傳入了,將註冊信息複製爲用戶名
0044FA25 8079 10 0F cmp byte ptr ds:[ecx+0x10],0xF ;比較用戶輸入是否大於15位
0044FA29 7F 08 jg short sublime_.0044FA33 ;大於15位,則[ECX]中爲指針
0044FA2B 8BF1 mov esi,ecx ;小於15位,直接存在ecx指向的緩衝區中
0044FA2D 0FB649 10 movzx ecx,byte ptr ds:[ecx+0x10] ;待拷貝長度設置爲輸入字符串長度
0044FA31 EB 07 jmp short sublime_.0044FA3A
0044FA33 8B31 mov esi,dword ptr ds:[ecx] ;大於15位,取指針爲字符串
0044FA35 B9 0F000000 mov ecx,0xF ;只取15位
0044FA3A FC cld ;通用過程,拷貝字符串到註冊信息中
0044FA3B 894A 10 mov dword ptr ds:[edx+0x10],ecx
0044FA3E F3:A4 rep movs byte ptr es:[edi],byte ptr ds:[esi]
0044FA40 EB 2A jmp short sublime_.0044FA6C ;拷貝完後,跳到退出處
0044FA42 8079 10 4F cmp byte ptr ds:[ecx+0x10],0x4F ;從上面判斷edx處跳來,判斷是不是作者構造的非法key(該key長度爲0x4F)
0044FA46 75 24 jnz short sublime_.0044FA6C ;不是則調到退出處
0044FA48 8B39 mov edi,dword ptr ds:[ecx] ;開始判斷是否是作者故意傳入的非法key
;非法key偏0x26的位置爲一串0,每串0x20個,然後是一個0xA,這樣一共重複9組
0044FA4A 8D7F 26 lea edi,dword ptr ds:[edi+0x26] ;從第一組0開始
0044FA4D 50 push eax ;暫存返回值
0044FA4E B0 30 mov al,0x30 ;找'0'
0044FA50 B9 29010000 mov ecx,0x129 ;最多找0x129次(到達末尾了)
0044FA55 8BF7 mov esi,edi ;暫存edi到esi
0044FA57 F3:AE repe scas byte ptr es:[edi] ;掃描目標串,直到ecx爲0或找到非'0'字符
0044FA59 57 push edi ;暫存edi
0044FA5A 2BFE sub edi,esi ;求'0'串長,
0044FA5C 83FF 21 cmp edi,0x21 ;'0'串長若是0x20(向後挪了一格)
0044FA5F 5F pop edi ;恢復edi的值
0044FA60 75 03 jnz short sublime_.0044FA65 ;'0'串長若是0x20(向後挪了一格)
0044FA62 42 inc edx ;循環計數器自增,能跳到這個分支edx定爲0
0044FA63 ^ EB F0 jmp short sublime_.0044FA55 ;跳回去繼續執行(模擬了一個for循環)
0044FA65 83FA 09 cmp edx,0x9 ;循環結束後,判斷是不是有9組0
0044FA68 58 pop eax ;恢復返回值
0044FA69 75 01 jnz short sublime_.0044FA6C ;如果不是,則是恰巧0x4F的串,返回1
0044FA6B 40 inc eax ;否則就是作者構造的用於釣魚的串,返回2
0044FA6C 8BE5 mov esp,ebp
0044FA6E 5F pop edi
0044FA6F 5E pop esi
0044FA70 5B pop ebx
0044FA71 5D pop ebp
0044FA72 C3 retn
測試效果
保存後進行測試:
輸入註冊信息
查看註冊信息
可以看到輸入的註冊信息出現在了關於中。