今年初二微机也考完了啊...... 也该翻出来这玩意了......
谨以此文, 追忆我将要过去的初中三年. 与那曾经一刹那的美好.
引言
可能一定会用到的工具: 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
数据表
不太重要的数据表:
考生机数据表 (computerinfo)
场次表 (sceneno)
学生信息表 (studentinfo)
考试环境表 (testenvset)
客户端
ZrComprs ILProtector脱壳
本来这段内容不应该在这里的, 不过客户端这实在没什么可写的了.....,
客户端与服务端均使用ZrComprs.dll与ZrComprsPlus.dll对其自有格式进行处理 (XUT, SCM, ZRC...)
其中ZrComprsPlus.dll会引入ZRComprsPlusProtect32.dll或ZRComprsPlusProtect64.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的, 那是真简单啊......)