目录
前言环境准备0、简单po jie:修改两字节1、验证序列号的过程——逆向分析1.1 定位“Serial Number错误”弹窗代码1.2 Serial Number验证函数 分析1.3 构造可用的Serial Number1.4 验证Serial Number2、验证RSA签名的过程——逆向分析2.1 验证RSA签名的流程2.2 简单的po jie2.3 自签名license文件po jie2.2.1 Base64Decode函数2.2.2 VerifySignature函数2.2.3 自签名 si4.lic 文件3. 在线检查序列号前言
这里po jie的是当前最新版本Source Insight 4.0.118,总结po jie过程有以下三个部分:
逆向Serial Number序列号验证算法,构造可用序列号逆向分析验证license签名过程,自签名license文件,并使用自己的公钥替换程序中原始公钥程序运行起来后,会单独开启一个线程在线检查Serial Number是否有效,需要修改二进制文件过掉此检查
除此之外,笔者也看到其他人的一些文章,提到有黑名单检查,因为没遇到这个逻辑,程序暂时也能用,所以这里的破解中没有涉及,后面如果遇到再研究也不迟。
文中用到的所有代码可点击右侧超链接,在 Github上查看
环境准备
IDA6.8、x32dbg
官网 下载sourceinsight40118-setup.exe,也可使用文后附件
0、简单po jie:修改两字节
这部分内容讲述简单po jie程序的方法,因为本文主要是研究自签名license并替换程序中公钥的po jie方法,比较麻烦,纯粹是为了学习目的。如果对这种方法不感兴趣,这部分内容就提供另一种简单的po jie方法:修改 sourceinsight4.exe 二进制文件中的两个字节完成po jie
第一处是VA=005141A8,FA=1135A8,将 74 改为 EB,原因可参考第 2.2 节第二处是VA=00513470,FA=112870,将 83 改为 C3,原因可参考第 3 节
最后,将文后的附件si4.lic放到C:\ProgramData\Source Insight\4.0文件夹中即可po jie程序
1、验证序列号的过程——逆向分析
这部分内容分4个小节:
1.1 节 讲述如何定位到验证序列号的代码
1.2 节 中逆向还原验证序列号格式的函数,并按照格式构造出正式版序列号
1.3 节 中逆向还原本地校验序列号的函数,构造出可通过本地校验的序列号
1.4 节 本地验证序列号成功后,在线验证序列号失败,从而引出第 2 节的分析
1.1 定位“Serial Number错误”弹窗代码
安装程序后,打开sourceinsight4.exe程序会看到选择license文件的界面,如下图:
选择第一项“输入serial number”,点击“Next >”继续,显示输入Serial Number的对话框,并且提示 Serial Number 的格式“S4XX-XXXX-XXXX-XXXX”。按照格式输入一个,点击"Next>",会弹出“无效的Serial Number”警告,如下图:
在IDA中搜索弹窗警告中的文本“The serial number …”,找到引用此字符串的地址是.text:00513A69。
分析*.text:00513A69*此位置上下文代码,可以看出.text:00513A45 call sub_510B50是Serial Number的验证函数,若验证失败(返回结果0),则会再判断是否是3.x版本的Serail Number,不是则弹出上图中的警告;若验证成功则跳转到loc_513ACB继续判断验证结果。代码如下所示:
.text:00513A25 moveax, dword_673488.text:00513A2A push 1; int.text:00513A2C leaecx, [eax+604h].text:00513A32 push ecx ; int.text:00513A33 leaedx, [eax+60Ch].text:00513A39 push edx ; int.text:00513A3A addeax, 608h.text:00513A3F push eax ; int.text:00513A40 leaeax, [esp+158h+MultiByteStr].text:00513A44 push eax ; char *.text:00513A45 call sub_510B50; 验证Serial Number是否正确.text:00513A4A addesp, 34h.text:00513A4D test eax, eax ; eax = 0, 弹出错误窗口并返回.text:00513A4D ; eax!= 0, 继续验证Serial Number格式.text:00513A4F jnzshort loc_513ACB.text:00513A51 leaecx, [esp+128h+MultiByteStr].text:00513A55 push ecx.text:00513A56 call sub_561CB0.text:00513A5B addesp, 4.text:00513A5E test eax, eax.text:00513A60 jzshort loc_513A69.text:00513A62 push offset aTheSerialNumbe ; "The serial number you entered is for ve"....text:00513A67 jmpshort loc_513A6E.text:00513A69 ; ---------------------------------------------------------------------------.text:00513A69.text:00513A69 loc_513A69: ; CODE XREF: sub_5139C0+A0j.text:00513A69 push offset aTheSerialNum_0 ; "The serial number you entered is not co"....text:00513A6E.text:00513A6E loc_513A6E: ; CODE XREF: sub_5139C0+A7j.text:00513A6E call sub_40AC20.text:00513A73 addesp, 4... ....text:00513ACA retn.text:00513ACB ; ---------------------------------------------------------------------------.text:00513ACB.text:00513ACB loc_513ACB: ; CODE XREF: sub_5139C0+8Fj... ....text:00513AE1 cmp[eax+604h], edx.text:00513AE7 jzshort loc_513B0D.text:00513AE9 push offset aTheSerialNum_1 ; "The serial number you entered is for a "....text:00513AEE call sub_40AC20... ....text:00513B0C retn
1.2 Serial Number验证函数 分析
详细分析上面步骤中提到的sub_510B50处的序列号验证函数,总结出Serial Number的验证规则如下:
SerailNumber字符串长度必须为19(16个字符加上3个分隔符)
SerailNumber[0]必须等’S’
SerailNumber[1]是’0’-‘9’
SerailNumber[2]等于’T’表示Trial license试用许可,等于’B’表示Beta license测试许可,等于’S’表示Standard license标准许可,等于’U’表示Upgrade license升级许可
SerailNumber[3]等于 ‘G’、‘V’、‘R’ 其中之一
SerailNumber[6]等于 ‘R’、‘G’、‘D’、‘F’ 其中之一
SerialNumber的前12个字符经过函数.text:00510C6B call sub_510320转换后得到4个字符,与SerialNumber的最后4个字符必须相同
代码如下所示:
.text:00510B50 ; int __cdecl sub_510B50(char *, int, int, int, int).text:00510B50 sub_510B50proc near; CODE XREF: .text:005129C4p.text:00510B50 ; sub_5139C0+85p.text:00510B50.text:00510B50 ary4char = dword ptr -18h.text:00510B50 szSNTemp = byte ptr -14h.text:00510B50 ptr_szSN = dword ptr 4.text:00510B50 .text:00510B50 ptr_nSN[3]Flag = dword ptr 8.text:00510B50 ptr_nLicTypeFlag= dword ptr 0Ch.text:00510B50 ptr_nVersionFlag= dword ptr 10h.text:00510B50 arg_10= dword ptr 14h.text:00510B50.text:00510B50 subesp, 18h.text:00510B53 push esi.text:00510B54 movesi, [esp+1Ch+ptr_szSN].text:00510B58 push esi ; char *.text:00510B59 call __strupr.text:00510B5E push esi ; char *.text:00510B5F call _strlen.text:00510B64 addesp, 8.text:00510B67 cmpeax, 13h ; strlen(ptr_szSN) == 13h.text:00510B6A jnzloc_510C86.text:00510B70 moval, '-'.text:00510B72 cmp[esi+4], al; ptr_szSN[4] = '-'.text:00510B75 jnzloc_510C86.text:00510B7B cmp[esi+9], al; ptr_szSN[9] = '-'.text:00510B7E jnzloc_510C86.text:00510B84 cmp[esi+0Eh], al ; ptr_szSN[14] = '-'.text:00510B87 jnzloc_510C86.text:00510B8D cmpbyte ptr [esi], 'S' ; ptr_szSN[0] = 'S'.text:00510B90 jnzloc_510C86.text:00510B96 movecx, [esp+1Ch+arg_10].text:00510B9A test ecx, ecx.text:00510B9C jzshort loc_510BB5.text:00510B9E moval, [esi+6].text:00510BA1 cmpal, 'R' ; ptr_szSN[6] == 'R'.text:00510BA3 jzshort loc_510BB5.text:00510BA5 cmpal, 'G' ; ptr_szSN[6] == 'G'.text:00510BA7 jzshort loc_510BB5.text:00510BA9 cmpal, 'D' ; ptr_szSN[6] == 'D'.text:00510BAB jzshort loc_510BB5.text:00510BAD cmpal, 'F' ; ptr_szSN[6] == 'F'.text:00510BAF jnzloc_510C86.text:00510BB5.text:00510BB5 loc_510BB5: ; CODE XREF: sub_510B50+4Cj.text:00510BB5 ; sub_510B50+53j ....text:00510BB5 moval, [esi+1].text:00510BB8 cmpal, '0' ; ptr_szSN[1] >= '0'.text:00510BBA jlloc_510C86.text:00510BC0 cmpal, '9' ; ptr_szSN[1] <= '9'.text:00510BC2 jgloc_510C86.text:00510BC8 movedx, [esp+1Ch+ptr_nVersionFlag].text:00510BCC movsx eax, al.text:00510BCF subeax, '0'.text:00510BD2 mov[edx], eax.text:00510BD4 moval, [esi+2].text:00510BD7 cmpal, 'T' ; ptr_szSN[2] == 'T', Trial license.text:00510BD9 jnzshort IF_BEGIN.text:00510BDB moveax, [esp+1Ch+ptr_nLicTypeFlag].text:00510BDF movdword ptr [eax], 1.text:00510BE5 jmpshort ELSE_END.text:00510BE7 ; ---------------------------------------------------------------------------.text:00510BE7.text:00510BE7 IF_BEGIN: ; CODE XREF: sub_510B50+89j.text:00510BE7 cmpal, 'B' ; ptr_szSN[2] == 'B', Bete license, cannot be used with the release version.text:00510BE9 jnzshort ELSE_IF.text:00510BEB movedx, [esp+1Ch+ptr_nLicTypeFlag].text:00510BEF movdword ptr [edx], 3.text:00510BF5 jmpshort ELSE_END.text:00510BF7 ; ---------------------------------------------------------------------------.text:00510BF7.text:00510BF7 ELSE_IF: ; CODE XREF: sub_510B50+99j.text:00510BF7 cmpal, 'S' ; ptr_szSN[2] == 'S', Standard license.text:00510BF9 jnzshort ELSE_IF_.text:00510BFB moveax, [esp+1Ch+ptr_nLicTypeFlag].text:00510BFF movdword ptr [eax], 0.text:00510C05 jmpshort ELSE_END.text:00510C07 ; ---------------------------------------------------------------------------.text:00510C07.text:00510C07 ELSE_IF_: ; CODE XREF: sub_510B50+A9j.text:00510C07 cmpal, 'U' ; ptr_szSN[2] == 'U', Upgrade License.text:00510C09 jnzshort loc_510A56.text:00510C0B movedx, [esp+1Ch+ptr_nLicTypeFlag].text:00510C0F movdword ptr [edx], 0.text:00510C15.text:00510C15 ELSE_END: ; CODE XREF: sub_510B50+95j.text:00510C15 ; SI_ValidateSNFmt+A5j ....text:00510C15 moval, [esi+3].text:00510C18 cmpal, 'G' ; ptr_szSN[3] == 'G'.text:00510C1A jnzshort IF_BEGIN2.text:00510C1C moveax, [esp+1Ch+ptr_nSN[3]Flag].text:00510C20 movdword ptr [eax], 1.text:00510C26 jmpshort ELSE_END2.text:00510C28 ; ---------------------------------------------------------------------------.text:00510C28.text:00510C28 IF_BEGIN2:; CODE XREF: sub_510B50+CAj.text:00510C28 cmpal, 'V' ; ptr_szSN[3] == 'V'.text:00510C2A jnzshort ELSE_IF2.text:00510C2C movedx, [esp+1Ch+ptr_nSN[3]Flag].text:00510C30 movdword ptr [edx], 2.text:00510C36 jmpshort ELSE_END2.text:00510C38 ; ---------------------------------------------------------------------------.text:00510C38.text:00510C38 ELSE_IF2: ; CODE XREF: sub_510B50+DAj.text:00510C38 cmpal, 'R' ; ptr_szSN[3] == 'R'.text:00510C3A jnzshort loc_510C86.text:00510C3C moveax, [esp+1Ch+ptr_nSN[3]Flag].text:00510C40 movdword ptr [eax], 0.text:00510C46.text:00510C46 ELSE_END2:; CODE XREF: sub_510B50+D6j.text:00510C46 ; sub_510B50+E6j.text:00510C46 test ecx, ecx.text:00510C48 jzshort loc_510C7C.text:00510C4A leaecx, [esp+1Ch+szSNTemp].text:00510C4E push esi ; char *.text:00510C4F push ecx ; char *.text:00510C50 call _strcpy.text:00510C55 leaedx, [esp+24h+ary4char].text:00510C59 push edx.text:00510C5A push offset ary256Chars.text:00510C5F leaeax, [esp+2Ch+szSNTemp].text:00510C63 push 0Fh.text:00510C65 push eax.text:00510C66 mov[esp+34h+szSNTemp+0Fh], 0.text:00510C6B call sub_510320; 通过SerialNumber前12个字符计算出最后4个字符,并与输入的SerialNumber最后4个字符做比较.text:00510C70 movecx, [esi+0Fh].text:00510C73 addesp, 18h.text:00510C76 cmpecx, [esp+1Ch+ary4char] ; 比较计算出的最后4个字符是否与输入的SerialNumber最后4个字符相同.text:00510C7A jnzshort loc_510C86.text:00510C7C.text:00510C7C loc_510C7C: ; CODE XREF: sub_510B50+F8j.text:00510C7C moveax, 1.text:00510C81 popesi.text:00510C82 addesp, 18h.text:00510C85 retn.text:00510C86 ; ---------------------------------------------------------------------------.text:00510C86.text:00510C86 loc_510C86: ; CODE XREF: sub_510B50+1Aj.text:00510C86 ; sub_510B50+25j ....text:00510C86 xoreax, eax.text:00510C88 popesi.text:00510C89 addesp, 18h.text:00510C8C retn.text:00510C8C sub_510B50endp
1.3 构造可用的Serial Number
步骤1.2中提到Serial Number的验证规则之一是:序列号的前12个字符经过函数sub_510320转换后得到4个字符,与序列号的最后4个字符相同。
以下便是是sub_510320汇编代码分析以及据此还原的C代码,将此函数命名为Get4charBySNPre12char
.text:00510320 sub_510320proc near; CODE XREF: sub_510390+73p.text:00510320 ; sub_510B50+11Bp ....text:00510320.text:00510320 ptr_szSN = dword ptr 4.text:00510320 nSNLen= dword ptr 8.text:00510320 ary256char= dword ptr 0Ch.text:00510320 ptr_4charsResult= dword ptr 10h.text:00510320.text:00510320 push ebx.text:00510321 movebx, [esp+4+nSNLen].text:00510325 push ebp.text:00510326 movebp, [esp+8+ptr_szSN].text:0051032A push esi.text:0051032B push edi.text:0051032C movedi, [esp+10h+ary256char].text:00510330 xoresi, esi.text:00510332.text:00510332 DO_BEGIN: ; CODE XREF: sub_510320+5Ej.text:00510332 movsx eax, byte ptr [ebp+0].text:00510336 addeax, esi.text:00510338 andeax, 0FFh.text:0051033D movcl, [eax+edi].text:00510340 moveax, 1.text:00510345 cmpebx, eax.text:00510347 jbeshort loc_510361.text:00510349 leaesp, [esp+0].text:00510350.text:00510350 _DO_BEGIN:; CODE XREF: sub_510320+3Fj.text:00510350 movsx edx, byte ptr [eax+ebp].text:00510354 movzx ecx, cl.text:00510357 xoredx, ecx.text:00510359 movcl, [edx+edi].text:0051035C inceax.text:0051035D cmpeax, ebx.text:0051035F jbshort _DO_BEGIN.text:00510361.text:00510361 _DO_END: ; CODE XREF: sub_510320+27j.text:00510361 movzx eax, cl.text:00510364 cdq.text:00510365 movecx, 1Ah.text:0051036A idiv ecx.text:0051036C moveax, [esp+10h+ptr_4charsResult].text:00510370 incesi.text:00510371 movdl, byte ptr ds:sz26Chars[edx] ; "KV96GMJYH7QF5TCW4U3XZPRSDN".text:00510377 mov[esi+eax-1], dl.text:0051037B cmpesi, 4.text:0051037E jbshort DO_BEGIN.text:00510380 popedi.text:00510381 popesi.text:00510382 popebp.text:00510383 popebx.text:00510384 retn.text:00510384 sub_510320endp//还原为C代码char g_sz26Chars[] = {"KV96GMJYH7QF5TCW4U3XZPRSDN" };void Get4charBySNPre12char(char* szSN, int nSNLen, char* ary256Chars, char* pResult) {for (int i = 0; i < 4; i++) {char cl = ary256Chars[(szSN[0] + i) & 0xFF];for (int j = 1; j < nSNLen; j++) {cl = ary256Chars[szSN[j] ^ (unsigned char)cl];}*(pResult + i) = g_sz26Chars[((unsigned char)cl % 0x1A)];}}//构造一个可用的Serial Numberchar g_ary256Chars[] = {0x23, 0xDD, 0x78... }; //这里是一个256大小的char数组void main() {char szSN[] = {"S4SG-KRGM-YD7Q-XXXX" };Get4charBySNPre12char(szSN, 15, g_ary256Chars, &szSN[15]);printf("%s\r\n", szSN); //S4SG-KRGM-YD7Q-RCFY}
因为Get4charBySNPre12char函数是通过SerialNumber的前12个字符生成后4个字符,所以可以将构造的前12个字符传入此函数,计算出一个可用的SerialNumber的后4个字符,代码如上,最终得到一个Standard正式版序列号:S4SG-KRGM-YD7Q-RCFY
1.4 验证Serial Number
将构造的序列号输入到序列号验证窗口中,点击“Next >”,验证通过,弹出“个人信息窗口”。
输入姓名、组织、邮箱信息,再点击“Next >”,弹窗“信息确认窗口”。
再点击“Next >”,先提示“正在激活许可”,接着又弹出了一个错误窗口,如下图:
通过定位字符串找到处理上述流程的函数sub_514740,该函数首先检查是否联网,
如果联网了,则会通过网络校验SerailNumer,验证失败就会弹出上面的错误信息;
如果没有联网,则会跳转到生成临时license文件的函数sub_513780,该函数会在C:\ProgramData\Source Insight\4.0目录生成si4.lic文件,允许程序单次运行,相当于一次性许可证,重启程序之后,这个许可也会失效。
据此猜测,既然生成了许可证文件,那么程序启动时就应该会去读si4.lic文件进行验证,打开监控软件便可看到对该文件进行了操作,如下图所示。至此,完成SerialNumber验证的分析,并构造出可用的序列号,接下来就分析si4.lic文件的加载及验证签名过程。
2、验证RSA签名的过程——逆向分析
这部分内容分3个小节:
2.1 节 根据读文件的API跟踪程序调用流程,还原出程序启动验证RSA签名的流程
2.2 节 讲述最简单的po jie方法,只需修改两个字节就可以po jie 程序
2.3 节 讲述自己签名license文件的po jie方法,相比2.2 节的方法要麻烦很多,但是学到了RSA签名校验的知识以及CryptoAPI编程
2.1 验证RSA签名的流程
在1.4 节分析的最后提到,程序启动后会加载si4.lic许可证文件,并且会调用CreateFileMappingAPI将文件内容映射到内存,所以对这个函数下API断点,就可以定位到读许可证的代码。而且读完许可证之后,就是验证许可证签名的代码。
程序中只有一处调用了这个API,结合动态调试,总结出调用关系如下所示:
.text:0045B460 WinMain.text:0045B6CDcall sub_515000.text:00515032 |- call sub_514ED0.text:00514EF3 |- call sub_417E40 检查C:\ProgramData\Source Insight\4.0\si4.lic文件是否存在.text:00514F03 |- call sub_5140E0 校验lic文件的函数,如果si4.lic文件存在,则执行校验.text:005140FD|- call sub_5127A0 加载si4.lic,校验字段值,保存校验结果。成功返回0xC8.text:005127EA| |- call sub_511150.text:00511156| | |- call sub_45A770 加载si4.lic文件.text:0045A779| | |- call sub_45A290 .text:0045A2E7| ||- call sub_41B290 .text:0041B2B2| ||- call sub_4567F0 .text:00456898| || |- call ds:CreateFileMappingW.text:0041B2D9| ||- call ds:MapViewOfFile .text:00512806| |- call sub_510570校验并存储<Type>值,与"Trial"、"Beta"、"Standard"相比较,设置校验结果.text:00512875| |- call sub_510570校验并存储<LicensedUser>值,不存在则结束,返回失败标志.text:005128FA| |- call sub_510570校验并存储<Serial>值,不存在则结束,返回失败标志.text:00512927| |- call sub_510570校验并存储<ActId>值,如果值等于"Deferred",设置标志位,表明si4.lic是断网激活时生成的一次性许可证.text:00514102|- cmp eax, 0C8h判断 sub_5127A0 返回值,失败则弹出错误窗口.text:00514133|- mov eax, [esi]判断如果是“一次性许可证”,则函数返回False.text:0051419B|- call sub_512CF0加载si4.lic,校验签名。成功返回0xC8.text:00512D39 |- call sub_45A770加载si4.lic文件 .text:00512D59 |- call sub_457D80遍历si4.lic中的数据,查找<Signature>标签.text:00512D6B |- call sub_458520找到<Signature>后,获取它的值.text:00512DEB |- call sub_402E00Base64解码Signature字符串值,得到二进制签名数据.text:00512E08 |- call sub_510640CryptoAPI校验签名。成功返回0xC8//还原成伪代码InfoObj *g_infoObj;int WinMain(){g_infoObj->sub_515000();}//校验si4.lic中签名的函数int sub_512CF0(){sub_45A770(); //加载si4.lic文件 sub_457D80(); //遍历si4.lic中的数据,查找<Signature>键值对sub_458520(); //从<Signature>键值对中,获取Value值sub_402E00(); //Base64解码Signature值,得到二进制签名数据sub_510640(); //使用CryptoAPI校验 RSA签名,校验成功返回C8}class InfoObj{int mLicenseType; //offset:+0 许可证类型:Trial/Beta/Standard//InfoObj的构造函数sub_512BF0() {}sub_515000() {this.sub_514ED0();}sub_514ED0() {//检查C:\ProgramData\Source Insight\4.0\si4.lic文件是否存在boolean success = sub_417E40();if(success){//如果si4.lic文件存在,则执行校验this.sub_5140E0(); }}sub_5140E0() {//加载si4.lic,校验字段值,保存校验结果int loadLicResult = this.sub_5127A0();if(loadLicResult != 0xC8) {//弹出错误窗口return false;}//检查是否是一次性许可if(mLicenseType == 2) {//弹出错误窗口return false;}//加载si4.lic,校验签名if(sub_512CF0() != 0xC8) {//弹出错误窗口return false;} }//读si4.lic文件,校验并保存字段值sub_5127A0(){sub_511150(); //加载si4.lic文件,这个函数里调用了 sub_45A770 CreateFileMappingW APIsub_510570("Type");//获取<Type>值,与"Trial"、"Beta"、"Standard"相比较,设置校验结果sub_510570("Serial"); //获取<Serial>值,不存在则结束,返回失败标志sub_510570("ActId"); //获取<ActId>值,如果值等于"Deferred",设置mLicenseType=2,表明si4.lic是断网激活时生成的一次性许可证...}};
调用关系看起来还是有些复杂,这里强调重要的三个函数作用,其中后两个函数需要还原成C语言代码, 以便验证自己签名的数据是否正确:
sub_5127A0函数,是校验si4.lic文件中字段的函数。如果使用之前生成的一次性许可,调用这个函数会校验失败,后续也不会走校验签名的逻辑。所以,需要注册一个试用许可,以便通过该函数的校验。sub_402E00函数,Base64解码函数,将si4.lic文件中<Signature>签名字符串,转为0x100字节大小的二进制签名数据。sub_510640函数,调用CryptoAPI,校验二进制签名数据。
2.2 简单的po jie
在2.1 节的调用流程中,sub_512CF0函数是签名校验的入口函数,如果此函数返回值等于0C8h,则表示校验通过;如果返回值不等于0C8h,则会弹出验证错误窗口,汇编代码如下面所示。
在*.text:005141A8*处检查校验结果,成功就跳转到loc_5141D4,失败则会执行错误流程。
所以,可以将此处的比较跳转jz改为无条件跳转jmp,修改二进制就是将 74 改为 EB,这样就过掉了校验签名。
.text:0051419B E8 50 EB+call sub_512CF0.text:005141A0 83 C4 08 addesp, 8.text:005141A3 3D C8 00+cmpeax, 0C8h.text:005141A8 74 2Ajzshort loc_5141D4.text:005141AA.text:005141AAloc_5141AA: ; CODE XREF: .text:00514179j.text:005141AA; .text:0051418Bj.text:005141AA 83 BC 24+cmpdword ptr [esp+108h], 0.text:005141B2 74 0Djzshort loc_5141C1.text:005141B4 50 push eax.text:005141B5 8B CEmovecx, esi.text:005141B7 E8 D4 CB+call sub_510D90.text:005141BC E8 2F E9+call sub_412AF0.text:005141C1.text:005141C1loc_5141C1: ; CODE XREF: .text:005141B2j.text:005141C1 8B CEmovecx, esi.text:005141C3 E8 08 CF+call sub_5110D0.text:005141C8 33 C0xoreax, eax.text:005141CA 5E popesi.text:005141CB 81 C4 00+addesp, 100h.text:005141D1 C2 04 00 retn 4.text:005141D4; ---------------------------------------------------------------------------.text:005141D4.text:005141D4loc_5141D4: ; CODE XREF: .text:00514184j.text:005141D4; .text:005141A8j
2.3 自签名license文件po jie
上面分析调用流程时,已经提到 :
sub_402E00函数,会把si4.lic文件中的字符串签名解码为0x100字节大小的二进制签名数据,命名该函数为Base64Decode;
sub_510640函数,会用程序中自带的2048位非对称加密的公钥,以si4.lic文件中<Signature>标签之前的数据作为校验数据,与二进制签名数据作比对,命名该函数为VerifySignature
先粗略描述一下服务器签名以及本地验证签名的过程:
首先,服务器有一对非对称密钥,导出PEM格式公钥后,写死在程序中,在后面VerifySignature函数中可以看到这个字符串公钥。私钥是存储在服务器上,且要严密保存的。在si4.lic文件中,<Signature>标签之前的所有内容(还要去掉所有的 \r\n\t和空格),就是待签名的数据。服务器使用私钥对数据签名后,生成0x100字节大小的签名数据,这部分数据经过 Base64编码,变成字符串数据,也就是si4.lic文件中<Signature>标签中的字符串。程序启动后,会加载si4.lic文件,先调用Base64Decode函数将文件中的 Signature 字符串签名转为二进制签名,再把二进制签名和si4.lic中原始的待签名的数据传给VerifySignature函数,该函数内调用CryptVerifySignatureWAPI校验签名是否匹配。
si4.lic文件内容如下:
<!--Source Insight 4.x License FileDO NOT EDIT THIS FILE. Doing so will render it unusable.This license was created for:xxx51asmxxx@--><SourceInsightLicense><HeaderValue="1"/><LicensePropertiesLicensedUser="51asm"ActId="9930152826"HWID="ZM6QWPSW-MNFUHVF5"Serial="S4SG-KRGM-YD7Q-RCFY"Organization=""Email="xxx@"Type="Standard"Version="4"MinorVersion="0"Date="-08-15"Expiration="2030-08-15"/><SignatureValue="TMNEF2MnPkl3+gLskqe8+X1Yq6ZiqFBMddeWJWC9ttGxxSBRHDMX9QCJowo5ffHqURy5/dhSJ5rDsqxMAK5h30WXcjJcP8D2Cc0P0igilnKX9gFoX/FaBMnDbQMTD6bq4UbV6lxiFXxmTVW8/Xt1rA1b7nMzdp1apkAquyPQizglC471Jo1JMeehEuVeLAe38cENDDQVQtV28u9AGyTaCrA6IIOIsJSrWeldmW8VHbTkmD4bt87OdTDt/oN3+sDcV8idMm02eGCX2/HQ/Ef0ozGQK1BoljDEJtGXlhzzKputxWK/O36WPPrE5iuiFNmfrqewH3NMhoEWszhFxapuHg=="/></SourceInsightLicense>
2.2.1 Base64Decode函数
这个是还原sub_402E00汇编代码的函数,该函数将si4.lic文件中的 Signature字符串签名数据解码为二进制签名数据。
char* szSignature传入参数,文件中<Signature>标签的字符串签名数据
BYTE* ptr_pbSignature传出参数,存放解码后的二进制签名数据的缓冲区
DWORD* ptr_dwSigLen传出参数,保存二进制签名数据的大小
//.text:00512DEB call sub_402E00int Base64Decode(char* szSignature, BYTE* ptr_pbSignature, DWORD* ptr_dwSigLen){int i = 0;int j = 0;int edi = 0;int edx = 0;while (true){unsigned int ecx = 0;if ((unsigned int)(szSignature[i] - 'A') <= 0x19) // char = 'A' ~ 'Z'{// ecx = 0 ~ 19hecx = (unsigned int)szSignature[i] - 'A';if (ecx < 0){break;}}else if ((unsigned int)(szSignature[i] - 'a') <= 0x19) // char = 'a' ~ 'z'{// ecx = 1Ah ~ 33hecx = (unsigned int)szSignature[i] - 'G';if (ecx < 0){break;}}else if ((unsigned int)(szSignature[i] - '0') <= 9)// char = '0' ~ '9'{// ecx = 34h ~ 3Dhecx = (unsigned int)szSignature[i] + 4;if (ecx < 0){break;}}else if (szSignature[i] == '+')// char = 2Bh{// ecx = 3Ehecx = '>';}else if (szSignature[i] == '/')// char = 2Fh{// ecx = 3Fhecx = '?';}else{break;}edi <<= 6;edi |= ecx;edx += 6;i++;edi &= 0xFFFF;if ((edx & 0xFFFF) >= 8){edx += 0xFFF8;unsigned short di = edi & 0xFFFF;unsigned char dl = edx & 0xFF;BYTE bt = (di >> dl) & 0xFF;ptr_pbSignature[j++] = bt;}}while (szSignature[i] == '='){i++;}*ptr_dwSigLen = j;return i;}
2.2.2 VerifySignature函数
这个是还原sub_510640汇编代码的函数,该函数完成签名校验。
BYTE* pbData待签名字符串数据,来源自si4.lic文件中<Signature>标签之前的所有内容(去掉所有的 \r\n\t和空格)
DWORD dwDataLen待签名字符串数据的长度
BYTE* pbSignature私钥签名的数据,2.2.1 节 Base64Decode函数的传出参数
DWORD dwSigLen私钥签名的数据长度,2.2.1 节 Base64Decode函数的传出参数
//.text:00512E08 call sub_510640DWORD VerifySignature(BYTE* pbData, DWORD dwDataLen, BYTE* pbSignature, DWORD dwSigLen){DWORD cbPublicKey = 0;//PEM格式公钥,作为CryptStringToBinaryA函数的第一个参数//.text:00510658 push offset pszString ; "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgk"..LPCSTR publicKey = g_pszPublicKey;BYTE bBinary[0x800] = {0 };DWORD cbBinary = sizeof(bBinary);if (!CryptStringToBinaryA((LPCSTR)publicKey, cbPublicKey, CRYPT_STRING_BASE64HEADER, bBinary, &cbBinary, NULL, NULL))return 0x1D8;PCERT_PUBLIC_KEY_INFO pvStructInfo = NULL;DWORD cbStructInfo = 0;if (!CryptDecodeObjectEx(X509_ASN_ENCODING, X509_PUBLIC_KEY_INFO, bBinary, cbBinary,CRYPT_DECODE_ALLOC_FLAG, NULL, &pvStructInfo, &cbStructInfo))return 0x1D8;HCRYPTPROV hProv = NULL;if (!CryptAcquireContextW(&hProv, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT))return 0x1D9;HCRYPTKEY hKey = NULL;if (!CryptImportPublicKeyInfo(hProv, X509_ASN_ENCODING, pvStructInfo, &hKey))return 0x1DA;LocalFree(pvStructInfo);HCRYPTHASH hHash = NULL;if (!CryptCreateHash(hProv, CALG_SHA1, NULL, 0, &hHash))return 0x1DA;if (!CryptHashData(hHash, pbData, dwDataLen, 0))return 0x1DB;BOOL success = CryptVerifySignatureW(hHash, pbSignature, dwSigLen, hKey, NULL, 0); CryptDestroyHash(hHash);CryptReleaseContext(hProv, 0);if (success)return 0xC8;return 0x1CE;}
2.2.3 自签名 si4.lic 文件
自签名许可证分为两个部分,
第一部分:签名数据
先导出公钥,并手工替换掉sourceinsight4.exe中的公钥签名si4.lic中的待签名数据,得到二进制签名数据转换二进制签名数据为字符串,并拷贝到si4.lic文件的<Signature>标签中
第二部分:校验签名
调用上面的Base64Decode和VerifySignature函数,校验我们自己签名的数据是否正确。
代码如下,完整代码可点击右侧超链接,在 Github上查看
//待签名数据char* sig4_data = "<!--\SourceInsight4.xLicenseFile\DONOTEDITTHISFILE.Doingsowillrenderitunusable.\Thislicensewascreatedfor:\xxx\51asm\xxx@\-->\<SourceInsightLicense>\<Header\Value=\"1\"\/>\<LicenseProperties\LicensedUser=\"51asm\"\ActId=\"9930152826\"\HWID=\"ZM6QWPSW-MNFUHVF5\"\Serial=\"S4SG-KRGM-YD7Q-RCFY\"\Organization=\"\"\Email=\"xxx@\"\Type=\"Standard\"\Version=\"4\"\MinorVersion=\"0\"\Date=\"-08-15\"\Expiration=\"2030-08-15\"\/>";void main(){// ----------------------------------------------------// 第一部分:签名 SignTestSignVerify edc;//TestSignVerify是封装的CryptoAPI,用来做签名和校验edc.InitializeProviderForSigner(NULL, PROV_RSA_FULL);// 1. 导出公钥edc.ExportX509PEMPublicKey("public.key");// 用导出的字符串公钥 替换掉程序中的原始公钥,用WinHex打开程序后,直接搜索"BEGIN PUBLIC KEY"就可以定位到公钥字符串的位置// 注意Windows平台导出的公钥中,换行是 0D 0A,而程序中提供的公钥是 0A// 2. 签名数据BYTE* bSig = NULL;DWORD szSigLen = 0;if (!edc.SignMessage(CALG_SHA1,(BYTE*)sig4_data, // 待签名字符串数据,来源自si4.lic 文件中 <Signature>标签之前的所有内容(去掉所有的 \r\n\t和空格)strlen(sig4_data), // 待签名字符串数据长度(BYTE**)&bSig,// 传出参数,二进制签名数据&szSigLen)) // 传出参数,二进制签名数据大小return;// 3. Base64编码二进制签名,转为字符串签名char szSig[345] = {0 };Base64Encode(bSig, szSig);printf("%s\r\n", szSig);// 这里需要把szSig字符串拷贝到 si4.lic文件的<Signature>标签中// ----------------------------------------------------// 第二部分:校验 Verify// 1. Base64解码字符串签名,转为二进制签名BYTE bSigBuff[0x2000] = {0 };DWORD dwSigLen = 0;int nConvertLen = Base64Decode(szSig, bSigBuff, &dwSigLen);// 2. 校验签名DWORD dwRet = VerifySignature((BYTE*)sig4_data, strlen(sig4_data), bSigBuff, dwSigLen);if (dwRet == 0xC8)printf("success\r\n");elseprintf("failed\r\n");return;}
3. 在线检查序列号
完成步骤2的po jie之后,启动程序,可以正常运行。但是程序运行2分钟左右之后,会再次弹出需要激活的窗口,所以猜测:程序启动先验证本地许可证,之后又联网验证,验证失败则会再弹出需要激活窗口。
基于此猜测,在导入表中查找网络发包相关API,看到有HttpSendRequestWAPI,在调试器中下API断点,成功断下后,看到lpOptional参数中包含有Serial Number,如下图所示:
分析调用关系可知,每次程序启动,都会在sub_514290函数中创建在线检查序列号的线程,该线程的启动函数地址是sub_513470。所以,可以将该函数入口处代码修改为ret,直接结束该线程。参考下面的代码,就是修改.text:00513470地址处的 83 改为C3。
//该函数中创建在线检查序列号的线程.text:00514290 sub_514290proc near; CODE XREF: sub_464B00+A6p... ....text:005142F2 push esi ; int.text:005142F3 push 0; dwStackSize.text:005142F5 push offset sub_513470 ; 在线检查序列号的线程函数地址.text:005142FA call __beginthread//在线检查的线程函数.text:00513470sub_513470proc near; DATA XREF: sub_514290+65o.text:00513470 83 EC 18 subesp, 18h... ....text:0051347D 68 C0 D4+push 1D4C0h; dwMilliseconds.text:00513482 FF D5call ebp ; Sleep; sleep 两分钟... ....text:005135B6 E8 15 FB+call sub_5130D0; 此函数中调用发包的API.text:00513109 |-- call sub_511250.text:0051156E |-- call sub_425150.text:004252AD |-- call ds:HttpSendRequestW
如果觉得《Windows逆向学习笔记——Source Insight 4 注册机制研究》对你有帮助,请点赞、收藏,并留下你的观点哦!