ZRSoft ITEXAMv5逆向

ZRSoft ITEXAMv5逆向

逆向ZR微机考试系统, 信息已脱敏处理

今年初二微机也考完了啊...... 也该翻出来这玩意了......

谨以此文, 追忆我将要过去的初中三年. 与那曾经一刹那的美好.

引言

可能一定会用到的工具: xspy-v0.3 IDA WindowsXP虚拟机 Win7虚拟机 (注意, 不同虚拟机EXE基址会有所不同, Windows XP地址与IDA显示一致)

呃, 由于没啥本事, 所以写的逆向之间并不连贯(能逆出啥就写啥), 不过总而言之凑活看吧.

文章内容已做脱敏处理

服务端 (考场管理系统examspot)

基本信息 (来自Detect It Easy):

PE32
    操作系统: Windows(XP)[I386, 32 位, GUI]
    链接程序: Microsoft Linker(6.00.8447)
    编译器: Microsoft Visual C/C++(19.16.27045)[C++]
    语言: C++
    库: SQLite
    工具: Visual Studio(6.0)
    调试数据: Binary[偏移=0x0066a658,大小=0x45]
        调试数据: PDB file link(7.0)

可知本程序(客户端同)为MFC程序

登录绕过

此为考场管理系统登录页面

按钮事件函数

使用xspy获取登录按钮ID(0403), 在窗体上获取到其ID对应的onCommandFunc地址为0x00595820 (XP)

分析&Patch

