solly 发表于 2019-12-20 18:06

160 Crackme 之 157 -- thecodingone.2 的跟踪分析及注册机实现

本帖最后由 solly 于 2019-12-20 18:18 编辑

160 之 157 - thecodingone.2 是一个命令行形式的 Crackme,通过命令行交互方式输入注册信息,并输出结果。


首先看看文件信息:

节的信息如下:

可以看到,这是一个由 Borland C++ 编译的 Crackme,没有加壳,因此跟踪分析也比较容易。


首先运行一下 Crackme,输出如下图:

需要一个 keyfile 文件验证,我们用 OD 载入 Crackme 程序来分析。载入后如下图所示:

可以看到,这一个调用 C++ 库写成的 Crackme,一开始 Crackme 创建了两个文件流(fstream)。并对流进行初始化,如下图所示:

初始化了两个流,文件名为空(NULL)。按 F8 执行,执行到下图所示位置:

可以看出,Crackme 将自身执行文件作为文件流打开了。同时,还需要打开另一个文件 crack.dat ,这个文件就是 Keyfile,目前不存在,我们先创建一个这样的文件,文件内容随意填,如下图所示:


用本文编辑器生成一个 crack.dat,输入一些内容并保存。

如上图所示,crackme 通过 >> 操作符,读入 crack.dat 的内容,并且是作为一个整数数据读入的。按 F8 执行上面代码,进入下面 keyfile 验证循环,如下图所示:

keyfile 的算法也简单,就是对 Crackme 的执行程序进行累加和的计算并与 crack.dat 的内容进行对比。
如上图所示, 中是 crack.dat 内容 78787878 ( 十六进制 0x04B23526)。ESI 中是 crackme 计算的累加校验和0xD80A721A,转成 10 进制为 3624563226。这个 3624563226 就是 crack.dat 的正确内容了。
累加校验和的计算方式为:sum = ∑ xor (i+1)],其中,i=0,1,2,...n-1,n 为文件长度。如下图所示:

具体代码如下:
00401223|.EB 2E                  jmp   short 00401253
00401225|>8D8D 97FEFFFF            /lea   ecx, dword ptr                ;char buff
0040122B|.51                     |push    ecx
0040122C|.8D85 14FFFFFF            |lea   eax, dword ptr                 ;stream_v130 内部成员:输入流, stream_v130.istream
00401232|.50                     |push    eax
00401233|.E8 24620000            |call    istream::get                           ;从流(CrackMe自身文件)中读取取一个字节至(istream::get(char &)),流指针后移一字节
00401238|.83C4 08                  |add   esp, 8
0040123B|.8D95 14FFFFFF            |lea   edx, dword ptr
00401241|.52                     |push    edx
00401242|.E8 9D620000            |call    istream::tellg                         ;取流的读取指针位置, eax
00401247|.59                     |pop   ecx                                    ;eax == 1....
00401248|.0FBE8D 97FEFFFF          |movsx   ecx, byte ptr                 ;ecx == CRACKME, i=0....,最终 ecx = 0xFFFFFFFF, == 0xFF, 当Eof时返回-1
0040124F|.33C1                     |xor   eax, ecx
00401251|.03F0                     |add   esi, eax                               ;sum += (tellg() ^ CRACKME), 最终 sum = 0xD80A721A
00401253|>8B85 D0FEFFFF             mov   eax, dword ptr                ;eax ===> stream_v130
00401259|.F640 0C 01               |test    byte ptr , 1                  ;std::basic_ios::eof(), 最终 == 0x00000003, Badbit | Eofbit
0040125D|.^ 74 C6                  \je      short 00401225
0040125F|.8D95 D0FEFFFF            lea   edx, dword ptr
00401265|.52                     push    edx
00401266|.E8 39580000            call    fstreambase::close                      ;stream_v130.close()
0040126B|.59                     pop   ecx
0040126C|.3BB5 98FEFFFF            cmp   esi, dword ptr                 ;sum < 0x04B23526 (78787878)Crack.dat 文件保存的数字
00401272|.72 08                  jb      short 0040127C                        ;低于跳转, esi = D80A721A = 3624563226 不小于 78787878,不跳转
00401274|.3BB5 98FEFFFF            cmp   esi, dword ptr                 ;sum <= 0x04B23526 (78787878)
0040127A|.76 6B                  jbe   short 004012E7                        ;不高于跳转,因此 Crack.dat 的文件内容为 3624563226 才会跳转到下一步验证,否则退出

如果文件内容校验不对,则输出如下图所示:


显示文件不正确的提示。重新将 crack.dat 的内容改成 3624563226,如下图所示:

重新保存 crack.dat,再次运行到比较累加校验码的位置,如下图所示:

这次就是正确的了。所以 keyfile 文件 crack.dat 的内容就是 3624563226 了。


如果 keyfile 验证通过,则会进入第二个验证,用户名/机构名/序列号 的验证。如下图所示:

首先要求输入用户名称。
如下图所示,第二步验证,需要输入用户名,机构名,序列号等三条数据。

我们在命令行按要求输入相关信息,如下图所示,序列号先随便输入:

输入完后回车,就会进入序列号的验证,如下图所示:

首先判断用户名,机构名的长度,长度都必须大于3,否则会将序列号置”0“,因此,验证也会失败。


长度没有问题,则调用 call    dword ptr (call 00401457)进行序列号验证,该验证函数如下图所示:

最后,输入的序列号(整数)与计算后的序列号进行比较,如下相等则表示序列号正确。而序列号的计算值只与用户名和机构名的第1个字符有关,其它字符没有参与序列号的计算。计算的具体代码如下:
0040146A|.33FF                     xor   edi, edi                              ;int sum = 0;
0040146C|.EB 17                  jmp   short 00401485
0040146E|>0FBE06                   /movsx   eax, byte ptr                   ;int a = (int)(* pName);
00401471|.0FBE13                   |movsx   edx, byte ptr                   ;int d = (int)(* pOrganization);
00401474|.F7EA                     |imul    edx
00401476|.03F8                     |add   edi, eax                               ;sum += a * d;
00401478|.53                     |push    ebx                                    ; /pOrganization ===> "ite123"
00401479|.E8 AE0A0000            |call    strlen                                 ; \eax = strlen(pOrganization)
0040147E|.59                     |pop   ecx
0040147F|.85C0                     |test    eax, eax                               ;无用检查
00401481|.FE03                     |inc   byte ptr                          ;Organization ++, 第1个字符的 ASCII 码值加 1
00401483|.FE06                     |inc   byte ptr                          ;Name ++, 第1个字符的 ASCII 码值加 1
00401485|>56                        push    esi                                    ; /pName ===> "solly"
00401486|.E8 A10A0000            |call    strlen                                 ; \eax = strlen(pName)
0040148B|.59                     |pop   ecx
0040148C|.85C0                     |test    eax, eax                               ;当 Name 递增到 0xFF,再加1溢出为0x00即会退出循环
0040148E|.^ 75 DE                  \jnz   short 0040146E
00401490|.FF75 10                  push    dword ptr                       ; /pSerial = 0x04B23526 (78787878)
00401493|.57                     push    edi                                     ; |calcSerial = 0x0009E501 (648449)
00401494|.E8 08000000            call    checkfunc                               ; \thecodin.checkfunc

由上图可以看到,最后是调用 call checkFunc 进行序列号比较的,该函数如下图所示:


就是一个 cmp 指令进行整数比较,相等则显示成功,不相等则显示失败。由上图可以看到,正确的序列号为:0x0009E501(十进制为 648449)。
上图序列不正确,最后显示如下:

表示序列号不正确。


我们重新运行 Crackme,输入正确的序列号,如下图所示:

这次输出如下,表示序列号正确:



另外,这里説明一下,crackme是由 borland c++ 编译的。其在读取文件流时,当到达 eof 时,其 get(char& ch) 取得 ch 为 -1,并且 tellg() 取值还是文件长度,而我用 Dev-C++ 时进行累加校验和计算时,当到达 eof 时,其 get(char& ch) 取得 ch = 0,并且 tellg() 取值是 -1,不再是文件长度。所以,如果用当前的C++编译器编译的程序计算的校验值不对时,可能是这个问题引起的,需要进行修正,修正方式如下:      //// Dev-C++ 下的修正
      crc = crc - (0 ^ (-1)) + ((-1) ^ 96879);

这个在下面的注册机中有体现,注册机源码如下。
#include <iostream>
#include <fstream>
#include <string>
#include <string.h>

using namespace std;

int getCrcFile(string filename);
int getSN(char * name, char * organ);

int main(int argc, char** argv) {
      
      string filename = "I:\\Downloads\\crack\\157_thecodingone.2\\thecodingone.2.exe";
      
      int crc = getCrcFile(filename);
      
      cout<<"File CRC: "<<(unsigned)crc<<endl;
      
      char pName[] = "solly";
      char pOrgan[] = "ite123";
      
      int sn = getSN(pName, pOrgan);
      cout<<"SN: "<<sn<<endl;
      
      return 0;
}

// file-size: 96879, crc: 3624563226
int getCrcFile(string filename) {
      int crc = 0;
      fstream fs(NULL);
      fs.open(filename.c_str(), ios::in | ios::binary);
      
      char c;
      int i=0;
      do {
                fs.get(c);
                int p = fs.tellg();
                if(++i>=96878) {
                        cout<<"c = "<<(int)c<<", p = "<<p<<endl;
                }
                crc += p ^ (int)c;
      } while(!fs.eof());
      
      /// Dev-C++ 下的修正
      crc = crc - (0 ^ (-1)) + ((-1) ^ 96879);
      
      fs.close();
      
      return crc;
}

int getSN(char * name, char * organ) {
      if((strlen(name) < 3) || (strlen(organ) < 3)) {
                cout<<"Length of name and organ must be more than 3."<<endl;
               
                return -1;
      }
      char a = name;
      char b = organ;
      int sn = 0;
      while(a!=0) {
                sn += (int)a * (int)b;
                a++, b++;
      }
      
      return sn;
}


以上代码由 dev-c++ 调试通过。


内容简单,分析完毕!!!

页: [1]
查看完整版本: 160 Crackme 之 157 -- thecodingone.2 的跟踪分析及注册机实现