羽夏逆向破解日记簿——逆向迅捷录屏的思考

Posted 羽夏的博客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了羽夏逆向破解日记簿——逆向迅捷录屏的思考相关的知识,希望对你有一定的参考价值。

看前必读

  “迅捷录屏”是免费录屏软件,但有VIP功能,而其中的录屏组件是商业软件,本篇文章仅仅介绍 逆向分析过程关于开发软件防止逆向的思考 ,不会提供任何成品破解补丁,仅限用于学习和研究目的,否则,一切后果自负。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述侵害合法权益的内容。如果您喜欢其包含的录屏组件,请支持正版软件,购买注册,得到更好的正版服务如有侵权到您的权益,请联系本人整改。

主角和工具

  • Detect it easy 1.01
  • PE Explorer
  • IDA 7.5
  • X32DBG
  • 迅捷录屏
  • ZDSoft SDK

浅析迅捷录屏

  迅捷屏幕录像工具支持游戏录制、直播录屏,教学视频录制等,保存本地后随时学习。支持多种视频录制和多种格式输出,且声画同步多种使用场景,是一个无论是上班族还是学生党都适用的录屏软件。当然,前面的只是官方的措辞,功能比较强,但如果免费,限制多多,广告多多。我们先看看这个软件:
  打开这个软件后,迎面而来的就是推广会员的广告,毕竟要恰钱嘛,无可厚非:

  关掉这个广告,整体来说界面挺好的,UI设计挺合理,然后看看设置里的东西:

  设置的功能挺多的,但是要修改某些设置,会让你登录。我们看看它的录屏功能:

  上面明码标价,普通用户有诸多限制,吧啦吧啦。先不管,我们来看看录屏效果:

  一个倒计时,挺合理:

  然后就进入录屏阶段,自己感兴趣测试一下,结束录屏后我们看看视频:

  视频挺清晰的,但是右下角有一个万恶的水印。

深入迅捷录屏

  接下来就深入看看它到底是怎样实现功能的,可以看到它的安装内容有大量不属于迅捷录屏的东西,比如下面图片所示的东西:

  DuiLib肯定不是迅捷的,这个是一个DirectUI库,在GithubMIT协议开源,不过有坑,或多或少有Bug。应该是互盾公司基于该库进行二次开发修补得到。
  ffmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。采用LGPL或GPL许可证。它提供了录制、转换以及流化音视频的完整解决方案。是开源的,遵守LGPL/GPL协议的,如果使用这个东西就必须带上它的License,如果你的项目用到它的代码就必须开源。对于该款录屏程序,最起码应该带上它的License以表示尊重开源成果,然而翻遍整个文件夹,都没找到。
  最可疑的就是下面的两个文件夹:

  根据名称命名,似乎不属于互盾公司开发的产品,点击去看看任意一个Dll,都不是互盾公司的数字签名,如下是其中一个:

  看样子是某深圳公司的,我们从网上查查看这个组件是何方神圣。

  点进去看看,看到logo正是数字签名中的ZD SOFT,对这套组件的开发者和来源一定程度上的验证,我们还看到了下面的表格:

  并且这个SDK价格不菲,属于终身制购买商业使用权:

  然后我们下载它的SDK,下载是免费的,解压后有三个版本,我们随便看一个,发现和那个录屏软件的东西是一模一样的:

  根据上面的价格我们也明白,互盾公司为什么卖这个略高的价钱,为什么也有终身制会员这个玩意,毕竟这个SDK也挺贵的:

  至此,我们对这款录屏软件有了一个猜测:互盾公司向紫迪公司购买了该SDK,然后基于开源项目进行构建,使用DuiLib作为界面开发库,使用C++语言进行开发,如果是正确的,所有的技术细节基本都暴露了出来。
  好,我们老套路分析,用Detect it easy查一查:

  根据上图结果证实了我的“使用C++语言进行开发”猜想,由于没加壳,直接拖到IDA看一看,等待分析完毕。
  查一查导入表,发现有大量的DuiLib相关的函数,坐实了界面库就是它:

  然后继续检查是不是用的上述分析的录屏SDK

  根据上面的搜索,我们基本证实了所有的猜想,甚至扒出了它的初始化录屏SDK的相关代码和它购买的Key,为了保护人家的合法权益,故打码处理:

  既然是基于DuiLib开发的,找它的所属文件也没找到它的UI界面皮肤文件,我们用PE Explorer查看一下资源,发现可疑部分:

  因为DuiLib的皮肤文件是可以使用压缩包嵌入程序资源使用,并且看到里面有PK字样,十分有可能是界面资源包,另存用十六进制查看器查看一下:

  可以看出,这个文件很符合zip格式,然后改后缀尝试打开:

  我们成功的把它的皮肤资源提取出来,使用Github上的皮肤编辑器,能够正常打开:

