Sublime Text 3143 Win32版本暴力破解過程

Sublime Text是一款強大的文本編輯器,在不註冊的情況下也可以使用,但標題欄的未註冊字樣與時不時彈出的nag窗口有時也讓人感覺很不爽,於是嘗試對其註冊過程進行分析與破解。

截至寫本篇文章時,Sublime Text的最新穩定版本爲3143版本。由於之前我們破解過其舊版本,所以對其註冊機制還算比較瞭解(已將之前破解Sublime Text 3126版本時寫的筆記整理成了博文,請見《Sublime Text 3126 Win32版本暴力破解過程》)。如果有心的話,大家不妨對比兩個版本的註冊機制和流程,看看從3126更新到3143版本這一年多的時間裏,作者爲了增大破解的難度做出了哪些努力。畢竟在攻與防的較量中,大家都在進步,我們在提高自己二進制代碼分析能力的同時,其實也有幸見證了軟件作者的成長。

一、第一次嘗試

在上個版本的Sublime中,我們很快就查到了註冊相關的邏輯,然後打開了局面。針對新版本,我們當然希望能夠故伎重演,就看作者給不給我們這個機會了。

首先隨意輸入一串註冊碼,點擊確定後彈出錯誤提示框,此時中斷程序,查看調用棧如下圖。
這裏寫圖片描述
根據MessageBoxW的調用者地址即可回溯到Wrapper函數頭部。
這裏寫圖片描述
從調用棧中可以看到再上幀的返回地址爲0x00449817,爲了保險,我們在Wrapper函數的頭部下斷後,重新輸入一次錯誤的註冊碼看看:
這裏寫圖片描述
試圖繼續回溯時發現不是第一現場(ecx爲0,而主調方是用ecx尋址的)
這裏寫圖片描述
很明顯,這裏把ecx清掉了。於是用IDA的交叉引用功能查到了調用源。emmmm,是JMP過去的,並且在之前清了ecx的值,作者也變猥瑣了。
這裏寫圖片描述
繼續回溯:
這裏寫圖片描述
分析發現ecx像是一個結構指針,其中第二個位置放的就是函數指針,第三個位置放的是傳入的參數。於是利用函數指針地址下條件斷點。顯示從ecx開始,[ECX+4]==0x0044F4E0,在下面代碼中註釋部分標註了下斷點的方法,由於ecx的值是通過esi取到的,於是[ESI+4]==0x0044F4E0,然後esi又是通過eax取到的,於是又更進下[[EAX+8]+4]==0x0044F4E0。果然這些條件斷點都被命中了。

.text:00589A1D                loc_589A1D:
.text:00589A1D 8B 70 08       mov     esi, [eax+8]  ;[[EAX+8]+4]==0x0044F4E0
.text:00589A20 8D 48 10       lea     ecx, [eax+10h]
.text:00589A23 50             push    eax
.text:00589A24 E8 B7 62 00 00 call    sub_58FCE0
.text:00589A29 83 2D F8 30 8A+sub     dword_8A30F8, 10h
.text:00589A30 8B 06          mov     eax, [esi]
.text:00589A32 59             pop     ecx
.text:00589A33 8B CE          mov     ecx, esi    ;[ESI+4]==0x0044F4E0
.text:00589A35 FF 50 04       call    dword ptr [eax+4]    ;[ECX+4]==0x0044F4E0
.text:00589A38 8B 06          mov     eax, [esi]
.text:00589A3A 8B CE          mov     ecx, esi
.text:00589A3C 6A 01          push    1
.text:00589A3E FF 10          call    dword ptr [eax]
.text:00589A40 E8 3D 6B 00 00 call    sub_59058

OD中在這句上下斷:

00589A1D >|>  8B70 08       mov esi,dword ptr ds:[eax+0x8]

再來一次,斷下後Ctrl+A分析一下程序,程序就顯示出了跳轉的來源,這裏十分不巧,有兩點,都下條件斷判定一下:
這裏寫圖片描述
再來一次,嗯,是從下面跳上來的:
這裏寫圖片描述
此時eax中的值是關鍵,當前爲0x03DCA1C0,應該是堆棧的地址。那是什麼時候取出來的呢?
這裏寫圖片描述
哇,全局變量來了,咱們終於落地了。只要有了全局變量,一切都好商量。
這裏寫圖片描述
哈哈,全局就只有一處可以設值的,其他三處都是改寫的。
大膽猜測,現在所在的函數就是從消息到消息響應函數的映射函數,而目標函數就是負責查出二者映射關係的函數。
這裏寫圖片描述
結合IDA發現這些函數是通過消息循環被調用的:

