周其仁:停止改革,我们将面临三大麻烦

抛开立场观点不谈,且看周小平写一句话能犯多少语病

罗马尼亚的声明:小事件隐藏着大趋势——黑暗中的风:坚持做对的事相信未来的结果

布林肯突访乌克兰,为何选择去吃麦当劳?

中国不再是美国第一大进口国,贸易战殃及纺织业? 美国进一步延长352项中国商品的关税豁免期

生成图片,分享到微信朋友圈

自由微信安卓APP发布,立即下载! | 提交文章网址
查看原文

Wibu Codemeter 7.3学习笔记——Codemeter API调用及通信协议

ericyudatou 看雪学苑 2023-04-24


本文为看雪论坛精华文章
看雪论坛作者ID:ericyudatou


Wibu Codemeter 是一个非常优秀的加密壳,论坛上讨论、研究的较少,只有少数帖子能够参考bluefish的wibu证书初探 ,德国威步CodeMeter通信协议中的漏洞分析 我斗胆记录一下我的学习历程,望请抛砖引玉,不吝赐教。


PS:因为wibu codemeter一直没有泄露的源码或者pdb,所以我对函数的命名就很玄学,大家多多包涵。


0x01样品


Codemeter SDK(Axprotector)7.3 [下载]CodeMeter5.22 AxProtector 9.12下载 4楼5楼https://bbs.pediy.com/thread-274263.htm

Ap**s

Gas****

建议使用IDA Pro搭配FindCrypt ScyllaHide,ScyllaHide。ScyllaHide调成下面的选项就能过掉所有的反调试反跟踪手段。

IDA调试前会报一大堆错,直接设置成No Suspend Pass to application静默处理即可,不要试图这时候调试,容易跑飞。

EXCEPTION_ILLEGAL_INSTRUCTION No Application Silent

EXCEPTION_ACCESS_VIOLATION No Application Log

EXCEPTION_WX86_BREAKPOINT No Application Log

MS_VC_EXCEPTION No Application Silent


0x02初步分析


Codemeter整体架构类似flexlm,rlm,都为server-client式的授权系统但破解难度上来说前者是后者的十倍甚至九倍。


Server Codemeter.exe(主要负责CmContainer即license的授权管理,为重点研究对象) 


CodemeterCC.exe CmWebAdmin.exe(为Codemeter.exe的图形化交互界面不重要)


Client wibucm32.dll wibucm64.dll(负责架起未用AxProtector加密的用户软件与Server间的桥梁,能被aheadlib的方式破掉)


被Axprotector保护的用户软件(直接与Server通信)


对于java,.Net,python则是通过WibuCmNET.dll,CodeMeter.jar等间接调用wibucm32(64).dll,大同小异不再赘述。


下面贴一段从Ap**s中扒下来进行授权的代码,Ap**s本体调用wibucm64.dll中的函数来实现授权。