分析录屏 SDK

  既然人家是使用录屏SDK,继续分析那个软件就没意思了,基本分析到这里自己就能重写一个和它一模一样的软件。如果使用录屏SDK,就必须得到其Key,当然你可以购买,尊重他人的开发成果。对于学习软件逆向的初学者,自然想看看里面的验证过程。如果能够你想出来,想想为什么能够被他人破解,对自己以后的软件开发成果保护提供反制思路。注意,不得将本篇文章的逆向结果用于非法用途,仅供学习思考,请尊重他人的劳动成果,如造成的损失,本人概不负责。
  既然函数都是ScnLib.dll导出的,作为破解者的身份,我肯定想要搞一个功能最全的,那就选ScnLibMax版本的,老套路,先用Detect it easy查一下:

  既然是逆向分析了,得到Key的算法,就必须从注册函数入手,为了方便观看。其伪代码如下:

int __stdcall ScnLib_SetLicenseW(LPCSTR pcszName, LPCSTR pcszEmail, LPCSTR pcszKey)
{
  struct AFX_MODULE_STATE *v3; // eax
  int result; // eax
  int v5[2]; // [esp+Ch] [ebp-8h] BYREF

  v3 = sub_100351ED();
  AFX_MAINTAIN_STATE2::AFX_MAINTAIN_STATE2(v5, v3);
  AfxGetModuleState();
  result = sub_10014A30(pcszName, pcszEmail, pcszKey);
  *(v5[1] + 4) = v5[0];
  return result;
}

  可以看出,它调用了sub_10014A30这个子函数,然后根据返回结果来提示是否注册成功,这个是这个SDK的说明:

If your license key is validated and enabled successfully, the return value is TRUE. Otherwise, the return value is FALSE.

  好,我们点击去看看这个函数是什么样子:

int __stdcall sub_10014A30(LPCSTR pcszName, LPCSTR pcszEmail, LPCSTR pcszKey)
{
  int res; // edi
  int v4; // eax
  wchar_t *v6; // [esp+10h] [ebp-14h] BYREF
  wchar_t *Source; // [esp+14h] [ebp-10h] BYREF
  int v8; // [esp+20h] [ebp-4h]

  res = 0;
  CString::CString(&Source, pcszName);
  v8 = 0;
  CString::CString(&v6, pcszEmail);
  LOBYTE(v8) = 1;
  CString::CString(&pcszName, pcszKey);
  LOBYTE(v8) = 2;
  CString::TrimLeft(&Source);
  CString::TrimRight(&Source);
  CString::TrimLeft(&v6);
  CString::TrimRight(&v6);
  CString::MakeLower(&v6);
  CString::TrimLeft(&pcszName);
  CString::TrimRight(&pcszName);
  CString::MakeUpper(&pcszName);
  wcscpy(&Destination, Source);
  wcscpy(&String1, v6);
  sub_1002C8D0(pcszName);
  v4 = sub_1002CF30(&word_1004E890);
  LOBYTE(v8) = 1;
  if ( v4 && dword_10051508 >= 300 )
  {
    res = 1;
    CString::~CString(&pcszName);
    LOBYTE(v8) = 0;
    CString::~CString(&v6);
    v8 = -1;
    CString::~CString(&Source);
  }
  else
  {
    CString::~CString(&pcszName);
    LOBYTE(v8) = 0;
    CString::~CString(&v6);
    v8 = -1;
    CString::~CString(&Source);
    sub_1002C8D0(0);
  }
  sub_10017C30(6u, 0);
  return res;
}

  然后我们注意到最关心的语句:

if ( v4 && dword_10051508 >= 300 )

  注册是否成功,完全由这两个变量决定,我们先看看dword_10051508的引用,看看有没有修改它的:

  不幸的是,压根没有。肯定有一个时刻,进行校验的时候,修改了这个值。
  我们可以看出,v4的结果跟word_1004E890这个全局变量有关系,为了方便观看,命个名:

  wcscpy(&UserName, Source);
  wcscpy(&UserEmail, v6);
  sub_1002C8D0(info, pcszName);
  isreg = CheckReg(info);
  LOBYTE(v8) = 1;
  if ( isreg && flag >= 300 )
  {
    res = 1;
    CString::~CString(&pcszName);
    LOBYTE(v8) = 0;
    CString::~CString(&v6);
    v8 = -1;
    CString::~CString(&Source);
  }

  细心的你可能会发现sub_1002C8D0怎么这次有两个参数了吗,怎么成了两个?是因为我点进去了CheckReg,然后返回重新生成伪代码,注意不要过于依赖F5
  根据猜测,sub_1002C8D0这个函数应该是生成与注册信息相关的东西,然后调用CheckReg进行检查,我们看看这个函数到底干了啥:

wchar_t *__thiscall sub_1002C8D0(int info, LPCSTR inputkey)
{
  wchar_t *v3; // ebp
  wchar_t *result; // eax
  int v5; // edi
  wint_t v6; // si
  bool v7; // zf
  int v8; // esi
  int v9; // [esp+10h] [ebp-4h]

  v3 = (info + 10852);
  result = 0;
  *(info + 11384) = 0;
  memset((info + 10852), 0, 0x208u);
  if ( inputkey )
  {
    v9 = 0;
LABEL_3:
    v5 = 0;
    while ( 1 )
    {
      v6 = *inputkey;
      result = inputkey + 1;
      v7 = *inputkey == 0;
      inputkey = (inputkey + 2);
      if ( v7 )
        break;
      if ( iswdigit(v6) || iswalpha(v6) )
        *(info + 2 * wcslen(v3) + 10852) = v6;
      else
        --v5;
      if ( ++v5 >= 5 )
      {
        v8 = v9 + 1;
        if ( v9 + 1 < 5 )
          *(info + 2 * wcslen(v3) + 10852) = 45;
        ++v9;
        if ( v8 >= 5 )
          return wcsupr(v3);
        goto LABEL_3;
      }
    }
  }
  return result;
}

  注意,上面的结果是经过我函数命名处理的,为什么在第二个参数命名为inputkey而不是注册名相关的吗?因为如下代码:

CString::CString(&pcszName, pcszKey);

  在调用该函数之前pcszName已不再是所谓的名字了,而是你输入的注册码了,故使用该名称,可以说,这个所谓的注册名压根没用到。
  由于用到的是全局变量,这个全局变量肯定是个结构体,我们先看看在哪里:

.data:1004E890 ; WCHAR info[4906]
.data:1004E890 info            dw ?                    ; DATA XREF: sub_10013E70+11↑o
.data:1004E890                                         ; unknown_libname_1↑o ...
.data:1004E892                 db    ? ;
.data:1004E893                 db    ? ;
.data:1004E894                 db    ? ;
.data:1004E895                 db    ? ;

  为了保持良好的可读性,先把它变成个数组,这肯定不是数组那么简单:

.data:1004E890 ; WCHAR info[4906]
.data:1004E890 info            dw 132Ah dup(?)         ; DATA XREF: sub_10013E70+11↑o

  接上续,可以看出这个函数是把输入的注册码进行整理,成XXXXX-XXXXX-XXXXX-XXXXX-XXXXX的只包含数字和字母的大写形式,放到info + 10852这个地址中,我们看看这个对应的是什么东西,经过计算在下面的位置:

.data:100512F4 unk_100512F4    db    ? ;               ; DATA XREF: sub_10002CE0+6E↑o
.data:100512F4                                         ; sub_10014940+39↑o
.data:100512F5                 db    ? ;
.data:100512F6                 db    ? ;

  这个地方正是我们放经过整理的注册码,给个名字,然后边车数组,最终看到下面的结果:

.data:1004E890 ; WCHAR info[4906]
.data:1004E890 info            dw 132Ah dup(?)         ; DATA XREF: sub_10013E70+11↑o
.data:1004E890                                         ; unknown_libname_1↑o ...
.data:10050EE4 ; wchar_t UserName
.data:10050EE4 UserName        dw 104h dup(?)          ; DATA XREF: sub_10002CE0+78↑o
.data:10050EE4                                         ; sub_10014940+43↑o ...
.data:100510EC ; wchar_t UserEmail
.data:100510EC UserEmail       dw 104h dup(?)          ; DATA XREF: sub_10002CE0+73↑o
.data:100510EC                                         ; sub_10014940+3E↑o ...
.data:100512F4 RegCode         db 208h dup(?)          ; DATA XREF: sub_10002CE0+6E↑o
.data:100512F4                                         ; sub_10014940+39↑o
.data:100514FC dword_100514FC  dd ?                    ; DATA XREF: sub_10017DF0+5A↑r
.data:10051500                 db    ? ;
.data:10051501                 db    ? ;
.data:10051502                 db    ? ;
.data:10051503                 db    ? ;
.data:10051504 dword_10051504  dd ?                    ; DATA XREF: sub_10017DF0+93↑r
.data:10051504                                         ; sub_10017DF0+144↑r
.data:10051508 flag            dd ?                    ; DATA XREF: sub_1000F410+F7↑r
.data:10051508                                         ; sub_10014940+21↑r ...
.data:1005150C dword_1005150C  dd ?                    ; DATA XREF: sub_10015390+5C↑w
.data:1005150C                                         ; sub_10015400+3↑w ...

  信息的你可能会发现我们之前命好名字的flag在这里,也就是info + 0x2C78的位置,在之后的分析要十分注意。
  然后看看最终Boss函数:

BOOL __thiscall CheckReg(int this)
{
  bool v2; // zf
  const wchar_t *v3; // ebx
  wchar_t *v4; // eax
  wchar_t *v5; // eax
  int v6; // eax
  int v7; // esi
  wchar_t Destination; // [esp+8h] [ebp-2000h] BYREF
  char v10[8188]; // [esp+Ah] [ebp-1FFEh] BYREF
  __int16 v11; // [esp+2006h] [ebp-2h]

  v2 = *(this + 0x285C) == 0;
  v3 = (this + 0x285C);
  *(this + 11384) = 0;
  if ( !v2 && *(this + 0x2A64) )
  {
    Destination = 0;
    memset(v10, 0, sizeof(v10));
    v11 = 0;
    v4 = wcscpy(&Destination, this);
    v5 = wcsrchr(v4, 0x5Cu);
    wcscpy(v5 + 1, v3);
    *(this + 0x2C78) = sub_1002CD80(1000, &Destination);
  }
  v6 = *(this + 0x2C78);
  if ( v6 <= 0 || v6 > 1000 )
    sub_1002C8D0(this, 0);
  if ( *(this + 0x2C70) )
    sub_1002CAE0(this);
  v7 = *(this + 0x2C78);
  return v7 > 0 && v7 <= 1000;
}

  如果眼尖,你就会发现一个华点:

*(this + 0x2C78) = sub_1002CD80(1000, &Destination);

  点进去看看,经过轻微的重命名得到如下结果:

int __thiscall sub_1002CD80(int this, int a2, wchar_t *String)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  result = 0;
  if ( a2 > 0 )
  {
    MultiByteStr = 0;
    memset(v10, 0, sizeof(v10));
    if ( sub_1002CBC0(&MultiByteStr, String) )
    {
      LOWORD(WideCharStr) = 0;
      memset(&WideCharStr + 2, 0, 0x40u);
      LOWORD(Destination) = 0;
      memset(&Destination + 2, 0, 0x204u);
      v12 = 0;
      wcscpy(&Destination, (this + 0x2A64));
      for ( i = 0; i < a2; ++i )
      {
        MultiByteToWideChar(0, 0, &MultiByteStr, -1, &WideCharStr, 33);
        sub_1002CBC0(&MultiByteStr, &WideCharStr);
        for ( j = 0; j < 32; ++j )
        {
          if ( !iswalpha(*(&WideCharStr + j))
            && (!(j % 2) || (*(&WideCharStr + j) & 0x80000001) == 0)
            && (j % 2 || (*(&WideCharStr + j) & 0x80000001) != 0) )
          {
            v7 = (i + j + *(&WideCharStr + j)) % 20;
            *(&WideCharStr + j) = v7 + 71;
            if ( v7 == 8 )
              *(&WideCharStr + j) = 48;
            if ( *(&WideCharStr + j) == 73 )
              *(&WideCharStr + j) = 49;
            if ( *(&WideCharStr + j) == 90 )
              *(&WideCharStr + j) = 50;
          }
        }
        sub_1002C8D0(this, &WideCharStr);
        if ( !wcscmp((this + 0x2A64), &Destination) )
          break;
      }
      result = i + 1;
    }
    else
    {
      result = -1;
    }
  }
  return result;
}

  从上面的伪代码可以看出,sub_1002CBC0函数是十分重要的函数,点击去看看,由于伪代码有些错误,经过调整:

int __stdcall sub_1002CBC0(char *Buffer, wchar_t *String)
{
  size_t v2; // edi
  CHAR *v3; // eax
  CHAR *v4; // esi
  char v6[16]; // [esp+4h] [ebp-B8h] BYREF
  char v7[152]; // [esp+18h] [ebp-A4h] BYREF
  int v8; // [esp+B8h] [ebp-4h]

  if ( !String || !*String )
    return 0;
  v2 = wcslen(String);
  v3 = operator new(v2 + 1);
  v4 = v3;
  if ( v3 )
  {
    WideCharToMultiByte(0, 0, String, -1, v3, v2 + 1, 0, 0);
    sub_1002D010(v7);
    *&v6[1] = 0;
    *&v6[5] = 0;
    *&v6[9] = 0;
    v8 = 0;
    *&v6[13] = 0;
    v6[0] = 0;
    v6[15] = 0;
    sub_1002D080(v4, v2);
    sub_1002D140(v6);
    sprintf(
      Buffer,
      "%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
      v6[0],
      v6[1],
      v6[2],
      v6[3],
      v6[4],
      v6[5],
      v6[6],
      v6[7],
      v6[8],
      v6[9],
      v6[10],
      v6[11],
      v6[12],
      v6[13],
      v6[14],
      v6[15]);
    v8 = -1;
    nullsub_2(v7);
    operator delete(v4);
  }
  return 1;
}

  可以看出,这个函数肯定返回为1。但是生成的Buffer是十分重要的东西,看样子是长度为32的普通字符串,对于以后的解密十分重要,这些东西说明了v6的重要性,而它与sub_1002D140有重要关系,点击去看看:

int __thiscall sub_1002D140(_DWORD *this, int a2)
{
  _DWORD *v3; // edi
  int v4; // ecx
  unsigned int v5; // eax
  char v7; // [esp+8h] [ebp-8h] BYREF
  int v8; // [esp+9h] [ebp-7h]
  __int16 v9; // [esp+Dh] [ebp-3h]
  char v10; // [esp+Fh] [ebp-1h]

  v3 = this + 4;
  v8 = 0;
  v9 = 0;
  v7 = 0;
  v10 = 0;
  sub_1002DB20(&v7, this + 4, 8);
  v4 = 56;
  v5 = (*v3 >> 3) & 0x3F;
  if ( v5 >= 0x38 )
    v4 = 120;
  sub_1002D080(this + 22, v4 - v5);
  sub_1002D080(&v7, 8);
  sub_1002DB20(a2, this, 16);
  return sub_1002D030(this);
}

  一看是两个函数,不明觉厉,返回去,重新反编译一下:

int __stdcall sub_1002CBC0(char *Buffer, wchar_t *String)
{
  size_t v2; // edi
  CHAR *v3; // eax
  CHAR *v4; // esi
  char v6[16]; // [esp+4h] [ebp-B8h] BYREF
  _DWORD v7[38]; // [esp+18h] [ebp-A4h] BYREF
  int v8; // [esp+B8h] [ebp-4h]

  if ( !String || !*String )
    return 0;
  v2 = wcslen(String);
  v3 = operator new(v2 + 1);
  v4 = v3;
  if ( v3 )
  {
    WideCharToMultiByte(0, 0, String, -1, v3, v2 + 1, 0, 0);
    sub_1002D010(v7);
    *&v6[1] = 0;
    *&v6[5] = 0;
    *&v6[9] = 0;
    v8 = 0;
    *&v6[13] = 0;
    v6[0] = 0;
    v6[15] = 0;
    sub_1002D080(v4, v2);
    sub_1002D140(v7, v6);
    sprintf(
      Buffer,
      "%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
      v6[0],
      v6[1],
      v6[2],
      v6[3],
      v6[4],
      v6[5],
      v6[6],
      v6[7],
      v6[8],
      v6[9],
      v6[10],
      v6[11],
      v6[12],
      v6[13],
      v6[14],
      v6[15]);
    v8 = -1;
    nullsub_2(v7);
    operator delete(v4);
  }
  return 1;
}

  突然发现这个v6太花心了,竟然和v7又扯上关系了,点击去看看:

void *__thiscall sub_1002D010(void *this)
{
  sub_1002D030(this);
  return this;
}

  还没看到底裤,继续跟进去:

int __thiscall sub_1002D030(_DWORD *this)
{
  int result; // eax

  result = 0;
  this[5] = 0;
  this[4] = 0;
  *this = 1732584193;
  this[1] = -271733879;
  this[2] = -1732584194;
  this[3] = 271733878;
  memset(this + 6, 0, 0x40u);
  memset(this + 22, 0, 0x40u);
  *(this + 88) = 0x80;
  return result;
}

  看到这里,是不是蒙圈了,这是啥玩意,弄成16进制看看:

int __thiscall sub_1002D030(_DWORD *this)
{
  int result; // eax

  result = 0;
  this[5] = 0;
  this[4] = 0;
  *this = 0x67452301;
  this[1] = 0xEFCDAB89;
  this[2] = 0x98BADCFE;
  this[3] = 0x10325476;
  memset(this + 6, 0, 0x40u);
  memset(this + 22, 0, 0x40u);
  *(this + 88) = 0x80;
  return result;
}

  别说这个挺有规律的,this搞完后在内存中就是如下形式:

0x01 0x23 0x45 0x67 0x89 0xAB 0xCD 0xEF
0xFE 0xDC 0xBA 0x98 0x76 0x54 0x32 0x10

  有一说一,挺有规律,但不知道这串数字是干啥的,然后sub_1002D140这里面的函数吧,看也看不懂,蒙圈了,继续不了了。
  学过或接触算法的人,很快能够意识到这个是魔数,我们用IDA的插件Findcrypt查一查结果:

  这个插件指明是MD5算法,什么是MD5算法请自行百度,它指向的地址正好是我们的函数。也就是说,sub_1002CBC0这个函数会将输入的String参数进行MD5运算,取出运算的字节数组的前16个进行格式化成32长度的大写字符串导出给Buffer,那么我们可以将其命名为GetMD5Code,剩下的就好好搞了,总结一下sub_1002CD80函数的具体流程:

  如果flag >= 300 && flag < 1000,就说明注册成功。为什么还要小于1000呢?是由于我们调用这个函数a2就是1000。这个SDK还有显示注册信息的函数ScnLib_About,里面的sub_10014940函数也佐证了这点:

void sub_10014940()
{
  int v0; // [esp+0h] [ebp-14h] BYREF
  unsigned __int16 *v1; // [esp+4h] [ebp-10h] BYREF
  int v2; // [esp+10h] [ebp-4h]

  CString::CString(&v0);
  v2 = 0;
  if ( flag <= 0 || flag > 1000 )
    CString::Format(&v0, 0x3EAu);
  else
    CString::Format(&v0, 0x3E9u, &UserName, &UserEmail, RegCode);
  CString::CString(&v1);
  LOBYTE(v2) = 1;
  CString::Format(&v1, aSDD0DSSS, aZdSdk, 1, 1, 6, asc_10046F30, aHrcgBG, v0);
  AfxMessageBox(v1, 0x40u, 0);
  LOBYTE(v2) = 0;
  CString::~CString(&v1);
  v2 = -1;
  CString::~CString(&v0);
}

  至此,此完整的验证流程就这些了。其中有一个东西还没有具体分析,就是v4 = wcscpy(&Destination, this);当中的this地址存储的是啥,为了方便分析,直接用动态分析的方式进行,怎么动态调试获得我就不说了,需要使用这个SDK的进程进行调试,而迅捷录屏就可以当作小白鼠,你会得到下面的字符串:Software\\ZD Soft\\Screen Recorder SDK\\。综上,我们的分析就结束了。