WPARAM sub_58BA83()
{
  signed int v0; // esi
  _DWORD *v1; // eax
  bool v2; // bl
  int v3; // ecx
  _DWORD *v4; // eax
  bool v5; // bl
  MSG Msg; // [esp+Ch] [ebp-64h]
  char v8; // [esp+28h] [ebp-48h]
  char v9; // [esp+30h] [ebp-40h]
  char v10; // [esp+38h] [ebp-38h]
  char v11; // [esp+40h] [ebp-30h]
  int v12; // [esp+48h] [ebp-28h]
  _DWORD *v13; // [esp+50h] [ebp-20h]
  WPARAM v14; // [esp+58h] [ebp-18h]
  int v15; // [esp+6Ch] [ebp-4h]

  v14 = -1;
  MsgHandler();
LABEL_2:
  Msg.hwnd = 0;
  memset(&Msg.message, 0, 0x18u);
  v0 = 0;
  while ( 1 )
  {
    if ( !PeekMessageW(&Msg, 0, 0, 0, 1u) )
    {
      sub_58EFBC(&v13);
      v15 = 0;
      while ( 1 )
      {
        v1 = (_DWORD *)sub_58EF7A(&v11);
        v2 = v13 != (_DWORD *)*v1;
        sub_452FC0(&v11);
        if ( !v2 )
          break;
        sub_589E11(*v13);
        sub_452FA3(&v9);
        sub_452FC0(&v9);
      }
      v15 = -1;
      sub_452FC0(&v13);
      sub_590582();
      if ( v0 == 2 )
        MsgHandler();
      if ( !PeekMessageW(&Msg, 0, 0, 0, 0) )
      {
        sub_58EFBC(&v12);
        v15 = 1;
        while ( 1 )
        {
          v4 = (_DWORD *)sub_58EF7A(&v10);
          v5 = v12 != *v4;
          sub_452FC0(&v10);
          if ( !v5 )
            break;
          v3 = *(_DWORD *)(*(_DWORD *)v12 + 4);
          if ( v3 )
            (*(void (**)(void))(*(_DWORD *)v3 + 24))();
          sub_452FA3(&v8);
          sub_452FC0(&v8);
        }
        v15 = -1;
        sub_452FC0(&v12);
        sub_590582();
        MsgWaitForMultipleObjectsEx(0, 0, 0xFFFFFFFF, 0x1CFFu, 6u);
      }
      goto LABEL_2;
    }
    if ( Msg.message == 18 )
      break;
    switch ( Msg.message )
    {
      case 0x7E9u:
        if ( v0 )
        {
          if ( v0 == 1 )
            v0 = 2;
        }
        else
        {
          MsgHandler();
          v0 = 1;
        }
        break;
      case 0x7EAu:
        v14 = Msg.wParam;
        break;
      case 0x7EBu:
        return v14;
      default:
        if ( (Msg.message == 256 || Msg.message == 257 || Msg.message == 260 || Msg.message == 261)
          && sub_589BFD(Msg.hwnd) )
        {
          if ( sub_58A100(Msg.hwnd, Msg.message, Msg.wParam, Msg.lParam) != 0 )
            TranslateMessage(&Msg);
        }
        else
        {
          TranslateMessage(&Msg);
          DispatchMessageW(&Msg);
        }
        sub_590582();
        break;
    }
  }
  PostQuitMessage(Msg.wParam);
  return v14;
}

可以看到,Sublime的作者自定義了0x7E9消息,在其中完成了錯誤對話框的顯示。並且經過分析,該消息至少還有如下兩種觸發方式:
1. 作者設置的定時器會調用MsgHandler函數
2. 該函數還負責處理其他窗口消息,如窗口獲得焦點等等

因此,在該函數處下斷或在SendMessage/PostMessage等函數上下斷都會有較大的工作量,意味着我們要從衆多幹擾信息中找出真正關注的目標信息。並且即便找到了發送該消息的真身,也可能並不意味着我們找到了註冊相關的邏輯。因爲作者完全可以在二者之間再採用一些猥瑣的方法增加熵值。基於上述分析,我決定放棄這種思路。

第二次嘗試

Sublime的未註冊版本中有一個nag窗口,是在用戶保存文件到一定次數時觸發的。依然採用下斷MessageBoxW的方式,配合棧回溯和IDA靜態分析,結果又來到了上面的0x7E9消息處理函數中。
這裏就不再截圖了,因爲大部分圖都和上面類似。

新思路

