目标程序:editplus 5.0分析工具:
1.ollyDbg
2.IDAPro
知识储备:
1.应用程序的编写流程.
2.ASM汇编语言的无障碍阅读.
3.开发语言,如:C++、C、python等.
目的:用C语言写editplus-5.0的注册机。思路:
1.注册机是什么,有哪些功能,如何编写注册机?
答:
功能:输入用户名后经过计算输出注册码.
函数:需要一个关键的计算函数.
2.如何实现注册机的计算函数?
答:
计算函数可视为editplus的验证注册码函数的反向实现.
那么实现该计算函数的步骤如下:
1.定位关键验证函数.
2.分析关键验证函数.
3.复现关键验证函数.
4.反向复现计算函数.
步骤:
整理思路可得如下图:
步骤如下:
1.对api下断,经过堆栈回溯定位关键函数.
2.分析关键函数的内部功能实现和逻辑.
3.根据分析内容编写验证函数和计算函数.
1.定位函数
1.1 定位
一般注册验证的流程如下,从程序的报错信息可以猜测为程序调用了MessageBox类函数,对该函数下断后堆栈回溯到CheckInfo()函数位置处.
if(CheckInfo()) MessageBox("succeed"); else MessageBox("failed");
1.2 验证
上述的if...else分支流程在汇编层会像如下的伪代码所述,那么如果将jnz指令修改为jz或je而导致注册成功,那么CheckInfo函数就是我们所需分析的验证函数.
call CheckInfo cmp eax,edx //edx ==0 jnz loc_succeed
对于editplus的分析得知对于注册码存在二次校验,校验失败如下图所示:
PS:分析时可以在IDA中导入sig文件、导出map文件有助于动态调式下的分析.
2.分析函数
一般程序在编写的时候至少会遵循功能和逻辑相分离的规范,或者说是函数封装的利用.对于函数应当按照功能和逻辑来拆分,拆分完毕后在对代码逐一分析.总而言之就是先分析整体汇编逻辑后写好C语言函数的逻辑,然后把汇编代码块还原到C语言函数的具体实现中.
个人感觉汇编代码的阅读分三种情况:
1.甲骨文翻译.
2.英语阅读理解.
3.看金庸小说.
CheckInfo函数拆解如下:
2.1 函数逻辑:
控制用户名和注册码的输入,在动态分析时有助于代码的分析(Name:Weaving Code:00000-00000-00000-00000-00000),保证用户名不变的情况下根据分析结果去修改Code.可以手动得出一条注册码:(注册码格式分析代码可得)
//注册机既是将0释放为任意数字 Name:Weaving Code:0EYYU-00000-00000-00000-00000
函数大致逻辑如下:
#includebool CheckInfo(); bool CheckReg_001(char* pUserName, char* pURegCode); char DeCodeChar(char ch); bool Check(char* pUserName, char* pDRegCode); int SprintfChars(char* pUserName,int len,char* pCUserName,int nNum); WORD SearchTable(int nNum ,char* pCUserName,int len); char* FormatStr(char* str,WORD sNum); bool CheckReg_002(); unsigned short* CreateTable(); unsigned short* pTable = {}; int _tmain(int argc, _TCHAR* argv[]) { if (CheckInfo()) MessageBoxA(0, 0, "Succeed", 0); else MessageBoxA(0, 0, "failed", 0); return 0; } bool CheckInfo() { //xor eax,esp; char UserName[] = "Weaving"; //注册码格式有代码分析得到 char RegCode[] = "xxxxx-xxxxx-xxxxx-xxxxx-xxxxx"; //小写最后会转换成大写去计算 CharUpperA(RegCode); if (!CheckReg_001(UserName, RegCode)) return false; //和主要验证算法无关 if (!CheckReg_002()) return false; char* pUserName = (char*)malloc(sizeof(char) * 7); char* pRegCode = (char*)malloc(sizeof(char) * 29); char* pDRegCode = (char*)malloc(sizeof(char) * 29); //二次校验时会使用到 _memccpy(pUserName, UserName, 0x1F4,7); _memccpy(pRegCode, RegCode, 0x32, 29); _memccpy(pDRegCode,RegCode, 0x32, 29); for (int i = 0; i < 29; i++) { if ((i + 1) % 6 != 0) { char ret = DeCodeChar(pDRegCode[i]); ret >>= 1; if (ret >= 10) pDRegCode[i] = ret + 55; else pDRegCode[i] = ret + 48; } } //checkEsp(); return true; } bool CheckReg_001(char* pUserName, char* pURegCode) { char *pDeReg = (char*)malloc(sizeof(char) * 50); memcpy_s(pDeReg, 50, pURegCode,50); //由此循环猜测注册码格式如下 //长度:0x1D==29; //格式:xxxxx-xxxxx-xxxxx-xxxxx-xxxxx //内容: X <== 数字or字母. //不变: 6、12、18、24位内容不变,故而假设为“-”字符. for (int i = 0; i < 29; i++) { if ((i + 1) % 6!=0) { char ret = DeCodeChar(pDeReg[i]); ret >>= 1; if (ret >= 10) pDeReg[i] = ret + 55; else pDeReg[i] = ret + 48; } } return Check(pUserName, pDeReg); } bool Check(char* pUserName, char* pDRegCode) { bool ret = false; int ulen = strlen(pUserName); int rlen = strlen(pDRegCode); pTable = CreateTable(); char* pAscii = (char*)malloc(sizeof(char) * 3000); int slen = SprintfChars(pUserName,ulen,pAscii,3000); WORD sNum = SearchTable(0, pAscii, slen); char* pCNum = (char*)malloc(sizeof(char) * 10); FormatStr(pCNum, sNum); if ((*(pDRegCode + 2) == pCNum[0])&&(*(pDRegCode+3))==pCNum[1]) { slen = SprintfChars(pDRegCode+2,rlen-2,pAscii,3000); sNum = SearchTable(0,pAscii,slen); FormatStr(pCNum, sNum); if ((*(pDRegCode) == pCNum[0]) && (*(pDRegCode + 1)) == pCNum[1]) ret = true; } return ret; }
2.2 函数实现:具体有CreateTable、DeCodeChar、SprintfChars、SearchTable、FormatStr这5个函数外加一个SecCheckInfo.
unsigned short* CreateTable() { unsigned short * pTable = (unsigned short *)malloc(0x200); memset(pTable, 0, 0x200); for (int i = 0; i < 0x100; i++) { int j = 1; unsigned short eax = 0xC0C1; while (j < 0x100) { //test edx,ecx; if (i&j) pTable[i] ^= eax; eax += eax; eax ^= 0x4003; j += j; } } return pTable; } char DeCodeChar(char ch) { //except:I/O char chArry[] = { 50, 51, 52, 53,// 2,3,4,5, 54, 55, 56, 57,// 6,7,8,9, 65, 66, 67, 68,// A,B,C,D, 69 ,70 ,71, 72,// E,F,G,H, 74 ,75 ,76 ,77,// J,K,L,M, 78 ,80 ,81 ,82,// N,P,Q,R, 83 ,84 ,85 ,86,// S,T,U,V, 87 ,88 ,89 ,90 // W,X,Y,Z. }; char ret = 0; int v1 = 2; char* p = &chArry[1]; while (v1 - 2 < 32) { if (ch == *(p - 1)) ret = v1 - 2; if (ch == *(p)) ret = v1 - 1; if (ch == *(p + 1)) ret = v1; if (ch == *(p + 2)) ret = v1 + 1; p += 4; v1 += 4; } return ret; } int SprintfChars(char* pUserName, int len, char* pCUserName, int nNum) { int ret; int i = 0; int j = 0; if (len<=0) { pCUserName = NULL; ret = 0; } else { while (j> 8); } } return ret; } char* FormatStr(char* str,WORD sNum) { sprintf_s(str, 10,"X", sNum); return str; }
对于二次验证只是对于经过DeCodeChar转换的注册码的第五位的校验.
char GetDReg05Code(char* pUserName) { char ch5 = {}; int nNum01 = 0; int nNum02 = 0; int nNum03 = 1; int i = 0; while (i < ulen - 1) { nNum01 += pUserName[i]; nNum02 += pUserName[i + 1]; i += 2; } if (i < ulen) nNum03 = pUserName[i] + 1; sprintf_s(&ch5, 2, "X", ((9 * (nNum01 + nNum02 + nNum03) + 0xA) / 3 + 36) % 16); return ch5; } bool SecCheckInfo() { //DRegCode:转换后的注册码 if(GetDReg05Code(UserName)==DRegCode[4]) return true; else return false; }
PS:对于和目的无关的函数可以直接略过,如:写入注册表的CheckReg_002函数、检查堆栈是否被破环的函数末尾的CheckEsp函数等.
3.编写函数
3.1验证函数:验证的算法可以由上分析整合可以得出。
3.2计算函数:KeyGen源码:
有效注册码示例:
KanXue AJQ4W-53QE4-UUI1Z-TI6S9-7ZKWY Weaving SGYYU-SUIYK-HTPFP-N7Q30-ROE94
PS:先随机生成29位注册码,通过用户名计算出3-4-5位置的内容并替换调注册码相应位置,在通过新的注册码计算得出1-2位置内容即可.
总结:
注册机的分析+编写流程,首先是在对于能够造成当前的窗口界面(MessageBox)或是将要进行的操作(leftClick)的背后所对应的API函数的下断,然后再复现情景的时候在通过断点加上堆栈回溯基本可以确定验证函数的所在位置.在确定位置后注册机的编写分大致就可以划分为三个步骤:
1.确认验证函数:
修改函数返回值或是修改JCC指令导致流程的改变,判断是否为关键Call.
2.分析验证函数:
通过自定义的输入数据加上函数分析,得出固定用户名对应的注册码.
3.编写注册函数:
通过对于验证函数的分析编写注册函数,得出随机用户名对应的注册码.
PS:此类工具一般不会对程序进行加壳或是混淆的操作,用来打发时间最适合不过~~