注册机编写

  既然都分析到这里了,就C++写个注册机吧。由于其中使用了MD5算法,我们不可能自己写,直接Github大法,找到了一个项目:

  然后创建一个控制台工程,把里面的文件包含进去,就可以愉快的编码了:

#include <iostream> 
#include <algorithm>  
#include "md5.h"

using namespace std;

int main()
{
  string str = "Software\\\\ZD Soft\\\\Screen Recorder SDK\\\\";
  string name;
  string email;
  string regcode;
  MD5* md5;

  int f;

  cout << "请输入注册用户名:" << endl;
  cin >> name;

  cout << endl << "请输入注册邮箱:" << endl;
  cin >> email;
  str.append(email);

  cout << endl << "请输入密钥计算递归深度(整数:300 - 999):  " << endl;
  cin >> f;

  if (f < 300)
  {
    f = 300;
    cout << endl << "【警告】由于输入的数字小于合法区间,故选择最小深度为 300 !" << endl;
  }

  if (f > 999)
  {
    f = 999;
    cout << endl << "【警告】由于输入的数字大于合法区间,故选择最大深度为 999 !" << endl;
  }

  for (int i = 0; i < f; i++)
  {
    md5 = new MD5(str);
    str = md5->toStr();
    delete md5;
    str.erase(32);
    transform(str.begin(), str.end(), str.begin(), toupper);
    regcode = str;

    for (int j = 0; j < 32; j++)
    {
      auto ch = str[j];

      if (!isalpha(ch) && (!(j & 1) || !(ch & 1)) && ((j &  1) || (ch & 1)))
      {
        auto c = (i + j + ch) % 20 + 71;

        switch (c)
        {
          case \'O\':
            regcode[j] = \'0\';
            break;
          case \'I\':
            regcode[j] = \'1\';
            break;
          case \'Z\':
            regcode[j] = \'2\';
            break;
          default:
            regcode[j] = c;
            break;
          
      }
    }
  }

  cout << endl << "==========注册信息==========" << endl
    << "注册用户名:" << name << endl
    << "注册邮箱:" << email << endl << "注册码:"
    << regcode.substr(0, 5) << \'-\' << regcode.substr(5, 5)  << \'-\'
    << regcode.substr(10, 5) << \'-\' << regcode.substr(15,   5) << \'-\' << regcode.substr(20, 5) << endl
    << "=========================" << endl << endl;

  system("pause");
  return 0;
} 

  注意:上面的代码仅限用于学习和研究目的,否则,一切后果自负。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述侵害合法权益的内容。如果您喜欢其包含的录屏组件,请支持正版软件,购买注册,得到更好的正版服务

小结

  这个或许是我写的最长的一篇分析文,通过这篇文章,我们认识到DuiLib在项目当中的应用,并了解了MD5算法在注册码的应用。这仅仅是一个算法应用的缩影。我们可以看到迅捷录屏没能保护好自己的License,使自己的Key被泄露。迅捷录屏的强大并不是因为自身的强大,而是由于录屏SDK的强大,本篇分析总结至此。

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
本文来自博客园,作者:寂静的羽夏 ,一个热爱计算机技术的菜鸟
转载请注明原文链接:https://www.cnblogs.com/wingsummer/p/15680444.html

以上是关于羽夏逆向破解日记簿——逆向迅捷录屏的思考的主要内容,如果未能解决你的问题,请参考以下文章

羽夏逆向指引——注入

JavaScript(JS)逆向工具Tampermonkey用法 (JS Hook代码逆向破解)

Wintel机器代码反逆向(C/C++反逆向破解)

软件录密码怎么解

Unity单机手游逆向破解思路(仅供学习参考,禁止用于非法行为)

IDA Pro代码破解揭秘的编辑推荐