在思路二中,我們其實想利用未註冊版Sublime的一些提示信息,如保存時的nag窗口、關於窗口中的未註冊等等、標題欄的未註冊字樣。既然nag窗口走不通,我們就試試後面的吧。
這裏寫圖片描述
搜索字符串:
這裏寫圖片描述
放到IDA裏看看流程圖:
這裏寫圖片描述
可以看到一個典型的分支結構。並且判斷的條件是將一個結構的第一字節是否爲0,如果爲0則進入了未註冊分支;且該結構爲全局結構。因此可對其下硬件訪問斷點:

hr 0x008845E8

先暫時不忙啓用該斷點,而是在註冊對話框中輸入了key後啓用該斷點,然後確認,程序斷在如下位置:
這裏寫圖片描述
回溯即找到0x45075D的函數,依然是IDA結合OD的方法:

.text:0045075D B8 EC 97 76 00 mov     eax, offset loc_7697EC
.text:00450762 E8 69 1B 31 00 call    __EH_prolog
.text:00450767 81 EC 8C 00 00+sub     esp, 8Ch
.text:0045076D 53             push    ebx
.text:0045076E 56             push    esi
.text:0045076F 57             push    edi
.text:00450770 8B F9          mov     edi, ecx
.text:00450772 8D 8D 68 FF FF+lea     ecx, [ebp+lpszKeyUnicode]
.text:00450778 51             push    ecx
.text:00450779 8B 87 E0 01 00+mov     eax, [edi+1E0h]
.text:0045077F 8B 80 B4 00 00+mov     eax, [eax+0B4h]
.text:00450785 8B 88 80 03 00+mov     ecx, [eax+380h]
.text:0045078B E8 3F 9D 04 00 call    GetKey
.text:00450790 33 DB          xor     ebx, ebx
.text:00450792 8D 4D D4       lea     ecx, [ebp+lpszKey]
.text:00450795 8B D0          mov     edx, eax
.text:00450797                ;   try {
.text:00450797 89 5D FC       mov     [ebp+var_4], ebx
.text:0045079A E8 C6 28 07 00 call    ConvertKeyToASCII
.text:0045079F 53             push    ebx
.text:004507A0 6A 01          push    1
.text:004507A2 8D 8D 68 FF FF+lea     ecx, [ebp+lpszKeyUnicode]
.text:004507A2 FF             ;   } // starts at 450797
.text:004507A8                ;   try {
.text:004507A8 C6 45 FC 02    mov     byte ptr [ebp+var_4], 2
.text:004507AC E8 0C 77 FC FF call    FreeString
.text:004507B1 8B 8F D8 01 00+mov     ecx, [edi+1D8h] ; 這裏取到了註冊結構
.text:004507B7 6A 0F          push    0Fh
.text:004507B9 58             pop     eax
.text:004507BA 89 45 9C       mov     [ebp+var_64], eax
.text:004507BD 89 45 B4       mov     [ebp+var_4C], eax
.text:004507C0 8D 45 80       lea     eax, [ebp+pOrgRegInfo]
.text:004507C3 50             push    eax
.text:004507C4 66 89 5D 80    mov     [ebp+pOrgRegInfo], bx ; 這裏ebx是0,因此是在清空該結構的各字段
.text:004507C8 89 5D 84       mov     [ebp+var_7C], ebx
.text:004507CB 89 5D 98       mov     [ebp+var_68], ebx
.text:004507CE 88 5D 88       mov     [ebp+var_78], bl
.text:004507D1 89 5D B0       mov     [ebp+var_50], ebx
.text:004507D4 88 5D A0       mov     [ebp+var_60], bl
.text:004507D7 E8 60 EF FF FF call    CopyRegInfo     ; 這裏將清空後的註冊結構拷貝給全局的註冊結構
.text:004507DC 53             push    ebx             ; size_t
.text:004507DD 6A 01          push    1               ; char
.text:004507DF 8D 4D A0       lea     ecx, [ebp+var_60] ; void *
.text:004507E2 E8 E2 56 FB FF call    sub_405EC9
.text:004507E7 53             push    ebx             ; size_t
.text:004507E8 6A 01          push    1               ; char
.text:004507EA 8D 4D 88       lea     ecx, [ebp+var_78] ; void *
.text:004507ED E8 D7 56 FB FF call    sub_405EC9
.text:004507F2 39 5D E4       cmp     [ebp+var_1C], ebx
.text:004507F5 0F 84 C4 01 00+jz      loc_4509BF
.text:004507FB 8B 97 D8 01 00+mov     edx, [edi+1D8h]     ;這裏取出了全局結構
.text:00450801 8D 4D D4       lea     ecx, [ebp+lpszKey]
.text:00450804 8D 42 01       lea     eax, [edx+1]
.text:00450807 50             push    eax
.text:00450808 8D 45 EC       lea     eax, [ebp+lpParameter]
.text:0045080B 50             push    eax
.text:0045080C 8D 42 04       lea     eax, [edx+4]
.text:0045080F 83 C2 08       add     edx, 8
.text:00450812 50             push    eax
.text:00450813 E8 F6 F1 FF FF call    CheckKey
.text:00450818 8B 8F D8 01 00+mov     ecx, [edi+1D8h] ; 取出全局結構
.text:0045081E 83 C4 0C       add     esp, 0Ch
.text:00450821 8B F0          mov     esi, eax
.text:00450823 83 FE 01       cmp     esi, 1
.text:00450826 0F 94 C2       setz    dl
.text:00450829 88 11          mov     [ecx], dl
.text:0045082B 8B 8F D8 01 00+mov     ecx, [edi+1D8h]
.text:00450831 38 19          cmp     [ecx], bl
.text:00450833 74 13          jz      short loc_450848

其中,CheckKey函數在註冊碼正確時應該返回1,緊接着就是對該返回值的判斷:
這裏寫圖片描述
可以看到,註冊失敗返回的是2。考慮到程序中可能還有其他地方調用註冊驗證函數,我們修改其頭部代碼,讓其直接返回1即可:
下面是原驗證函數的入口:
這裏寫圖片描述
修改以後變成了這樣:
這裏寫圖片描述

此外,通過IDA的靜態分析結合OD動態調試,在註冊流程下面還開啓了一個線程向服務器報告註冊情況:
這裏寫圖片描述
對應的線程是由下面這段代碼開啓的。

.text:00450938
.text:00450938                loc_450938:             ; lpThreadId
.text:00450938 53             push    ebx
.text:00450939 53             push    ebx             ; dwCreationFlags
.text:0045093A FF 75 EC       push    [ebp+lpParameter] ; lpParameter
.text:0045093D 68 73 F7 44 00 push    offset sub_44F773 ; lpStartAddress
.text:00450942 53             push    ebx             ; dwStackSize
.text:00450943 53             push    ebx             ; lpThreadAttributes
.text:00450944 FF 15 1C 61 78+call    ds:CreateThread
.text:0045094A 50             push    eax             ; hObject
.text:0045094B FF 15 3C 63 78+call    ds:CloseHandle
.text:00450951 8B 87 D8 01 00+mov     eax, [edi+1D8h]
.text:00450957 6A 0C          push    0Ch
.text:00450959 80 78 01 00    cmp     byte ptr [eax+1], 0
.text:0045095D 74 08          jz      short loc_450967

在OD中查看如下:
這裏寫圖片描述
本着不打擾作者也不被作者打擾的考量將其幹掉:

首先幹掉線程函數,直接使用頭部ret大法即可。注意這裏是__stdcall調用約定,線程函數傳入一個lpParameter,因此應該爲ret4.
這裏寫圖片描述

下面再把負責CreateThread的那片代碼也幹掉:

這裏寫圖片描述

雖然將傳參、壓棧過程全部用0x90填充了,但是由於CreateThread的某些參數(如函數入口)在可執行程序加載時涉及到重定位,因此還是直接用一個短跳轉略過這一片比較好。至此,sublime破解完成。隨意輸入序列號即可完成註冊。

在啓動過程中,Sublime也會發送註冊信息到驗證服務器:

這裏寫圖片描述

於是將其啓動時連網進行版本檢查的代碼也幹掉。還是在InternetOpenW處下斷點,然後回溯到調用處:0x005A750D,進而回溯到函數頭部,使用ret大法。
這裏寫圖片描述

讓程序啓動即爲破解版

對於懶人來說,連手動註冊這一步都希望能省掉。根據前面的分析,我們知道程序是否註冊是由一個全局結構來控制的。因此在啓動過程中對該全局地址下硬件訪問斷點。看該地址中的值何時被讀取:
在OD中設置硬件訪問斷點:hr 0x008845E8,然後重新運行程序。
很快斷點命中:
這裏寫圖片描述
可以看出,此時全局結構中註冊標誌爲0,而這裏取出了該標誌與0判斷。因此我們將其改爲
or byte ptr [eax], 1
這裏寫圖片描述
這樣無論如何執行完該語句後,該結構中的值都爲1了。再次運行程序,直接就成爲了已註冊版。至此破解完畢。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章