本文使用调试API为CrackMe写了一个显示密码的程序。在编写关于CrackMe的密码显示程序之前,需要准备两个任务。第一个任务是知道在哪里合理设置断点,第二个任务是在哪里读取密码。带着这两个问题再想想。这里的程序中,需要比较两个字符串,比较函数是strcmp(),它有两个参数,分别是输入密码和真实密码。即在调用strcmp()函数的位置设置断点,通过查看其参数可以得到正确的密码。在调用strcmp()函数的位置设置INT3断点,即向该地址写入0xCC机器码。使用OD查看调用strcmp()函数的地址,如图1所示。图1调用strcmp()函数的地址从图1可以看出,调用strcmp()函数的地址为00401E9E。有了这个地址,只要找到函数的两个参数,就可以找到错误的密码和正确的密码。从图1可以看出,EDX中存放的是正确密码的起始地址,ECX中存放的是错误密码的起始地址。只要在地址00401E9E下断点,通过线程环境读取EDX和ECX寄存器值,就可以得到两个密码的起始地址。准备工作已经做好了,我们来写一个控制台程序吧。首先定义两个常量,一个是用来设置断点的地址,另一个是INT3指令的机器码。定义如下://INT3需要设置断点的位置#defineBP_VA0x00401E9E//INT3的机器码constBYTEbInt3='\xCC';将CrackMe的文件路径和文件名作为参数传递给显示密码的程序。显示的程序首先要创建一个调试模式的CrackMe,代码如下://启动信息STARTUPINFOsi={0};si.cb=sizeof(STARTUPINFO);GetStartupInfo(&si);//进程信息PROCESS_INFORMATIONpi={0};//创建被调试进程BOOLbRet=CreateProcess(pszFileName,NULL,NULL,NULL,FALSE,DEBUG_PROCESS|DEBUG_ONLY_THIS_PROCESS,NULL,NULL,&si,&pi);if(bRet==FALSE){printf("CreateProcessError\r\n");return-1;}然后进入调试循环,处理两个调试事件,一个是CREATE_PROCESS_DEBUG_EVENT,一个是EXCEPTION_DEBUG_EVENT下的EXCEPTION_BREAKPOINT。处理CREATE_PROCESS_DEBUG_EVENT的代码如下://创建进程时的调试事件caseCREATE_PROCESS_DEBUG_EVENT:{//读取要设置INT3断点的机器码//方便后面恢复ReadProcessMemory(pi.hProcess,(LPVOID)BP_VA,(LPVOID)&bOldByte,sizeof(BYTE),&dwReadWriteNum);//将INT3的机器码0xCC写入断点WriteProcessMemory(pi.hProcess,(LPVOID)BP_VA,(LPVOID)&bInt3,sizeof(BYTE),&dwReadWriteNum);break;}在CREATE_PROCESS_DEBUG_EVENT中,在调用strcmp()函数的地址设置一个INT3断点,然后在此处写入0xCC时读取原机器码。使用ReadProcessMemory()读取原始机器码,使用WriteProcessMemory()写入INT3的机器码。读取原机器码的作用是当写入的0xCC被中断时,需要将原机器码写回,程序才能继续正确运行。我们来看看EXCEPTION_DEBUG_EVENT下的EXCEPTION_BREAKPOINT是如何处理的。代码如下://异常发生时的调试事件caseEXCEPTION_DEBUG_EVENT:{//判断异常类型异常caseEXCEPTION_BREAKPOINT:{//获取线程环境context.ContextFlags=CONTEXT_FULL;GetThreadContext(pi.hThread,&context);//在设置的断点判断是否被破解if((BP_VA+1)==context.Eip){//读取正确的密码ReadProcessMemory(pi.hProcess,(LPVOID)context.Edx,(LPVOID)pszPassword,MAXBYTE,&dwReadWriteNum);//读错密码ReadProcessMemory(pi.hProcess,(LPVOID)context.Ecx,(LPVOID)pszErrorPass,MAXBYTE,&dwReadWriteNum);printf("您输入的密码是:%s\r\n",pszErrorPass);printf("正确的密码是:%s\r\n",pszPassword);//指令执行到INT3被中断//INT3的机器指令长度为1字节//所以需要减1EIP改正EIP//EIP是指令指针寄存器//它存放的是下一条要执行的指令的地址上下文。eip--;//修正原地址的机器码WriteProcessMemory(pi.hProcess,(LPVOID)BP_VA,(LPVOID)&bOldByte,sizeof(BYTE),&dwReadWriteNum);//设置当前线程环境SetThreadContext(pi.hThread,&context);}break;}}}调试事件的处理应该放在调试循环中。上面代码展示了调试事件的处理,我们看一下调试循环的大致代码:while(TRUE){//获取调试事件WaitForDebugEvent(&de,INFINITE);//判断事件类型switch(de.dwDebugEventCode){//创建进程时的调试事件caseCREATE_PROCESS_DEBUG_EVENT:{break;}//产生异常时的调试事件caseEXCEPTION_DEBUG_EVENT:{//判断异常类型switch(de.u.Exception.ExceptionRecord.ExceptionCode){//INT3类型的异常caseEXCEPTION_BREAKPOINT:{}break;}}}ContinueDebugEvent(de.dwProcessId,de.dwThreadId,DBG_CONTINUE);}只要把调试事件的处理方法放到调试循环中,程序就完成了。接下来编译连接,然后将CrackMe直接拖放到密码显示程序上。该程序启动CrackMe进程并等待用户输入。输入帐号和密码后,点击“确定”按钮,程序会显示正确的密码和用户输入的密码,如图2所示。图2根据结果验证密码是否正确如图2所示,可以看出获取到的密码是正确的。程序到此结束。可以通过附上调试过程,将程序改成显示密码,巩固所学知识。