if ( a1[8] == 2 ) { memset(&CMACC, 0, sizeof(CMACC)); CMACC.mflCtrl |= 0x100u; setReleaseDate(&CMACC, *a1); CMACC.mulFirmCode = a2; CMACC.mulProductCode = a1[6] + 9000; if ( (unsigned int)sub_1813506F0(v7) ) { CMACC.mulSerialNumber = sub_1813506F0(v8); CMACC.musBoxMask = sub_1813506D0(v9); } strncpy(CMACC.mszServername, "localhost", 0x80ui64); hcmse = CmAccess2(0i64, &CMACC);//获取对应license的指针,失败,不存在返回0 hcmse_1 = hcmse; if ( hcmse )//判断一,必修得有这个license { v35 = (void ***)hcmse; len = CmGetInfo(hcmse, 4i64, 0i64); // CM_GEI_ENTRYDATA 0x0004 len_1 = len; if ( len >= 528 ) { v18 = len / 0x210ui64; v19 = (int)v18; entrydata = (CMENTRYDATA *)operator new(saturated_mul((int)v18, 0x210ui64)); if ( (unsigned int)CmGetInfo(hcmse_1, 4i64, entrydata) )//获取license的用户自定义数据 { if ( (int)v18 > 0 ) { secret_data = entrydata->mabData; do { v24 = *((_DWORD *)secret_data - 4); if ( (_WORD)v24 == 64 && (v24 & 0xFFFF0000) == 0x10000 ) //判断二:用户自建算法 { v25 = *((_DWORD *)secret_data - 1); if ( v25 >= 1 ) a1[14] = *secret_data; if ( v25 >= 2 ) a1[15] = secret_data[1]; if ( v25 >= 3 ) a1[16] = secret_data[2]; if ( v25 >= 4 ) a1[17] = secret_data[3]; if ( v25 >= 5 ) a1[18] = secret_data[4]; } secret_data += 528; --v19; } while ( v19 ); } if ( a1[9] == a1[14] && a1[10] == a1[15] && a1[11] == a1[16] && a1[12] == a1[17] && a1[13] == a1[18] ) { v17 = 0; }


从这段代码以及wibu官方提供的指南可知,wibu codemeter checkout时只是去服务器上查找是否有对应授权,但在客户端上却不进行检查,有也是软件服务商的自建算法。因此,我们需要重点照顾的对象便是CmAccess和CmGetInfo这两个函数。


0x03向CmAccess进发


WibuCm32(64).dll没有加壳所以分析还是比较快乐的,导出的CmAccess通过几层皮套Api到了真正做事的地方(Codemeter Api中所有函数,类型,结构体等都能在Codemeter/Devkit/include/CodeMeter.h中找到,可以稍微减轻点工作量)。

CODEMETER_API HCMSysEntry CMAPIENTRY CmAccess(CMULONG flCtrl, CMACCESS *pcmAcc); CODEMETER_API HCMSysEntry CMAPIENTRY CmAccess2(CMULONG flCtrl, CMACCESS2 *pcmAcc);

int __thiscall cm_access(int calltbl, int flCtrl, CMACCESS2 *cmacc, int isCMACCESS2){ int calltbl_1; // esi UINT_PTR len; // eax int hcmse; // eax char isCMACCESS2_1; // cl unsigned int mulUsedRuntimeVersion; // eax char flCtrl_1; // di void *v10; // eax _DWORD *v11; // eax _BYTE *v12; // eax __int16 v13; // ax char v14; // al int hcmse_1; // edi CMACCESS2 *cmacc_2; // eax int v17; // eax char v18; // al unsigned int serverversion; // eax unsigned int runtimeversion; // [esp+10h] [ebp-188h] int v22; // [esp+14h] [ebp-184h] CMACCESS2 *cmacc_1; // [esp+18h] [ebp-180h] CMACCESS2 *cmacc_3; // [esp+1Ch] [ebp-17Ch] int buf_1[56]; // [esp+28h] [ebp-170h] BYREF __int128 v26; // [esp+108h] [ebp-90h] BYREF unsigned int buf[16]; // [esp+118h] [ebp-80h] BYREF _QWORD buf_2[6]; // [esp+158h] [ebp-40h] BYREF int v29; // [esp+194h] [ebp-4h] calltbl_1 = calltbl; // /* // flags for mflCtrl in CMACCESS 280000h CM_ACCESS_SUBSYSTEM+ // */ // /* flags for kind of access */ // #define CM_ACCESS_USERLIMIT 0x00000000 // #define CM_ACCESS_NOUSERLIMIT 0x00000100 // #define CM_ACCESS_EXCLUSIVE 0x00000200 // #define CM_ACCESS_STATIONSHARE 0x00000300 // #define CM_ACCESS_CONVENIENT 0x00000400 // /* mask for the access modes */ // #define CM_ACCESS_STRUCTMASK 0x00000700 // // /* no validation check of the entry data */ // #define CM_ACCESS_FORCE 0x00010000 // /* constant for searching a fitting FSB entry */ // #define CM_ACCESS_CHECK_FSB 0x00020000 // /* constant for searching a fitting CTSB entry */ // #define CM_ACCESS_CHECK_CTSB 0x00040000 // /* allow normal subsystem access if no CmContainer is found */ // #define CM_ACCESS_SUBSYSTEM 0x00080000 // /* force FI access to prevent access to a FC:PC=x:0 */ // #define CM_ACCESS_FIRMITEM 0x00100000 // // /* flag access to borrow license */ // #define CM_ACCESS_BORROW_ACCESS 0x01000000 // /* flag release to borrow license */ // #define CM_ACCESS_BORROW_RELEASE 0x02000000 // /* flag check borrowed license */ // #define CM_ACCESS_BORROW_VALIDATE 0x04000000 // /* flag ignore entry state for release to borrow license */ // #define CM_ACCESS_BORROW_IGNORESTATE 0x08000000 // // #define CM_ACCESS_BORROW_MASK 0x0f000000 // /* flag ignore Linger Time of License */ // #define CM_ACCESS_IGNORE_LINGERTIME 0x10000000 cmacc_3 = 0; cmacc_1 = 0; len = 0x2C0; if ( !(_BYTE)isCMACCESS2 ) len = 0xB0; if ( !sub_385100((void *)calltbl, cmacc, len) )// ->sub_7D17A0 检查输入参数 // 给出的内存地址无效, 错误 113. return 0; if ( !cmacc ) {LABEL_5: (*(void (__thiscall **)(int, int))(*(_DWORD *)calltbl_1 + 4))(calltbl_1, 105);// 指定了一个无效的参数, 错误 105. return 0; } isCMACCESS2_1 = isCMACCESS2; if ( (_BYTE)isCMACCESS2 ) { cmacc_1 = cmacc; if ( !cmacc->mulFirmCode && cmacc->mulProductCode == 0x186A0 ) cmacc->mflCtrl |= 0x10000u; if ( cmacc->mulLicenseQuantity > 1 ) { mulUsedRuntimeVersion = cmacc->mulUsedRuntimeVersion; if ( mulUsedRuntimeVersion ) { if ( mulUsedRuntimeVersion < 0x614083E ) goto LABEL_5; } else { cmacc->mulUsedRuntimeVersion = 0x614083E; } } } else { cmacc_3 = cmacc; if ( !cmacc->mulFirmCode && cmacc->mulProductCode == 100000 ) cmacc->mflCtrl |= 0x10000u; } flCtrl_1 = flCtrl; if ( flCtrl < 0 ) { v10 = (void *)sub_383BC0(); if ( !sub_383E90(v10) ) { (*(void (__thiscall **)(int, int))(*(_DWORD *)calltbl_1 + 4))(calltbl_1, 101);// 未找到CodeMeter服务器, 错误 101. return 0; } isCMACCESS2_1 = isCMACCESS2; } v22 = 0; while ( 1 ) { if ( isCMACCESS2_1 ) { hcmse_1 = cmaccesss2((_BYTE *)calltbl_1, flCtrl_1, cmacc_1);// cmaccess2重点 cmacc_2 = cmacc_1; } else { memset((__m128i *)buf_1, 0, sizeof(buf_1));// cmaccess1 buf_1[1] = -1; buf_1[2] = 0; LOBYTE(buf_1[3]) = 0; buf_1[4] = 0; buf_1[5] = 0; buf_1[6] = -1; buf_1[7] = 0; LOBYTE(buf_1[8]) = 0; buf_1[0] = (int)&YS0061::`vftable'; LOBYTE(buf_1[9]) = 10; memset((__m128i *)&buf_1[10], 0, 0xB8u); v29 = 0; v11 = (_DWORD *)sub_32CD60(); cmacc_3->mulReserved1 = getpid(v11); v12 = (_BYTE *)sub_383BC0(); if ( sub_384C80(v12) ) cmacc_3->mulReserved1 = 0; buf_1[10] = flCtrl_1 & 0x13 | 0x10000000; qmemcpy(&buf_1[11], cmacc_3, 0xB0u); calltbl_1 = calltbl; v13 = *(_BYTE *)(calltbl + 500) ? *(_WORD *)(calltbl + 498) : (unsigned __int8)sub_3741C0((_BYTE *)calltbl); HIWORD(buf_1[17]) = v13; v14 = send_cm_socket_req(calltbl + 24, buf_1, 184, 8u, 1); //重点 v29 = -1; buf_1[0] = (int)&YS0061::`vftable'; hcmse_1 = v14 ? buf_1[55] : 0; sub_39E140(buf_1); cmacc_2 = cmacc_3; } runtimeversion = cmacc_2->mulUsedRuntimeVersion; if ( !hcmse_1 ) break; if ( (unsigned __int8)sub_3A2480(hcmse_1) ) { ++*(_DWORD *)(calltbl_1 + 104); break; } memset((__m128i *)buf, 0, sizeof(buf)); sub_370CC0((int)buf); v29 = 1; v17 = sub_3A25B0(calltbl_1 + 92, hcmse_1); sub_39E430(buf, (unsigned __int16)hcmse_1, 0x200B, 0x10u, v17); v18 = send_cm_socket_req(calltbl_1 + 24, buf, 16, 0x1Cu, 0);// cm_get_info v26 = 0i64; if ( !v18 || !sub_39E2A0(buf, (unsigned int)&v26) ) { (*(void (__thiscall **)(int, _DWORD))(*(_DWORD *)calltbl_1 + 4))(calltbl_1, 0);// good sub_3A2630((unsigned __int16)hcmse_1); if ( (unsigned __int8)sub_3A2480(hcmse_1) ) { ++*(_DWORD *)(calltbl_1 + 104); v29 = -1; buf[0] = (unsigned int)&YS0065::`vftable'; if ( buf[15] ) (**(void (__thiscall ***)(unsigned int, int))buf[15])(buf[15], 1); sub_39E140(buf); break; } } memset((__m128i *)buf_2, 0, sizeof(buf_2)); HIDWORD(buf_2[0]) = -1; LODWORD(buf_2[1]) = 0; BYTE4(buf_2[1]) = 0; buf_2[2] = 0i64; buf_2[3] = 0xFFFFFFFFi64; LOBYTE(buf_2[4]) = 0; LODWORD(buf_2[0]) = &YS0060::`vftable'; BYTE4(buf_2[4]) = 11; LOBYTE(v29) = 2; buf_2[5] = (unsigned int)hcmse_1; send_cm_socket_req(calltbl_1 + 24, buf_2, 8, 8u, 0); LODWORD(buf_2[0]) = &YS0060::`vftable'; sub_39E140(buf_2); v29 = -1; buf[0] = (unsigned int)&YS0065::`vftable'; if ( buf[15] ) (**(void (__thiscall ***)(unsigned int, int))buf[15])(buf[15], 1); sub_39E140(buf); flCtrl_1 = flCtrl; isCMACCESS2_1 = isCMACCESS2; if ( ++v22 >= 5 ) { (*(void (__thiscall **)(int, int))(*(_DWORD *)calltbl_1 + 4))(calltbl_1, 127);// CodeMeter客户端和服务端的句柄不一致, 错误 127. return 0; } } hcmse = (unsigned __int16)hcmse_1; if ( (_WORD)hcmse_1 && runtimeversion ) { serverversion = get_server_version((void *)calltbl_1, (unsigned __int16)hcmse_1); if ( serverversion && serverversion >= runtimeversion ) { return (unsigned __int16)hcmse_1; } else { (*(void (__thiscall **)(int, _DWORD))(*(_DWORD *)calltbl_1 + 16))(calltbl_1, (unsigned __int16)hcmse_1);// sub_7CDB80 (*(void (__thiscall **)(int, int))(*(_DWORD *)calltbl_1 + 4))(calltbl_1, 0x7D);// 所连接的CodeMeter 服务器版本过旧, 错误 125. return 0; } } return hcmse;}int __thiscall cmaccesss2(_BYTE *this, char flCtrl, CMACCESS2 *CMACC){ _DWORD *v4; // eax _BYTE *v5; // eax const char *v6; // edx void *v7; // ecx int hcmse; // esi char *v9; // edi bool v10; // zf char v11; // al void *v13[5]; // [esp+10h] [ebp-28h] BYREF unsigned int v14; // [esp+24h] [ebp-14h] const char *v15; // [esp+28h] [ebp-10h] int v16; // [esp+34h] [ebp-4h] int buf[188]; // [esp+38h] [ebp+0h] BYREF __int64 v18; // [esp+328h] [ebp+2F0h] BYREF int v19; // [esp+330h] [ebp+2F8h] memset((__m128i *)buf, 0, sizeof(buf)); buf[1] = -1; buf[2] = 0; LOBYTE(buf[3]) = 0; buf[4] = 0; buf[5] = 0; buf[6] = -1; buf[7] = 0; LOBYTE(buf[8]) = 0; buf[0] = (int)&YS0062::`vftable'; LOBYTE(buf[9]) = 100; memset((__m128i *)&buf[10], 0, 0x2C8u); v16 = 0; v4 = (_DWORD *)sub_4FCD60(); // get proccess CMACC->mcmCredential.mulPID = getpid(v4); v5 = (_BYTE *)sub_553BC0(); if ( sub_554C80(v5) ) // 是否为wine模拟器 CMACC->mcmCredential.mulPID = 0; CMACC->mcmCredential.mbLibType = 1; CMACC->mcmCredential.mulLibVersion = 0x71E12D4; setusername(CMACC); v19 = 0; v18 = 0i64; sub_549300((int)&v18); LOBYTE(v16) = 1; v6 = sub_4F9BE0(&v18); v13[4] = 0; v14 = 15; LOBYTE(v13[0]) = 0; v15 = v6 + 1; allocstring(v13, (unsigned int)v6, strlen(v6)); LOBYTE(v16) = 2; sub_57B260((int)&CMACC->mcmCredential, (int)v13); if ( v14 >= 0x10 ) { v7 = v13[0]; LOBYTE(v16) = 3; if ( v14 + 1 >= 0x1000 ) { v7 = (void *)*((_DWORD *)v13[0] - 1); if ( (unsigned int)(v13[0] - v7 - 4) > 0x1F ) sub_50EA4C(); } sub_5D3A97(v7); } LOBYTE(v16) = 0; sub_48FC60((void **)&v18); if ( sub_57B9C0((__m128i *)CMACC->mcmCredential.mszUserDefinedText, 0x80u) ) { CMACC->mcmBorrowData.mszClientName[127] = 0; if ( !strlen(CMACC->mcmBorrowData.mszClientName) ) gethostname(CMACC->mcmBorrowData.mszClientName, 127); v9 = &CMACC->mcmBorrowData.mszClientName[strlen(CMACC->mcmBorrowData.mszClientName) + 1]; memset((__m128i *)(v9 - 1), 0, 128 - (v9 - &CMACC->mcmBorrowData.mszClientName[1])); v10 = this[500] == 0; buf[10] = flCtrl & 0x13 | 0x10000000; qmemcpy(&buf[11], CMACC, 0x2C0u); if ( v10 ) { buf[128] = (unsigned __int8)sub_5441C0(this); HIBYTE(buf[179]) = 0; } else { v11 = sub_5441C0(this); buf[128] = *((unsigned __int16 *)this + 249); HIBYTE(buf[179]) = v11; } if ( send_cm_socket_req((int)(this + 24), buf, 712, 8u, 1) ) hcmse = buf[187]; else hcmse = 0; } else { (*(void (__thiscall **)(_BYTE *, int))(*(_DWORD *)this + 4))(this, 105);// 指定了一个无效的参数, 错误 105. hcmse = 0; } buf[0] = (int)&YS0062::`vftable'; sub_56E140(buf); return hcmse;}


通过分析,可以一眼丁真的分析可以发现整个cm_access函数中基本上都是参数,版本检查,错误处理的鸡毛蒜皮的小事,而cmacc这个重要的参数最终传进了send_cm_socket_req这个函数。


看一眼send_cm_socket_req的交叉引用,发现这个函数跟基本上所有的Cm API有一腿。

send_cm_socket_Reqcm_get_info_cm_access_borrowcm_accessCmCalculateSignature_1CmControl_1cm_get_info_1CmProgramS_1CmBoxIoControl_0CmCalculateSignature_0CmControl_0CmCreateLicenseFile_0CmCreateProductItemOption_0CmCreateSequence_0CmEnablingGetChallenge_0CmEnablingSendResponse_0CmExecuteRemoteUpdate_0CmGetAccountInfo_0CmGetBoxDiagnosticData_0CmGetBoxes_0CmGetContainerInfo_0CmGetFileInfo_0CmCalculatePioCoreKey_0CmEnablingGetApplicationContext_0CmEnablingWriteApplicationKey_0CmBoxIoControlCmCalculateSignatureCmControlCmExtendedDiscControl_0CmCreateLicenseFile...(212 Lines Total)


所以基本断定,授权是在服务器codemeter.exe上进行,wibucm32(64)就是个给人打工的小喽喽。


0x04初探send_cm_socket_req


char __thiscall send_cm_socket_req(int this, _DWORD *buf, int final_length, unsigned int len2, char flag){ _DWORD *buf_1; // edi int v7; // eax int err_4; // eax int function_table_006F8A58; // eax char result; // al int v11; // eax int v12; // ebx char err; // cl int v14; // eax int err_2; // eax int v16; // eax unsigned __int64 v17; // kr10_8 int v18; // eax int v19; // eax int v20; // eax int v21; // eax int err_3; // [esp+10h] [ebp-38h] int v23; // [esp+14h] [ebp-34h] int v24; // [esp+1Ch] [ebp-2Ch] char err_1; // [esp+23h] [ebp-25h] int v26[2]; // [esp+24h] [ebp-24h] BYREF int time1[2]; // [esp+2Ch] [ebp-1Ch] BYREF LPCRITICAL_SECTION *v28[5]; // [esp+34h] [ebp-14h] BYREF buf_1 = buf; v7 = sub_553BC0(); // 多线程相关 err_4 = sub_554DF0(v7, 0); // should ret 1 Codemeter服务器检测 没启动的话挂起服务 err_3 = err_4; if ( err_4 != 1 ) { switch ( err_4 ) { case 0: case 2: case 3: function_table_006F8A58 = get_function_table_006F8A58(); (*(void (__thiscall **)(int, int))(*(_DWORD *)function_table_006F8A58 + 4))(function_table_006F8A58, 101);// 未找到CodeMeter服务器, 错误 101. result = 0; break; case 4: v11 = get_function_table_006F8A58(); (*(void (__thiscall **)(int, int))(*(_DWORD *)v11 + 4))(v11, 308);// 该数据无法写入安全区, 错误 308. goto LABEL_5; default:LABEL_5: result = 0; break; } return result; } v28[0] = 0; sub_4FE310(v28, (LPCRITICAL_SECTION *)(this + 60)); v28[4] = 0; v12 = 0; v24 = 3; v23 = 0; while ( 1 ) { get_time(time1); err = send_cm_req_encrypt((struct_this_2 *)this, buf_1, final_length, len2);// should ret 0 加密数据包并发送 err_1 = err; if ( err && buf_1[2] == 238 ) // CodeMeter 许可服务器启动仍旧在待定中,错误238. { sleep(1000u); v14 = 20; v24 = 20; goto LABEL_24; // try again } err_2 = *(_DWORD *)(*(_DWORD *)(this + 40) + 12);// getlasterror if ( err_2 == 309 ) // CodeMeter许可服务器超载, 访问请求已超时,该请求已被拒绝, 错误309. { sleep(1000 * (v23 + 1)); v12 = v23; v24 = 20; v14 = 20; goto LABEL_24; // try again } if ( err ) {LABEL_31: v21 = get_function_table_006F8A58(); (*(void (__thiscall **)(int, _DWORD))(*(_DWORD *)v21 + 4))(v21, *(_DWORD *)(*(_DWORD *)(this + 40) + 12));// cm_set_error goto LABEL_32; } if ( flag && (err_2 == 101 || err_2 == 102) )// 无法将请求发送至其他CodeMeter服务器, 错误 102. // 未找到CodeMeter服务器, 错误 101. { v16 = sub_553BC0(); err_3 = sub_554DF0(v16, 1); } get_time(v26); v17 = v26[1] + 1000000 * (v26[0] - (__int64)time1[0]) - time1[1]; if ( v17 >= 950 * (unsigned __int64)(unsigned int)(*(int (__thiscall **)(_DWORD))(**(_DWORD **)(this + 40) + 96))(*(_DWORD *)(this + 40)) ) break; // 超时检测(网络,反调试) v12 = v23; if ( v23 != 2 || *(_DWORD *)(this + 56) != 1 ) { buf_1 = buf;LABEL_23: v14 = v24; goto LABEL_24; } buf_1 = buf; if ( (*(_BYTE *)(sub_5269C0() + 132) & 2) == 0 ) goto LABEL_23; *(_DWORD *)(this + 40) = *(_DWORD *)(this + 48); v14 = v24 + 1; *(_DWORD *)(this + 56) = 2; ++v24;LABEL_24: v23 = ++v12; if ( v12 >= v14 ) goto LABEL_28; } v18 = *(_DWORD *)(this + 40); if ( !*(_DWORD *)(v18 + 12) ) *(_DWORD *)(v18 + 12) = 100;LABEL_28: switch ( err_3 ) { case 0: case 1: goto LABEL_31; case 2: case 3: v19 = get_function_table_006F8A58(); (*(void (__thiscall **)(int, int))(*(_DWORD *)v19 + 4))(v19, 0x65);// 未找到CodeMeter服务器, 错误 101 break; case 4: v20 = get_function_table_006F8A58(); (*(void (__thiscall **)(int, int))(*(_DWORD *)v20 + 4))(v20, 0x134);// 该数据无法写入安全区, 错误 308. break; default: break; }LABEL_32: sub_4FE380(v28); return err_1;}


通过分析可以发现,send_cm_socket_req并未实现加密、发送数据的逻辑,除去进行连接服务器等不是那么重要的工作,都转交给send_cm_req_encrypt这个函数。


0x05CodeMeter has never been cracked……吗?


对用axprotector保护的axprotector.exe进行分析,尽管原始代码已经变得很抽象但是壳本体倒是没有进行混淆或者是虚拟机保护。抱有侥幸心理用bindiff比对一下,嚯,您猜怎么着,那堆CM API绝大部分都有,连咱心心念念的CmAccess,CmGetInfo都在,并且代码上跟wibucm32中的别无二致。还是通过send_cm_socket_req跟服务器唠嗑,自己就等着服务器给个授不授权的信儿然后解密。


再次我斗胆提出一个思路,望诸多大佬指正:


对于Wibu Axprotector,可以避其锋芒,不直接面对那些反调试,反dump的奇技淫巧,可否对壳与服务器,wibucm32(64).dll与服务器间的协议进行逆向,编写出一个虚假的服务器,让壳以为虚假的服务器进行了可信的授权然后运行呢?


类似的思路已经有人实践,TEAM R2R的codemeter waifu通过伪造wibucm32(64).dll攻击类似Ap**s这种通过调用dll进行授权的软件系统,并且这种攻击方式确实有效。


0x06通信协议(天坑)

诸位先贤

(bluefish的wibu证书初探

https://bbs.pediy.com/thread-274300.htm

德国威步CodeMeter通信协议中的漏洞分析

https://bbs.pediy.com/thread-265283.htm)

已经指出,Codemeter服务器与客户端间通信的私有协议的加密方式:

① 生成与时间(getTickCount)相关的random key getTickCount->sha1->randomkey。
② 原始报文进行crc32并与原报文进行拓展。
③ 对拓展的报文进行AES-128 CBC加密,密钥,iv拆分于randomkey。


德国威步CodeMeter通信协议中的漏洞分析(https://bbs.pediy.com/thread-265283.htm)这篇文章给出了encrypt_telegram这个函数的实现方式,因此为我们减少了很大的工作量。

int __thiscall getRandomkey(struct_this *this, char flag){ int time; // edx int result; // eax unsigned int v5; // [esp+4h] [ebp-7Ch] BYREF _DWORD sha1[24]; // [esp+8h] [ebp-78h] BYREF char sha1res[20]; // [esp+68h] [ebp-18h] BYREF if ( flag == 1 ) time = GetTimeFromFile(0); else time = GetTickCount() / 1000; this->time = time; //this + 36 sha1[23] = 0; v5 = 1000 * time / 1009u; sha1_init(sha1); sha1_update(sha1, (unsigned int)&v5, 4u); result = sha1_final((char *)sha1, (int *)sha1res); this->key = *(_OWORD *)sha1res; //this + 4 this->iv = *(_OWORD *)&sha1res[4]; //this + 20 return result;}int __cdecl bitnegation(_DWORD *a1){ if ( a1 ) return ~*a1; else return -1;}__m128i *__cdecl expand_text(int buf, unsigned int *end_pos, char a3){ int v3; // ecx unsigned int v4; // eax __m128i *result; // eax if ( buf ) { if ( end_pos ) { // ========================== // | | | // ========================== // (end_pos) // // ============================= // | ^ --> | // ============================= // (ep) (ep+39) // (new_ep) // ============================== // | | // ============================== // (new_ep) // // ============================== // | |<=^ // ============================== // // =========================== // | | // =========================== // 最终使长度变为16的整数倍 v3 = *end_pos; v4 = (*end_pos + 0x27) & 0xFFFFFFF0; *end_pos = v4; return memset((__m128i *)(v3 + buf), a3, v4 - v3); } } return result;}char __thiscall encrypt_telegram(struct_this *this, char *buf, unsigned int *end_pos, char flag){ int nCrc; // edi unsigned int len; // esi unsigned int len_1; // eax struct_this *this_1; // [esp+10h] [ebp-Ch] char crc[4]; // [esp+14h] [ebp-8h] BYREF this_1 = this; init(crc); crc32((unsigned int *)crc, buf, *end_pos); nCrc = bitnegation(crc); getRandomkey(this, flag); len = *end_pos; expand_text((int)buf, end_pos, 0); len_1 = *end_pos; *(_DWORD *)&buf[len_1 - 4] = nCrc; // crc *(_DWORD *)&buf[len_1 - 8] = len; // 原长 return cm_aes_encrypt_cbc(this_1, buf, end_pos, 0);}


经过确认,这几个核心函数与德国威步CodeMeter通信协议中的漏洞分析中分析结果逻辑上相同,故可认为7.30a版本及之前通信协议的加密方法是相同的,可以进行复用。


但应该指出的德国威步CodeMeter通信协议中的漏洞分析这篇文章只是分析了encrypt_telegram这个函数及其输入参数,并没有细究其中输入的数据与调用的操作之间的联系。我们最后要做到伪造服务端,这种程度是不够的,因此我们应该对send_cm_socket_req send_cm_req_encrypt这两个函数进行分析,并找出其中的内在规律。


0x07 send_cm_socket_Req初探二——参数协议


char __thiscall send_cm_socket_Req( LPCRITICAL_SECTION *this, _DWORD *buf, //发送的数据报内容 unsigned int final_length,//长度 unsigned int len2, //长度final_length,len2取最长 char flag) //数据发送失败是否尝试重启Codemeter服务

显而易见,需要重点分析buf的结构,从wibucm32.dll中可以通过调用以及编译残留的字符串可以发现各个Cm api调用send_cm_socket_Req的参数。初步分析如下(完整版见附件):


通过归类可知,这些Cm Api分为三大类。


① 超短型:不需要传递太多的参数,如CmGetVersion CmGetInfoExt CmRelease CmGetBoxes等。


② 本地实现型:通过调用其他Cm Api或者直接本地解决,如CmCryptEcies CmCalculateDigest等。


③ 超长型:1.传递巨量参数 2.buf[9]=0 3.buf[1]~buf[8]={-1,0,0,0,0,-1,0,0}这个特定的开头 如CmGetContainerInfo CmGetAccountInfo CmWriteSettings 等。 (黄色标出),不是研究的重点。


④ 一般型:1.传递一定参数 2.buf[9]≠0 3.buf[1]~buf[8]={-1,0,0,0,0,-1,0,0}这个特定的开头 如CmAccess CmAccess2 CmGetInfo等,为研究重点。


通过分析我们可以发现一些未公开的Cm Api如CmControl CmCreateLicenseFile等,其中CmControl非常值得我们分析研究(挖坑)。


0x08 send_cm_socket_Req初探三——我们需要再深入一些send_cm_req_encrypt


char __thiscall send_cm_socket_req(int this, _DWORD *buf, int final_length, unsigned int len2, char flag){ _DWORD *buf_1; // edi int v7; // eax int err_4; // eax int function_table_006F8A58; // eax char result; // al int v11; // eax int v12; // ebx char err; // cl int v14; // eax int err_2; // eax int v16; // eax unsigned __int64 v17; // kr10_8 int v18; // eax int v19; // eax int v20; // eax int v21; // eax int err_3; // [esp+10h] [ebp-38h] int v23; // [esp+14h] [ebp-34h] int v24; // [esp+1Ch] [ebp-2Ch] char err_1; // [esp+23h] [ebp-25h] int v26[2]; // [esp+24h] [ebp-24h] BYREF int time1[2]; // [esp+2Ch] [ebp-1Ch] BYREF LPCRITICAL_SECTION *v28[5]; // [esp+34h] [ebp-14h] BYREF buf_1 = buf; v7 = sub_553BC0(); // 多线程相关 err_4 = sub_554DF0(v7, 0); // should ret 1 Codemeter服务器检测 没启动的话挂起服务 err_3 = err_4; if ( err_4 != 1 ) { switch ( err_4 ) { case 0: case 2: case 3: function_table_006F8A58 = get_function_table_006F8A58(); (*(void (__thiscall **)(int, int))(*(_DWORD *)function_table_006F8A58 + 4))(function_table_006F8A58, 101);// 未找到CodeMeter服务器, 错误 101. result = 0; break; case 4: v11 = get_function_table_006F8A58(); (*(void (__thiscall **)(int, int))(*(_DWORD *)v11 + 4))(v11, 308);// 该数据无法写入安全区, 错误 308. goto LABEL_5; default:LABEL_5: result = 0; break; } return result; } v28[0] = 0; sub_4FE310(v28, (LPCRITICAL_SECTION *)(this + 60)); v28[4] = 0; v12 = 0; v24 = 3; v23 = 0; while ( 1 ) { get_time(time1); err = send_cm_req_encrypt((struct_this_2 *)this, buf_1, final_length, len2);// should ret 0 加密数据包并发送 err_1 = err; if ( err && buf_1[2] == 238 ) // CodeMeter 许可服务器启动仍旧在待定中,错误238. { sleep(1000u); v14 = 20; v24 = 20; goto LABEL_24; // try again } err_2 = *(_DWORD *)(*(_DWORD *)(this + 40) + 12);// getlasterror if ( err_2 == 309 ) // CodeMeter许可服务器超载, 访问请求已超时,该请求已被拒绝, 错误309. { sleep(1000 * (v23 + 1)); v12 = v23; v24 = 20; v14 = 20; goto LABEL_24; // try again } if ( err ) {LABEL_31: v21 = get_function_table_006F8A58(); (*(void (__thiscall **)(int, _DWORD))(*(_DWORD *)v21 + 4))(v21, *(_DWORD *)(*(_DWORD *)(this + 40) + 12));// cm_set_error goto LABEL_32; } if ( flag && (err_2 == 101 || err_2 == 102) )// 无法将请求发送至其他CodeMeter服务器, 错误 102. // 未找到CodeMeter服务器, 错误 101. { v16 = sub_553BC0(); err_3 = sub_554DF0(v16, 1); } get_time(v26); v17 = v26[1] + 1000000 * (v26[0] - (__int64)time1[0]) - time1[1]; if ( v17 >= 950 * (unsigned __int64)(unsigned int)(*(int (__thiscall **)(_DWORD))(**(_DWORD **)(this + 40) + 96))(*(_DWORD *)(this + 40)) ) break; // 超时检测(网络,反调试) v12 = v23; if ( v23 != 2 || *(_DWORD *)(this + 56) != 1 ) { buf_1 = buf;LABEL_23: v14 = v24; goto LABEL_24; } buf_1 = buf; if ( (*(_BYTE *)(sub_5269C0() + 132) & 2) == 0 ) goto LABEL_23; *(_DWORD *)(this + 40) = *(_DWORD *)(this + 48); v14 = v24 + 1; *(_DWORD *)(this + 56) = 2; ++v24;LABEL_24: v23 = ++v12; if ( v12 >= v14 ) goto LABEL_28; } v18 = *(_DWORD *)(this + 40); if ( !*(_DWORD *)(v18 + 12) ) *(_DWORD *)(v18 + 12) = 100;LABEL_28: switch ( err_3 ) { case 0: case 1: goto LABEL_31; case 2: case 3: v19 = get_function_table_006F8A58(); (*(void (__thiscall **)(int, int))(*(_DWORD *)v19 + 4))(v19, 0x65);// 未找到CodeMeter服务器, 错误 101 break; case 4: v20 = get_function_table_006F8A58(); (*(void (__thiscall **)(int, int))(*(_DWORD *)v20 + 4))(v20, 0x134);// 该数据无法写入安全区, 错误 308. break; default: break; }LABEL_32: sub_4FE380(v28); return err_1;}


分析可知send_cm_socket_Req的主要功能是判断服务器状态以及错误处理,实际上转发给send_cm_req_encrypt。


0x09 通信协议初探


Codemeter在通信上除了使用一个私有的算法进行加密并与系统时间挂钩以外并没有别的门槛,只是一个简单的winsock通信框架,Wireshark能抓包。这里观察一下通信上的行为。



可以发现端口50777的客户端与端口22350的Codemeter服务器间的通信行为:



客户端对服务端的通信行为可以概括如下:


1、首先发送一个长度为16字节的明文握手包package01结构为如下,length为下一个加密数据包的长度。

struct handshake_package{ char magic[4] = "samc"; dword length; byte flag; char gap[7] = {00,01,00,00,00,00,00,00};}


2、发送一个长度为length的加密包package02,内容全部为加密后的数据,没有长度等信息。

(更正)



第二个加密数据包第一个字节是固定的,0xA0,之后才是被加密的数据。


3、等待服务器回话。


但服务端对客户端的通信行为却有点不同。



服务器对客户端倒像一个高冷御姐,没有了单独的握手包,但是却把握手包与数据杂合到一块。


服务器返回的数据包前16字节是跟客户端handshake_package的结构一样,后面就是长度为length的加密后的数据。


0x10 send_cm_req_encrypt,func_10_send_message, func_6_recv_telegram


char __thiscall send_cm_req_encrypt(struct_this_2 *this, _DWORD *buf, int final_length, unsigned int definelength){ unsigned int len2; // edx _DWORD *buf_1; // edi SIZE_T len1; // eax unsigned int lenbuf_2; // ecx __m128i *buf_2; // eax __int8 *v10; // ecx struct_name_3 *arr; // eax char err; // al int dword28; // edx int final_length_2; // ecx _DWORD *buf_4; // eax unsigned int len_1; // ecx _DWORD *calltb; // edx _DWORD *buf_3; // edi char v19; // al int v20; // ecx char *v21; // esi int v22; // eax char flag; // [esp+Ch] [ebp-58h] unsigned int len1_; // [esp+10h] [ebp-54h] int *plaintext; // [esp+10h] [ebp-54h] char err_1; // [esp+1Bh] [ebp-49h] unsigned int v28; // [esp+1Ch] [ebp-48h] BYREF int flag_1; // [esp+20h] [ebp-44h] BYREF struct_reallocateMem lpMem; // [esp+24h] [ebp-40h] OVERLAPPED BYREF char *v31; // [esp+3Ch] [ebp-28h] BYREF int v32; // [esp+40h] [ebp-24h] int v33; // [esp+44h] [ebp-20h] int len; // [esp+48h] [ebp-1Ch] BYREF int final_length_1; // [esp+4Ch] [ebp-18h] BYREF char gapshould1; // [esp+53h] [ebp-11h] BYREF int issuccess; // [esp+60h] [ebp-4h] len2 = 0x1000; // 准备:确定长度 buf_1 = buf; // buf + 0 ??_7YS0061@@6B@ if ( definelength > 0x1000 ) len2 = definelength; len1 = final_length + 56; v33 = 0; *(_OWORD *)&lpMem.dword0 = 0i64; if ( len2 > final_length + 56 ) len1 = len2; lenbuf_2 = 0; // len1取最大长度 final_length_1 = 0; *(_DWORD *)&lpMem.isReallocated = 1; len = len2; LOBYTE(flag_1) = 0; gapshould1 = 0; v28 = 0; len1_ = len1; lpMem.dword0 = &YS0073::YS0080<unsigned char>::`vftable'; memset(&lpMem.pBuf, 0, 12); *(_OWORD *)&lpMem.doInit0 = 0i64; issuccess = 0; if ( len1 ) // 准备:分配内存 { buf_2 = (__m128i *)allocmem(len1); lenbuf_2 = len1_; lpMem.pBuf = buf_2->m128i_i32; // buf lpMem.size = len1_; // len lpMem.used_size = len1_; // len if ( lpMem.doInit0 == 1 ) { memset(buf_2, 0, len1_); lenbuf_2 = lpMem.used_size; buf_2 = (__m128i *)lpMem.pBuf; } } else { buf_2 = 0; } lpMem.pBuf = buf_2->m128i_i32; issuccess = 1; if ( lenbuf_2 ) { v10 = &buf_2->m128i_i8[lenbuf_2]; // wtf? } else { v10 = 0; buf_2 = 0; } memset(buf_2, 0, v10 - (__int8 *)buf_2); // 准备发送 err_1 = prepareNetwork((int)this); // 网络连接ret 1 if ( err_1 ) { final_length_1 = final_length; arr = 0; if ( lpMem.used_size ) arr = (struct_name_3 *)lpMem.pBuf; plaintext = &arr->buf_new; err_1 = (*(int (__thiscall **)(_DWORD *, int *, int *))(*buf + 4))(buf, &arr->buf_new, &final_length_1);// expand_plaintext 将buf中的原文进行操作转换成v11+16数据包 if ( err_1 && final_length_1 == final_length ) { flag = (*(int (__thiscall **)(_DWORD *))(*buf + 24))(buf);// ret 0 NTI if ( flag ) { final_length_2 = final_length_1; } else { err = (*(int (__thiscall **)(struct_this_2 *, int *, int *, _DWORD))(this->dword0 + 12))(// encrypt_telegram this, plaintext, &final_length_1, this->unsigned___int840); dword28 = this->dword28; err_1 = err; if ( !err ) { *(_DWORD *)(dword28 + 12) = 301; // 对CodeMeter Runtime Server的访问操作无法被加密, 错误 301. goto LABEL_45; } final_length_2 = final_length_1; if ( (unsigned int)final_length_1 > *(_DWORD *)(dword28 + 16) ) { *(_DWORD *)(dword28 + 12) = 112; // 传递给CodeMeter驱动程序的数据段太小, 错误 112. goto LABEL_45; } } buf_4 = 0; if ( lpMem.used_size ) buf_4 = lpMem.pBuf; len_1 = final_length_2 + 1; *((_BYTE *)buf_4 + 15) = 0xA0; calltb = (_DWORD *)this->dword28; if ( len_1 < calltb[4] ) { err_1 = (*(int (__thiscall **)(_DWORD *, int, unsigned int, char))(*calltb + 24))( calltb, (int)buf_4 + 15, len_1, flag); // func_10_send_message 网络:发送数据 if ( err_1 ) { sub_56E3D0(buf); // buf[6]=-1 while ( 1 ) { err_1 = (*(int (__thiscall **)(_DWORD, struct_reallocateMem *, int *, int *, char *, _DWORD))(*(_DWORD *)this->dword28 + 32))(// char __thiscall func_5_recv_message(struct_this_1 *this, struct_reallocateMem *buf, int pLen, _BYTE *flag, int gapshould1, int a6) // 网络,接受服务器返回数据 this->dword28, &lpMem, &len, &flag_1, &gapshould1, 0); if ( !err_1 ) break; (*(void (__thiscall **)(_DWORD *, int))(*buf_1 + 28))(buf_1, flag_1); buf_3 = 0; if ( lpMem.used_size ) buf_3 = lpMem.pBuf; if ( !flag ) { if ( len == 1 ) goto LABEL_40; err_1 = (*(int (__thiscall **)(struct_this_2 *, _DWORD *, int *, _DWORD))(this->dword0 + 16))( this, buf_3, &len, this->unsigned___int840);// decrypt_telegram if ( !err_1 ) { *(_DWORD *)(this->dword28 + 12) = 302;// 通讯加解密出错, 错误 302. break; } } v28 = 0; if ( !sub_56F180(buf, buf_3, len, &v28) ) { if ( sub_56E240(buf_3, len) ) { *(_DWORD *)(this->dword28 + 12) = 309;// CodeMeter许可服务器超载, 访问请求已超时,该请求已被拒绝, 错误309.LABEL_40: err_1 = 0; break; } v19 = (*(int (__thiscall **)(_DWORD *, _DWORD *, int))(*buf + 8))(buf, buf_3, len); v20 = this->dword28; err_1 = v19; if ( v19 ) *(_DWORD *)(v20 + 12) = buf[2]; else *(_DWORD *)(v20 + 12) = 100; // 发生网络错误, 错误 100. break; } buf_1 = buf; } } } else { calltb[3] = 112; err_1 = 0; } } else { *(_DWORD *)(this->dword28 + 12) = 100; // 发生网络错误, 错误 100. } }LABEL_45: v21 = v31; v22 = v32; issuccess = 2; for ( lpMem.dword0 = &YS0073::YS0080<unsigned char>::`vftable'; v21 != (char *)v22; v21 += 4 ) { if ( *(_DWORD *)v21 ) { (*(void (__thiscall **)(_DWORD, _DWORD))(**(_DWORD **)v21 + 4))(*(_DWORD *)v21, 0);// good v22 = v32; } } if ( lpMem.isReallocated ) { if ( lpMem.pBuf ) { if ( lpMem.doInit0 == 1 ) memset((__m128i *)lpMem.pBuf, 0, lpMem.used_size); free(lpMem.pBuf); // realease } memset(&lpMem.pBuf, 0, 12); lpMem.isReallocated = 1; } release(&v31); // release return err_1;}char __userpurge func_10_send_message@<al>( _DWORD *this@<ecx>, int a2@<ebx>, int a3@<edi>, char *buf, unsigned int len, int a6){ unsigned int i; // edi int len_1; // eax int v12; // [esp-8h] [ebp-10h] int v14; // [esp-4h] [ebp-Ch] if ( (this[2] & 2) == 0 ) { this[3] = 100; // 发生网络错误, 错误 100. return 0; } if ( (*(unsigned __int8 (__thiscall **)(_DWORD *, unsigned int, int))(*this + 112))(this, len, a6) )// func_9_send_handshake // 握手,发送密文的长度 { i = 0; if ( !len ) return 1; while ( 1 ) { len_1 = send(this[5], buf, len - i, 0); // 发送加密报文 if ( len_1 < 0 ) break; i += len_1; buf += len_1; if ( i >= len ) return 1; } if ( (*(unsigned __int8 (__thiscall **)(_DWORD *, int, int))(*this + 80))(this, a3, a2) ) send(this[5], Default, 0, 0); if ( (*(unsigned __int8 (__thiscall **)(_DWORD *, int, int))(*this + 80))(this, v12, v14) ) { shutdown(this[5], 2); if ( closesocket(this[5]) < 0 ) this[3] = 100; // 发生网络错误, 错误 100. this[2] &= ~2u; this[5] = -1; } sub_4FE2C0(); this[3] = 102; // 无法将请求发送至其他CodeMeter服务器, 错误 102. } return 0;}bool __thiscall func_9_send_handshake(_DWORD *this, int len, char a3){ char buf[16]; // [esp+4h] [ebp-14h] BYREF constractHandshake((int)buf, len, a3); // 73 61 6D 63 D1 00 00 00 41 00 01 00 00 00 00 00 // // 73 61 6D 63 len 00 00 00 a3|1 00 01 00 00 00 00 00 return send_handshake(this, this[5], buf, 16) == 16;// this[5] socket}handshake_package *__cdecl constractHandshake(handshake_package *buf, int len, char a3){ *buf = 0i64; buf->len = len; *(_DWORD *)buf->magic = 'cmas'; buf->gap[1] = 1; if ( a3 ) buf->flag = a3 | 1; else buf->flag = 65; return buf;}char __thiscall func_5_recv_message( struct_this_1 *this, struct_reallocateMem *buf, int pLen, _BYTE *flag, int gapshould1, int a6){ SOCKET fd; // edi *flag = 0; fd = this->fd; if ( (this->dword8 & 2) != 0 ) { if ( (*(unsigned __int8 (__thiscall **)(struct_this_1 *, SOCKET, struct_reallocateMem *, int, _BYTE *, int))(this->dword0 + 36))( this, this->fd, buf, pLen, flag, gapshould1) ) // bool __thiscall func_6_recv_telegram(_DWORD *this, SOCKET fd, struct_reallocateMem *buf_2, int *plen, char *flag_1, _BYTE *gapshould1) { return 1; } else { if ( fd == this->fd ) { this->dword8 &= ~2u; this->fd = -1; } return 0; } } else { this->errcode = 100; return 0; }}bool __thiscall func_6_recv_telegram( struct_this_1 *this, SOCKET fd, struct_reallocateMem *buf_2, int *plen, char *flag_1, _BYTE *gapshould1){ int v6; // esi int len; // ebx char flag; // bl int len_1; // ecx struct_this_1 *v10; // edx unsigned int used_size; // eax SIZE_T newsize; // eax int v13; // eax __m128i *buf; // edi int v15; // eax unsigned int v17; // eax __m128i *pBuf; // edi int recvlen; // eax handshake_package buf_1; // [esp+1Ch] [ebp-14h] BYREF *flag_1 = 0; v6 = 0; *gapshould1 = 0; if ( !*plen ) return 0; buf_1 = 0i64; len = recv_message((int)this, fd, buf_1.magic, 16); if ( !(*(unsigned __int8 (__thiscall **)(struct_this_1 *, int, int *))(this->dword0 + 108))(this, len, plen) ) return 0; if ( len == 16 ) { if ( *(_DWORD *)buf_1.magic == 'cmas' ) // handshake len and magic check { flag = buf_1.flag; // a3 | 1 len_1 = buf_1.len; // len if ( (buf_1.flag & 1) == 0 && buf_1.len >= 0x20000u )// 握手包参数检查 return 0; v10 = this; if ( buf_1.len >= *(_DWORD *)this->gap10 ) return 0; used_size = buf_2->used_size; if ( used_size < buf_1.len ) { newsize = 0x1000; if ( buf_1.len > 0x1000u ) newsize = buf_1.len; reallocateMem(buf_2, newsize); v13 = buf_2->used_size; if ( !v13 ) return 0; flag = buf_1.flag; len_1 = buf_1.len; v10 = this; *plen = v13; used_size = buf_2->used_size; } if ( used_size ) buf = buf_2->pBuf; else buf = 0; *flag_1 = flag; *gapshould1 = buf_1.gap[1]; if ( len_1 > 0 ) { while ( 1 ) { v15 = recv_message((int)v10, fd, buf->m128i_i8, len_1 - v6); if ( v15 < 0 ) break; if ( !v15 ) goto LABEL_34; len_1 = buf_1.len; v6 += v15; v10 = this; buf = (__m128i *)((char *)buf + v15); if ( v6 >= buf_1.len ) { *plen = v6; return v6 != 0; } } if ( v15 == -2 ) v6 = -1; goto LABEL_23; } goto LABEL_34; } } else if ( len < 16 ) { goto LABEL_34; } v17 = buf_2->used_size; if ( v17 < 0x1000 ) { reallocateMem(buf_2, 0x1000u); v17 = buf_2->used_size; } if ( v17 ) pBuf = buf_2->pBuf; else pBuf = 0; memmove((unsigned int)pBuf, (unsigned int)&buf_1, len); v6 = len; recvlen = recv_message((int)this, fd, &pBuf->m128i_i8[len], *plen - len); if ( recvlen < 0 ) {LABEL_23: *plen = v6; return 0; } if ( recvlen ) v6 = recvlen + len;LABEL_34: *plen = v6; return v6 != 0;}


对Codemeter的逆向的感受有别于flexlm,最突出也是最蛋疼的就是codemeter运用了大量的虚函数,虚表。使得有很多程序逻辑不能直接了当的呈现出来,而是必须通过动态调试才能知悉。



因为这个send_cm_req_encrypt很长所以直接呈上分析结果,验证后发现func_10_send_message,func_6_recv_telegram的逻辑与wireshark抓包的结果相符,可以认为分析是正确的。


0x11 伪造服务端


通过调试可知encrypt_telegram,decrypt_telegram,expand_plaintext等加解密函数在服务端也被复用,因此我们可以通过对wibucm32.dll的魔改,导出这些重要的函数以减少工作量,不用复现那些加解密算法。

encrypt_telegramdecrypt_telegram

通过实践发现只需要这两个函数。


经过几天摸爬滚打,Fake server已经初见雏形。已经能够解密客户端发送的加密数据包了。


简单的记录一下过程中遇到的问题和发现。


1、让人摸不着头脑的错误


一开始通过GetProcAddress获得函数的地址进行访问,但总是离奇出错,出错原因各不相同。后来排查发现

typedef char(* Type_encrypt_telegram)(struct_communication*, char* buf, int* length, char flag);

这样编译出来的和这么编译出来的

typedef char(__thiscall* Type_encrypt_telegram)(struct_communication*, char* buf, int* length, char flag);


生成出的汇编代码不同,前一种报错而后一种则正常运行。也算是涨经验了。


2、客户端对服务端的请求格式

struct PKG{ char APICODE; FUNCTION_ARGS};//举个例子 对于 CODEMETER_API HCMSysEntry CMAPIENTRY CmAccess2(CMULONG flCtrl, CMACCESS2 *pcmAcc);//请求包的结构就是这样struct PKG_CMACCESS2{ char APICODE; CMULONG flCtrl; CMACCESS2 pcmAcc;};


举个例子

[*]Client 127.0.0.1:13794 login,time=63479375[*]Valid Handeshake[*]Recieved handshake package,length=737,magic=samc?[*]Encrypted package decrypt:SUCCESS,len:712[*]Function Request:CmAccess2[*][CmAccess2] flctrl=0x10000000,mflCtrl=0x00000100,firmcode=1000,productcode=114514,featurecode=1919810[*]Printing Decrypted Request:[*]-------------------------------------------PRINT HEX---------------------------------------------------[*]00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F[*]-------------------------------------------------------------------------------------------------------[*]64 00 00 00 00 00 00 10 00 01 00 00 ffffffe8 03 00 00[*]52 ffffffbf 01 00 42 4b 1d 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 65 64 61 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 fffffff8 45 00 00 3d ffffffc0 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 73 68 65 6e 77 65 6e 68[*]75 61 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[*]00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00[*]00 00 00 00 45 44 41 00 00 00 00 00 00 00 00 00[*]00 00 00 00 ffffffd4 12 1e 07[*]----------------------------------------------END------------------------------------------------------[*]Client 127.0.0.1:13794 out


之后需要做的就是:

① 完成服务器对客户端的回复这一行为的函数。
② 优先实现cmaccess cmaccess2 cmrelease等函数的模拟。
③ 实现其他函数。
④ 做个GUI。


附件(点击文末阅读原文在原帖获取)为更新过的各个API所对应的API Code。





看雪ID:ericyudatou

https://bbs.kanxue.com/user-home-786730.htm

*本文由看雪论坛 ericyudatou 原创,转载请注明来自看雪社区



# 往期推荐

1、Windows_AFD_LPE_CVE-2023-21768分析

2、x64dbg插件编写基础

3、设备登记APP趣味破解

4、移动安全——从零开始root

5、基于树莓派的蓝牙调试环境搭建

6、从应用层到MCU看Windows处理键盘输入



球分享

球点赞

球在看


点击“阅读原文”,了解更多!

文章有问题?点此查看未经处理的缓存