// 登录函数 (onCommand) 真的, IDA自动给起的名有点太过抽象了些
void __thiscall sub_595820(int *lpParameter)
{
  /* 类型定义全省略*/
  
  // 连数据库, 查信息
  if ( (unsigned __int8)sub_594A50(v73, v74) == 1 )
  {
    sub_591750(0);
    n2 = 0;
    if ( lpParameter )
      v92 = lpParameter[8];
    else
      v92 = 0;
    v93 = 1;
    if ( CDialog::DoModal((CDialog *)v91) != 1 )
    {
      n10 = 0;
      n12_1 = 0;
      ThreadId = (DWORD)&n12_3;
      v3 = sub_7019FC(v2);
      if ( !v3 )
        sub_49DD90(-2147467259);
      n12_3 = (CHAR *)((*(int (__thiscall **)(int))(*(_DWORD *)v3 + 12))(v3) + 16);
      LOBYTE(n2) = 1;
      if ( !rsrc_71F5E0(&n12_3, (HRSRC)&hResInfo_) )
        sub_49FF70((void *)&hResInfo_, 0x5Cu);
      LOBYTE(n2) = 0;
      sub_599A30(n12_3, n12_1, n10);
      (*(void (__thiscall **)(int *))(*lpParameter + 384))(lpParameter);
    }
    n2 = -1;
    sub_71DA13((CWnd *)v99, v73, v74);
    sub_6C8F20(v98);
    sub_4B0B10(v97);
    sub_4B0B10(v96);
    sub_4B0B10(v95);
    v4 = (_DWORD *)(v94 - 16);
    if ( _InterlockedDecrement((volatile signed __int32 *)(v94 - 4)) <= 0 )
      (*(void (__thiscall **)(_DWORD, _DWORD *))(*(_DWORD *)*v4 + 4))(*v4, v4);
    CDialog::~CDialog((CDialog *)v91);
  }
  
  // 一种特殊登录方式, 啊也不算特殊, 就只要我们走这个分支就能无密码登录
  if ( IsWindowVisible((HWND)lpParameter[1024]) )
  {
    v5 = sub_7019FC(v73);
    if ( !v5 )
      sub_49DD90(-2147467259);
    lpFileName = (LPCSTR)((*(int (__thiscall **)(int))(*(_DWORD *)v5 + 12))(v5) + 16);
    n2 = 2;
    v6 = sub_7019FC(v73);
    if ( !v6 )
      sub_49DD90(-2147467259);
    lpFileName_5 = (unsigned __int8 *)((*(int (__thiscall **)(int))(*(_DWORD *)v6 + 12))(v6) + 16);
    LOBYTE(n2) = 3;
    CWnd::GetWindowTextA(&lpFileName_5);
    CWnd::GetWindowTextA(&lpFileName);
    if ( _mbscmp(lpFileName_5, (const unsigned __int8 *)&stru_99B33C) && *((_DWORD *)lpFileName_5 - 3) ) // 这是一个比对, 不能走这里
    {
      if ( _mbscmp((const unsigned __int8 *)lpFileName, &byte_9B3144) // 直接比较固定值 (看是否等于其中一个值)
        && _mbscmp((const unsigned __int8 *)lpFileName, "password") )
      {
        n10 = 0;
        n12_1 = 0;
        ThreadId = (DWORD)&n12_3;
        v9 = sub_7019FC(v8);
        if ( !v9 )
          sub_49DD90(-2147467259);
        n12_3 = (CHAR *)((*(int (__thiscall **)(int))(*(_DWORD *)v9 + 12))(v9) + 16);
        LOBYTE(n2) = 6;
        if ( !rsrc_71F5E0(&n12_3, (HRSRC)&hResInfo__0) )
          sub_49FF70((void *)&hResInfo__0, 0xEu);
        LOBYTE(n2) = 3;
        sub_599A30(n12_3, n12_1, n10);
        CWnd::SetFocus((CWnd *)(lpParameter + 984));
        *((_BYTE *)lpParameter + 201) = 0;
        goto LABEL_29;
      }
      if ( _mbscmp(dword_AF83FC, (const unsigned __int8 *)&stru_99CB08) && dword_AF82EC != 1 ) // 直接走这里就行......
      {
        v10 = *lpParameter;
        *((_BYTE *)lpParameter + 201) = 1;
        (*(void (__thiscall **)(int *))(v10 + 384))(lpParameter);
        goto LABEL_29;
      }
      sub_580BF0(0);
      LOBYTE(n2) = 5;
      CDialog::DoModal((CDialog *)v90);
      if ( v98[48] )
      {
        v11 = *lpParameter;
        *((_BYTE *)lpParameter + 201) = 1;
        (*(void (__thiscall **)(int *))(v11 + 384))(lpParameter);
        sub_592900((CDialog *)v90, v73, v74);
LABEL_29:
        LOBYTE(n2) = 2;
        v12 = lpFileName_5 - 16;
        if ( _InterlockedDecrement((volatile signed __int32 *)lpFileName_5 - 1) <= 0 )
          (*(void (__thiscall **)(_DWORD, unsigned __int8 *))(**(_DWORD **)v12 + 4))(*(_DWORD *)v12, v12);
        goto LABEL_31;
      }
      *((_BYTE *)lpParameter + 201) = 0;
      sub_592900((CDialog *)v90, v73, v74);
    }
    else
    {
      n10 = 0;
      n12_1 = 0;
      ThreadId = (DWORD)&n12_3;
      v14 = sub_7019FC(v7);
      if ( !v14 )
        sub_49DD90(-2147467259);
      n12_3 = (CHAR *)((*(int (__thiscall **)(int))(*(_DWORD *)v14 + 12))(v14) + 16);
      LOBYTE(n2) = 4;
      if ( !rsrc_71F5E0(&n12_3, (HRSRC)&hResInfo__1) )
        sub_49FF70((void *)&hResInfo__1, 0x10u);
      LOBYTE(n2) = 3;
      sub_599A30(n12_3, n12_1, n10);
      CWnd::SetFocus((CWnd *)(lpParameter + 1016));
    }
    LOBYTE(n2) = 2;
    v15 = lpFileName_5 - 16;
    // 省略部分...
  }
  // 又是一种方式
  if ( !IsWindowVisible((HWND)lpParameter[528]) )
  {
    if ( !IsWindowVisible((HWND)lpParameter[496]) )
      return;
    v27 = sub_7019FC(v73);
    if ( !v27 )
      sub_49DD90(-2147467259);
    lpFileName_4 = (unsigned __int8 *)((*(int (__thiscall **)(int))(*(_DWORD *)v27 + 12))(v27) + 16);
    n2 = 14;
    v28 = sub_7019FC(v73);
    if ( !v28 )
      sub_49DD90(-2147467259);
    v85 = (unsigned __int8 *)((*(int (__thiscall **)(int))(*(_DWORD *)v28 + 12))(v28) + 16);
    LOBYTE(n2) = 15;
    CWnd::GetWindowTextA(&v85);
    CWnd::GetWindowTextA(&lpFileName_4);
    sub_4A00D0(&v85);
    sub_4A01C0(&v85);
    sub_4A00D0(&lpFileName_4);
    sub_4A01C0(&lpFileName_4);
    if ( !_mbscmp(v85, (const unsigned __int8 *)&stru_99B33C)
      || !*((_DWORD *)v85 - 3)
      || !_mbscmp(lpFileName_4, (const unsigned __int8 *)&stru_99B33C)
      || !*((_DWORD *)lpFileName_4 - 3) )
    {
      n10 = 0;
      n12_1 = 0;
      lpThreadId_ = (DWORD)&n12_3;
      v67 = sub_7019FC(n12_14);
      if ( !v67 )
        sub_49DD90(-2147467259);
      n12_3 = (CHAR *)((*(int (__thiscall **)(int))(*(_DWORD *)v67 + 12))(v67) + 16);
      LOBYTE(n2) = 16;
      if ( !rsrc_71F5E0(&n12_3, (HRSRC)&hResInfo__2) )
        sub_49FF70((void *)&hResInfo__2, 0x14u);
      LOBYTE(n2) = 15;
      goto LABEL_136;
    }
    n6 = *((_DWORD *)v85 - 3);
    if ( n6 < 6 || n6 > 32 )
    {
      n10 = 0;
      n12_1 = 0;
      n12_3 = n12_14;
      sub_4B0DF0((HRSRC)&hResInfo__3);
      goto LABEL_136;
    }
    v31 = 0;
    if ( byte_AF7FCC )
    {
      while ( (unsigned __int8)(unknown_libname_1050(v31) - 48) <= 9u )
      {
        if ( ++v31 >= *((_DWORD *)v85 - 3) )
        {
          n10 = 10;
          n12_1 = (int)n12_2;
          sub_4B0DC0(&v85);
          sub_592CC0(&lpFileName_5, n12_1, n10);
          LOBYTE(n2) = 17;
          if ( (unsigned __int8)sub_4F9C50(&lpFileName_5, &lpFileName_4) )
          {
            n10 = 0;
            n12_1 = 0;
            n12_3 = n12_15;
            sub_4B0DF0((HRSRC)&hResInfo__4);
            sub_599A30(n12_3, n12_1, n10);
            CWnd::SetFocus((CWnd *)(lpParameter + 520));
            *((_BYTE *)lpParameter + 201) = 0;
            sub_49D840(&lpFileName_5);
            goto LABEL_137;
          }
          sub_49D840(&lpFileName_5);
          goto LABEL_85;
        }
      }
LABEL_72:
      n10 = 0;
      n12_1 = 0;
      n12_3 = n12_2;
      sub_4B0DF0((HRSRC)&hResInfo__5);
LABEL_136:
      sub_599A30(n12_3, n12_1, n10);
      CWnd::SetFocus((CWnd *)(lpParameter + 488));
      goto LABEL_137;
    }
  }
}

在IDA中找到两处判断Patch掉改为无条件, 然后重新打开输入留空直接登录即可.

成果:

试卷包密码获取/绕过校验

在获取到试卷包文件后(*.xut: 试卷文件, *.scm: 配置文件), 我们需要进行导入, 但这时会出现密码.

XX市2024年初二信息技术练习.xut, XX市2024年初二信息技术练习.scm为例

这要求我们必须输入一个正确的密码才能将试卷包导入进服务端中, 否则会提示"试卷包密码错误"

按钮事件函数

同理, xspy一把梭 (: 这里这个确认按钮的父窗口是"试卷包密码"窗口)

可得按钮ID=03eb , 事件处理函数地址onCommand=0x00590760 (XP)

但是! 当你用IDA打开后你会发现

int __thiscall  __thiscall CNewTypeDlg::`vcall'{384,{flat}}(void *this)
{
  return (*(int (__thiscall **)(void *))(*(_DWORD *)this + 384))(this);
}

这个地址并非函数真实地址, 指向的是个虚函数 (?)

这时候就要使用OD跟真实地址了

OD启动!

使用Ollydbg启动程序 (附加上去应该也可以) (XP)

0x00590760 处下断 (bp 0x00590760 ) 按下确定后跟进JMP

可以发现真实地址为0x00590850

静态分析

void __thiscall LoadPaperPre(const unsigned __int8 **this)
{
  ATL::CSimpleStringT<char,0> *v2; // esi
  CHAR *v3; // ecx
  _DWORD *v4; // edx
  int v5; // ebx
  CHAR *v6; // esi
  char *v7; // ecx
  CHAR v8; // al
  signed int i_1; // kr00_4
  signed int i; // esi
  CHAR *v11; // eax
  CHAR *v12; // ecx
  bool IsOK; // bl
  CHAR *v14; // edx
  CHAR *v15; // edx
  CHAR *v16[6]; // [esp-4h] [ebp-430h] BYREF
  CHAR *v17; // [esp+14h] [ebp-418h] BYREF
  CHAR *v18; // [esp+18h] [ebp-414h] BYREF
  struct HRSRC__ hResInfo; // [esp+1Ch] [ebp-410h] BYREF
  int v20; // [esp+428h] [ebp-4h]

  v2 = (ATL::CSimpleStringT<char,0> *)(this + 42);
  CWnd::GetWindowTextA(this + 42);              // this + 42为输入的密码
  if ( *(int *)(*(_DWORD *)v2 - 12) <= 0 )
  {
    v16[0] = v3;
    sub_4B0DF0(v16, (HRSRC)&hResInfo_);         // 输入为空
    sub_590B90(v16[0]);
    CWnd::SetFocus((CWnd *)(this + 692));
  }
  else
  {
    sub_4B0DF0(&v18, (HRSRC)&a2);
    v4 = *(_DWORD **)v2;
    v20 = 0;
    v5 = *(v4 - 3);
    if ( v5 < 0 )
      failchk(-2147024809);
    if ( ((1 - *(v4 - 1)) | (*(v4 - 2) - v5)) < 0 )
    {
      ATL::CSimpleStringT<char,0>::PrepareWrite2(v2, *(v4 - 3));
      v4 = *(_DWORD **)v2;
    }
    sub_4BB300(v4);
    v6 = v18;
    memset(&hResInfo, 0, *((_DWORD *)v18 - 3) + 1);
    v7 = (char *)((char *)&hResInfo - v6);
    do
    {
      v8 = *v6++;
      v6[(_DWORD)v7 - 1] = v8;
    }
    while ( v8 );
    i_1 = strlen((const char *)&hResInfo);
    for ( i = 0; i < i_1; ++i )
      *((_BYTE *)&hResInfo.unused + i) = 16 * *((_BYTE *)&hResInfo.unused + i) + (*((_BYTE *)&hResInfo.unused + i) >> 4);// 这里将输入高低四位交换
                                                // transformed_byte = (original_byte << 4) | (original_byte >> 4);
                                                // 
    v11 = *sub_4B0DF0(&v17, &hResInfo); // 不知道从哪搞到的, 反正我真没搞明白
    if ( !v11 )
      failchk(-2147467259);
    IsOK = _mbscmp(this[43], (const unsigned __int8 *)v11) == 0;// this[43]为 this + 42; 当相等时IsOK为true
    v14 = v17 - 16;
    if ( _InterlockedDecrement((volatile signed __int32 *)v17 - 1) <= 0 )
      (*(void (__thiscall **)(_DWORD, CHAR *))(**(_DWORD **)v14 + 4))(*(_DWORD *)v14, v14);
    if ( IsOK )
    {
      CDialog::OnOK((CDialog *)this);
    }
    else
    {
      v16[0] = v12;
      sub_4B0DF0(v16, (HRSRC)&hResInfo__1);
      sub_590B90(v16[0]);
    }
    v20 = -1;
    v15 = v18 - 16;
    if ( _InterlockedDecrement((volatile signed __int32 *)v18 - 1) <= 0 )
      (*(void (__thiscall **)(_DWORD, CHAR *))(**(_DWORD **)v15 + 4))(*(_DWORD *)v15, v15);
  }
}

据此可知, 对于用户输入的密码, 程序会将每个字节的高低四位交换, 然后将结果与通过sub_4B0DF0得到的正确密码进行比较, 相等时则提示成功并进入下一步. 经过测试, 即使直接跳过验证使用错误密码也可以正确加载, 输入密码正确与否并不影响试卷包.

对于高低四位交换, 有如下示例 (提醒: 一个字节是八位二进制哦):

单字节变换:
原始输入:    1F
变换后:      F1

原始输入:    59 52 4C 48 61 70 70 79          YRLHappy
变换后:      95 25 C4 84 16 07 07 97          ........ (大部分为非ASCII字符, 省略)

动态调试得密码

观察IDA提示可知, 参数通过栈传入 (v11对应eax, this[43]this+42对应dword ptr...

使用OD, 在_mbscmp(this[43], (const unsigned __int8 *)v11) == 0 处下断 (内存地址为: 0x00590980) (XP)

选中push dword....然后数据窗口中跟随数值 (下图中数据窗口还未跟随)

可得: XX XX XX 87 23 (XX为脱敏处理, 不同密码的试题包得到的整个HEX不一样)

将其再进行以此高低四位变换后即可得到正确密码.

Patch绕过密码检查

[静态分析]中, 我们可以发现如下代码

IsOK = _mbscmp(this[43], (const unsigned __int8 *)v11) == 0;// this[43]为 this + 42; 当密码正确时IsOK为true
v14 = v17 - 16;
if ( _InterlockedDecrement((volatile signed __int32 *)v17 - 1) <= 0 )
  (*(void (__thiscall **)(_DWORD, CHAR *))(**(_DWORD **)v14 + 4))(*(_DWORD *)v14, v14);
if ( IsOK ) // 判断是否正确
{
  CDialog::OnOK((CDialog *)this); // 相等时继续下一步操作, 貌似走的个分发器, 不太好跟
}
else
{ // 当不正确时弹窗
  v16[0] = v12;
  sub_4B0DF0(v16, (HRSRC)&hResInfo__1); // 从Res中加载字符串"试卷包密码错误!"
  sub_590B90(v16[0]);
}

我们只需要单字节Patch, 将JE改为JNE即可

005909A5  |.  8B0A          mov ecx,dword ptr ds:[edx]
005909A7  |.  52            push edx
005909A8  |.  8B01          mov eax,dword ptr ds:[ecx]
005909AA  |.  FF50 04       call dword ptr ds:[eax+0x4]
005909AD  |>  84DB          test bl,bl
005909AF  |.  74 09         je short examspot.005909BA // 此处改JNE

成果如图所示

数据库

基础信息

可以发现, 安装程序中有机考数据库引擎Setup.exe 而经过安装发现, 实际上就是MYSQL

以及查看examspot.exe的导出表, 可以发现也用了MYSQL

查找mysql_real_connect, 可以发现有如下初始化数据库代码:

mysql_init(*(_DWORD *)(this + 968));
mysql_options(*(_DWORD *)(this + 968), 7, &unk_9BA480);
if ( !mysql_real_connect(*(_DWORD *)(this + 968), v17, "root", "zrsoft", *(_DWORD *)(this + 8), 3306, 0, 0) )
{
  v6 = mysql_errno(*(_DWORD *)(this + 968), v15);
  ArgList = mysql_error(*(_DWORD *)(this + 968), v6);
  sub_49E820(this + 972, (char *)&Format_, ArgList);
  v8 = 0;
  goto LABEL_12;
}

mysql_real_connect函数签名:

MYSQL *mysql_real_connect(MYSQL *mysql, const char *host, const char *user, const char *passwd, const char *db, unsigned int port, const char *unix_socket, unsigned long client_flag);

可知端口为3306, 用户名为root, 密码为zrsoft, 数据库为zrdata

数据表

不太重要的数据表:

表名

作用

abnormcode

Code, CodeName, Remark(解释)

答题异常标记 (设备断电, 设备异常...)

abnormpro

Num, Approch(类型), Remark

重考/续考标记

capacitycode

Code, TargetCode, Name, class

能力水平标识 (Name: 了解水平, 理解水平...)

examleveldetail

testflag, ElectiveMkCode, Subject, ElectiveMkName

考试等级信息

examsubject

testflag, SubjectID, SubjectName, ExamTime

科目名称, ID, 考试时间

inputmethod

InputMethod

输入模式 (考察打字?)

maincode

MainCode, ModCode, Name, class

能力点

studentphoto

testflag, ExamineeID, photo

学生照片

考生机数据表 (computerinfo)

类型

注解 (与示例数据)

IpAddr

String

考生机内网IP (例: "192.168.0.11")

ClientID

String

桌号

WSDirect

String

计算机名称 (例: "YComputer")

BakDevice

Integer | null

是否为备用机 (例: null)

State

Integer

机器状态

CheckTime

String | Null

检查时间

IpTail

Integer

IP最后一段 (例: "11")

IsEnvChecked

Integer

是否检查过环境 (例: 1)

Version

String

客户端版本 (例: "0000")

ModifyTime

String

修改时间

PatchNum

String

Patch版本

场次表 (sceneno)

类型

注解

testflag

String

考试标识? 什么逆天名字

TestCCID

Integer

场次ID, 递增

Year, Month, Day

Integer

年, 月, 日

SH, SM

Integer

开始时间 (小时:分钟)

EH, EM

Integer

结束时间 (小时:分钟)

TestIDA, TestIDB

Integer

测试ID

State

Integer

状态

学生信息表 (studentinfo)

类型

注解

testflag

String

考试标识

ExamineeID, ExamCertID

String

准考证号

ExamineeName

String

姓名

Sex

String

性别

SchoolName

String

学校名称

CompanyName

String

公司名称 (?)

ExamDate

String

考试时间 (例: "2025-3-29")

StartTime

String

开始时间 (例: "12:17")

EndTime

String

结束时间 (例: "12:47")

ExamRoomID

String

考场

ClientID

String

桌号

IpAddr

String

IP地址

WSDirect

String

主机名

TestCCID

String

场次ID

UStartTime, UEndTime

String

同StartTime, EndTime

CurrCode

Integer

当前状态

Code

String

校验码

ProNum, TestNum, Zmark

Integer

?

MarkLog

Blob

成绩日志

Mark

Blob

成绩

......

/

其余省略

考试环境表 (testenvset)

注解

ExamID

考试ID

PaperFlag

试卷ID

FileName

文件名

MKName

成绩名 (?)

......

省略部分

Password

启动考试的密码

TimeLen, HzTime

考试时长

ProdDirect

考试机题目文件夹

PaperFolder

试卷文件夹

ExamName

考试名称

TestName

测试名称

客户端

ZrComprs ILProtector脱壳

本来这段内容不应该在这里的, 不过客户端这实在没什么可写的了.....,

客户端与服务端均使用ZrComprs.dllZrComprsPlus.dll对其自有格式进行处理 (XUT, SCM, ZRC...)

其中ZrComprsPlus.dll会引入ZRComprsPlusProtect32.dllZRComprsPlusProtect64.dll

查壳ZrComprsPlus, ZRComprsPlusProtect结果如下

ZrComprsPlus.dll
PE32
    操作系统: Windows(95)[I386, 32 位, DLL]
    链接程序: Microsoft Linker(8.0)
    语言: MSIL/C#
    库: .NET Framework(v4.0, CLR v4.0.30319)
    (Heur)保护: Obfuscation[CLR constructor]

ZrComprsPlusProtect32.dll
PE32
    操作系统: Windows(XP)[I386, 32 位, DLL]
    链接程序: Microsoft Linker(10.00.40219)
    编译器: Microsoft Visual C/C++(16.00.40219)[LTCG/C++]
    语言: C++
    工具: Visual Studio(2010)
    调试数据: Binary[偏移=0x000908e8,大小=0x6c]
        调试数据: PDB file link(7.0)

查看其调试数据, 得到:

c:\MyProjects\gitlab\ILProtector\ILProtector\Output2010\Win32\Release\Protect32.pdb

可知使用ILProtector加壳 (不 抹 符 号 信 息 的 坏 处)

使用ILProtectorUnpacker脱壳ZrComprsPlus.dll, 可得

顺带一提, 解压不是Decompress吗?????? 你用个毛线的Un前缀

和一个有点意思的东西

客户端会不断向服务端发送"COMM1200COMM OK?" 貌似是心跳包

I'm not OK ( )

后记

啊什么你说为什么文章这么水, 因为这玩意考试系统写的太烂了, 且这项目体量(甚至基本纯C++, this乱飞), 我能看看而不是扔进回收站就不错的了 ( )

建议给我PDB!!!

如果要有个.NET的就好了 (话说回来以前逆过一个.NET的, 那是真简单啊......)

LICENSED UNDER CC BY-NC-SA 4.0
Comment