-
-
[原创]ACprotect 1.? 脱壳 + 写x64dbg插件
-
发表于: 2021-4-9 02:08 7614
-
目标程序https://bbs.pediy.com/thread-30330.htm
平台win7
用ExeinfoPe查壳,显示为AC protect 1.?
用x64dbg运行,调试器直接就退出了,显然有反调试,开始分析
反调试
x64dbg先设置异常,如下图 (注:新版本,不同的异常,可以单独设置)
Unkonwn exceptions可以理解为,除了已经设置过的异常,另外的其他异常
如果没有单独设置异常,Unkonwn exceptions表示的是全部(00000000~FFFFFFFF)
所以下图的意思是,全部异常(Unknown exceptions),都不忽略(暂停于第一次机会),都自己处理(异常处理者调试器)
既然退出了,先在退出相关的api下断点
所有模块中搜索,带exit的全部F2下断点,然后运行
停在RtlReportSilentProcessExit上,此时看栈的返回地址
760FE5A1转到反汇编,是TerminateProcess调用的RtlReportSilentProcessExit
00649E20跟进,调用了TerminateProcess
0064D3D8跟进,call上面有个比较,等于0,就可以跳过调用TerminateProcess的函数
能看出是CreateToolhelp32Snapshot的地址,此处只是当用于判断的标志(实际是遍历进程,对比父进程ID)
0064D3C6下硬件断点,重新运行,停下时,把值清0或修改jcc标志,继续F9运行
此时,会停在异常,栈中能看出有seh链,跟进函数
①处,eip+1 ②处,删除硬件断点
这个异常自己处理,把0064B396设置为新的运行点(其实也可以直接F9)
如果用的不是x64dbg的新版本,就在回调函数中的jcc下F2断点,运行,双击修改jcc标志
注:这个803异常,不一定能遇到,时有时无
继续F9运行
遇到异常,seh跟进,只有eip+2
把0064B090设置为新的运行点,继续F9运行
此时会遇到异常(注:随机2种情况:栈中没有返回地址 或 栈中有返回地址)
1.栈中没有返回地址
代码向上翻,找能跳过eip的jcc,找到后下硬件断点,可以多下几个断点
注:可能有不少都满足条件,但是要找比较靠上的(低地址的)
还有3个硬件断点可用,所以我下了3个地址能跳过的
①处,翻找的过程中,已经能发现其他的反调试方式了,先不理会
下好断点后,重新运行,重复最初的步骤,直到停在断点0064B599,按F9继续执行
只要还停在0064B599上,就继续F9,此处的断点不重要,因为上面的call是取随机数的
为了方便和腾出一个硬件断点,直接删除断点,F9运行
还会遇到之前说的2种情况的异常(栈中没有返回地址 或 栈中有返回地址)
栈中没有返回地址(此时停在0064B5BB上,如果再F9运行的话,就是没返回地址的)
eax不等于0时,会跳走,双击修改zf标志为1 (此处的代码,下面会总结)
再F9运行,就会变成 栈中有返回地址 的情况
2.栈中有返回地址
跟进第一个返回地址0064B949,向上找能跳过0064B949地址的jcc
找到2个,0064B87E上面的call是取随机数,所以这个jcc忽略
①处,看到调用了IsDebuggerPresent,所以在0064B893下硬件断点
注:②处,其实还有别的情况,只不过极少能遇到(写笔记时,一直没复现出来)
上面下好断点后,重新运行程序,重复步骤到断点上
遇到0064B5BB和0064B893时,就改跳转的标志,然后F9运行
之后有可能再次遇到803异常,按之前的方式处理,再F9运行
遇到异常,此时选项里设置异常
不暂停(忽略所有异常)
异常处理者为被调试对象(异常处理交给程序)
F9运行,程序就能正常跑起来了
小总结:
壳的反调试主要2个部分
1.用CreateToolhelp32Snapshot遍历进程,对比父进程ID
2.用IsDebuggerPresent拿Flags 和 壳自己拿peb中的Flags
IsDebuggerPresent
windbg查看peb
反调试部分结束
为了方便之后的调试,我会给x64dbg写个插件,自动处理这2种反调试
写插件的部分,最后再写,先写脱壳的部分,下面的内容,默认已经没有反调试了
获取IAT
重新加载程序
在GetProcAddress下硬件断点
停下后,注意eax有残留的函数名称信息
点击运行到用户代码
到程序代码后
1 2 3 4 5 6 7 8 | 00648E0E | FF | call dword ptr ss:[ebp + 41C268 ] |;GetProcAddress 00648E14 | E9 | jmp calories. 648EC6 |;eax = = api地址 ... 00648EC6 | 5A | pop edx |;jmp到这 00648EC7 | 89 | mov dword ptr ss:[ebp + edx],eax |;eax存入某内存 / / 很像iat,实际是壳使用的api 00648ECB | 58 | pop eax | 00648ECC | 5B | pop ebx | 00648ECD | C3 | ret |;函数返回 |
函数返回后
看到一些相同call,给eax和edx值,然后call
1 2 3 4 5 6 7 8 9 10 11 12 | 0064C091 | B8 | mov eax,calories. 404372 | 0064C096 | BA | mov edx,calories. 4044E1 | 0064C09B | E8 | call calories. 648DF6 | 0064C0A0 | B8 | mov eax,calories. 40437E | 0064C0A5 | BA | mov edx,calories. 4044E5 | 0064C0AA | E8 | call calories. 648DF6 | ... 0064C1AE | B8 | mov eax,calories. 404581 | 0064C1B3 | BA | mov edx,calories. 40457D | 0064C1B8 | E8 | call calories. 648DF6 | 0064C1BD | 83 | cmp dword ptr ss:[ebp + 4020F1 ], 0 | 0064C1C4 | 74 | je calories. 64C1EA | |
进call看看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 00648DF6 | 53 | push ebx | 00648DF7 | 50 | push eax | 00648DF8 | 52 | push edx | 00648DF9 | 03 | add eax,ebp |;算出api名称的地址 00648DFB | 50 | push eax | 00648DFC | 53 | push ebx | 00648DFD | 50 | push eax | 00648DFE | 8B | mov eax,dword ptr ss:[ebp + 41C268 ] | 00648E04 | 80 | cmp byte ptr ds:[eax],CC |;检测CC断点 00648E07 | 74 | je calories. 648E19 | 00648E09 | 90 | nop | 00648E0A | 90 | nop | 00648E0B | 90 | nop | 00648E0C | 90 | nop | 00648E0D | 58 | pop eax |;api名称弹到eax中,所以eax有残留 00648E0E | FF | call dword ptr ss:[ebp + 41C268 ] |;GetProcAddress 下面的之前分析过了 00648E14 | E9 | jmp calories. 648EC6 | ... 00648EC6 | 5A | pop edx | 00648EC7 | 89 | mov dword ptr ss:[ebp + edx],eax | 00648ECB | 58 | pop eax | 00648ECC | 5B | pop ebx | 00648ECD | C3 | ret | |
不停的F9,观察eax
当eax不再是api名时,表示已经是另一段代码调用了GetProcAddress
ebx是api名了,返回到程序代码,再观察
返回后
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | 0064E319 | FF | call dword ptr ss:[ebp + 41C268 ] |;GetProcAddress 0064E31F | 3B | cmp ebx,dword ptr ss:[ebp + 404028 ] |;观察返回值eax去了哪里 0064E325 | 7C | jl calories. 64E336 | 0064E327 | 90 | nop | 0064E328 | 90 | nop | 0064E329 | 90 | nop | 0064E32A | 90 | nop | 0064E32B | 60 | pushad |;保存环境 0064E32C | 2B | sub eax,eax | 0064E32E | 88 | mov byte ptr ds:[ebx],al | 0064E330 | 43 | inc ebx | 0064E331 | 38 | cmp byte ptr ds:[ebx],al | 0064E333 | 75 | jne calories. 64E32E | 0064E335 | 61 | popad |;恢复环境 0064E336 | 0B | or eax,eax | 0064E338 | 0F | je calories. 64E26C |;检查返回值eax是否为空 0064E33E | 3B | cmp eax,dword ptr ss:[ebp + 41C278 ] |;不知道在对比什么,不理会,看eax去向 0064E344 | 75 | jne calories. 64E350 |;跳走 ... 0064E350 | 56 | push esi |;到这 0064E351 | FF | push dword ptr ss:[ebp + 404020 ] | 0064E357 | 5E | pop esi | 0064E358 | 39 | cmp dword ptr ss:[ebp + 4020E9 ],esi | 0064E35E | 74 | je calories. 64E375 |;跳走 ... 0064E375 | 80 | cmp byte ptr ss:[ebp + 40A387 ], 0 |;到这 0064E37C | 74 | je calories. 64E3D2 | 0064E37E | 90 | nop | 0064E37F | 90 | nop | 0064E380 | 90 | nop | 0064E381 | 90 | nop | 0064E382 | EB | jmp calories. 64E38B |;跳走 ... 0064E38B | 8B | mov esi,dword ptr ss:[ebp + 4040ED ] |;到这 0064E391 | 83 | add esi,D | 0064E394 | 81 | sub esi,calories. 401FC7 | 0064E39A | 2B | sub esi,ebp | 0064E39C | 83 | cmp esi, 0 | 0064E39F | 7F | jg calories. 64E3D2 | 0064E3A1 | 90 | nop | 0064E3A2 | 90 | nop | 0064E3A3 | 90 | nop | 0064E3A4 | 90 | nop | 0064E3A5 | 8B | mov esi,dword ptr ss:[ebp + 4040ED ] | 0064E3AB | 53 | push ebx |;保存环境 0064E3AC | 50 | push eax |;保存环境 0064E3AD | 0F | rdtsc |;取 "随机值" ,ebx和eax被改变 0064E3AF | 8B | mov ebx,eax | 0064E3B1 | 58 | pop eax |;恢复环境 eax = api地址 0064E3B2 | 33 | xor eax,ebx |;异或加密 api地址 0064E3B4 | C6 | mov byte ptr ds:[esi], 68 |;写机器码push 0064E3B7 | 89 | mov dword ptr ds:[esi + 1 ],eax |;加密后的 api地址写机器码 0064E3BA | C7 | mov dword ptr ds:[esi + 5 ], 243481 | 0064E3C1 | 89 | mov dword ptr ds:[esi + 8 ],ebx |;写解密的值 0064E3C4 | C6 | mov byte ptr ds:[esi + C],C3 |;写机器码 ret 0064E3C8 | 5B | pop ebx |;恢复环境 0064E3C9 | 8B | mov eax,esi |;eax = esi / / esi是刚刚写的代码地址 0064E3CB | 83 | add dword ptr ss:[ebp + 4040ED ],D | 0064E3D2 | 5E | pop esi | 0064E3D3 | 89 | mov dword ptr ds:[edi],eax |;eax写入iat 0064E3D5 | 83 | add dword ptr ss:[ebp + 404024 ], 4 | |
写入的代码,如图,可以看出是利用了ep处的空间
解密出api地址,通过ret跳到api
看iat的内存,转成地址类型查看,能看出被替换成了上面的函数
从上面的分析中,就可以获取真正的iat了,在0064E3AB下硬件断点,重新运行程序,停在这个断点
把0064E3AB到0064E3C9的代码,nop掉,eax中的api地址,就会直接写入edi的iat地址
获取IAT的部分结束
要dump的时候,按上面的方法处理一下就可以了
寻找oep
开始找oep,重载程序,esp定律,F9
停在
1 2 3 4 5 6 7 8 9 | 0065FAF2 | 61 | popad | 0065FAF3 | 55 | push ebp |;被偷的oep代码 0065FAF4 | 8B | mov ebp,esp |;被偷的oep代码 0065FAF6 | 90 | nop | 0065FAF7 | 90 | nop | 0065FAF8 | 90 | nop | 0065FAF9 | 60 | pushad |;执行完这一行,再esp定律,F9 0065FAFA | 60 | pushad | 0065FAFB | E8 | call calories. 65FB00 | |
停下
1 2 3 4 | 0065FE8E | 61 | popad | 0065FE8F | 60 | pushad |;eip停在这,popad和pushad抵消,继续F9 0065FE90 | E8 | call calories. 65FE95 | 0065FE95 | 5E | pop esi | |
停下
1 2 3 4 5 6 7 8 9 10 | 0065FECB | EB | jmp calories. 65FECE |;跳走 0065FECD | E9 | jmp FF3A24D1 | ... 0065FECE | FF | jmp dword ptr ds:[ 65FED4 ] |;到这,再跳走 .. 0056949B | B9 | mov ecx, 4 |;到达oep 005694A0 | 6A | push 0 | 005694A2 | 6A | push 0 | 005694A4 | 49 | dec ecx | 005694A5 | 75 | jne calories. 5694A0 | |
寻找oep的部分结束
记录好信息,dump时使用
Dump
梳理一下现在的情况
反调试,已经通过插件处理
iat处理的地址和方法已经确定
oep的地址和被偷的oep代码也有了
重载程序
0064E3AB下硬件断点,停下时nop代码,获取iat
0056949B下硬件断点,停下时,恢复oep(0056949B前面修改机器码)
先设置00569498为新的eip,再dump,这样不用之后再修正PE格式中的入口点
检查oep是否正确,dump文件
IAT Autosearch获取iat的va和size
Get Imports获取iat,有2条无效的,直接删除就行了
fix dump修复刚刚dump出的文件
Dump的部分结束
修复Code Splicing
dump出的程序,运行出错,载入看看(异常设置为,全部都不忽略,自己处理)
F9运行,停在下图的情况,开始分析
到原程序(未脱壳的)的006461D9看看,如下图
其实就是把代码放到了申请的内存上,dump时会少一部分代码,所以运行出错
观察代码,非常有规律,下面会分析
来源https://bbs.pediy.com/thread-17253.htm
开始修复
在申请的内存上转到内存布局中
把这块内存转存成文件
用CFF_Explorer打开原来dump出来的程序(修复后的Calories_dump_SCY)
把转存出来的内存,加到文件中
内存属性修改成相同的
分析规律
原程序中
1 2 3 4 5 6 7 | 006460F5 | FF25 28E37400 | jmp dword ptr ds:[ 74E328 ] |;开始 006460FB | FF25 2CE37400 | jmp dword ptr ds:[ 74E32C ] | ... 00647859 | FF25 C0F27400 | jmp dword ptr ds:[ 74F2C0 ] | 0064785F | FF25 C4F27400 | jmp dword ptr ds:[ 74F2C4 ] |;结束 0064785F - 006460F5 = 176A / / 尾 减 头 |
1 2 3 | 176Ah = = 5994d 5994 + 6 = 6000 / / 6 是最后一条指令的机器码字节数 6000 / 6 = 1000 / / 6 是每一条指令的机器码字节数 / / 共 1000 条jmp |
1 2 3 4 5 6 | 头 006460F5 跳过去的地址 0074F2C8 | 8B7E 0C | mov edi,dword ptr ds:[esi + C] | 尾 0064785F 跳过去的地址 00750A32 | 74 74 | je 750AA8 | 00750A32 - 0074F2C8 = 176A / / 尾 减 头 |
观察总结
都是连续的
步长都是固定的
一一对应的
结论:每个jmp跳到目标地址的偏移,都是固定的
计算偏移值,找到dump程序中jmp对应的地址
从原程序中的这些jmp中,找一条分析一下对应的地址
1 2 3 4 5 6 7 8 9 10 11 | 006460FB | FF25 2CE37400 | jmp dword ptr ds:[ 74E32C ] |;[ 0074E32C ] = 0074F2CE 006460FB 的jmp,会跳到申请内存中的 0074F2CE 处 申请内存的信息 地址 = 00720000 ;基址 大小 = 00085000 分配类型 = PRV 当前保护 = - RW - - 初始保护 = - RW - - 0074F2CE - 00720000 = 2F2CE |
到dump出的程序中,看后来添加的节的信息
1 2 3 4 5 6 7 8 | 地址 = 0066A000 ;基址 大小 = 00085000 页面信息 = "12345678" 分配类型 = IMG 当前保护 = ERWC - 初始保护 = ERWC - 0066A000 + 2F2CE = 6992CE |
修改dump程序,获取机器码
原本的指令是间接jmp [xxxxxxxx]
修改成直接jmp xxxxxxxx
得到机器码 E9 CE 31 05 00 90 可以去修改文件了
va转fa (文件要修改的地址)
写程序修改文件,毕竟量太多了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | #include <iostream> #include <Windows.h> int main() { HANDLE hFile = CreateFile( L "C:\\Users\\win7\\Desktop\\ceshi.exe" , GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); SetFilePointer(hFile, 0x2072F5 , NULL, FILE_BEGIN); unsigned char buf[ 6 ] = { 0xE9 , 0xCE , 0x31 , 0x05 , 0x00 , 0x90 }; DWORD retBytes; for ( int i = 0 ; i < 1000 ; i + + ) { WriteFile(hFile, buf, 6 , &retBytes, NULL); } CloseHandle(hFile); return 0 ; } |
修正Code Splicing后
程序运行成功,查壳为Delphi
脱壳部分结束
写x64dbg插件
要过掉2个地方
1.遍历进程 CreateToolhelp32Snapshot Process32First Process32Next
2.PEB的调试标志
看看OD插件HideOD是怎么做的,跟着学就行了
HookApi在Process32NextW入口写
xor eax,eax
ret 8
直接让函数返回0
段选择子拿线性地址
teb地址+30h拿peb,再+2,BeingDebugged清0
注:x64dbg提供了这种功能,不用自己拼gdt
方法有了,开始写插件
https://help.x64dbg.com/ 手册
x64dbg里面有pluginsdk
下载模板https://github.com/x64dbg/PluginTemplate
vs新建dll工程,添加模板中的文件到工程中
plugin.h
plugin.cpp
pluginmain.h
pluginmain.cpp
pluginconfig.h //pluginconfig.h.in这个文件改的
设置vs
预编译头改为不使用
包含目录和库目录,添加x64dbg的目录 如E:\x64dbg
目标文件扩展名改为.dp32或.dp64
输出目录改为插件目录 如E:\x64dbg\release\x32\plugins\
调试的命令改为x64dbg的程序 如E:\x64dbg\release\x32\x32dbg.exe
代码如下
1 2 3 4 5 | pluginconfig.h #define PLUGIN_NAME u8"-------- 插件名称 --------" //要用utf-8 #ifndef PLUGIN_NAME #error PLUGIN_NAME not defined #endif // PLUGIN_NAME |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | plugin.cpp #include "plugin.h" enum { CMD_1 = 1 , CMD_2 }; void MyCB1(CBTYPE cbType, void * callbackInfo); void MyCB2(CBTYPE cbType, void * callbackInfo); int Process32NextW_Zero(LPVOID DllBase); / / Initialize your plugin data here. bool pluginInit(PLUG_INITSTRUCT * initStruct) { return true; / / Return false to cancel loading the plugin. } / / Deinitialize your plugin data here. void pluginStop() { } / / Do GUI / Menu related things here. void pluginSetup() { / / 添加菜单 _plugin_menuaddentry(hMenu, CMD_1, u8 "功能1" ); _plugin_menuaddentry(hMenu, CMD_2, u8 "功能2" ); / / 注册回调 _plugin_registercallback(pluginHandle, CB_LOADDLL, MyCB1); _plugin_registercallback(pluginHandle, CB_CREATEPROCESS, MyCB1); _plugin_registercallback(pluginHandle, CB_MENUENTRY, MyCB2); } void MyCB1(CBTYPE cbType, void * callbackInfo) { switch (cbType) { case CB_LOADDLL: { PLUG_CB_LOADDLL * pTemp = (PLUG_CB_LOADDLL * )callbackInfo; if (strcmp(pTemp - >modname, "kernel32.dll" ) = = 0 ) { int Ret = Process32NextW_Zero(pTemp - >LoadDll - >lpBaseOfDll); if (Ret = = 0 ) { dprintf( "Error:Process32NextW_Zero\n" ); } } break ; } case CB_CREATEPROCESS: { Cmd( "HideDebugger" ); break ; } } } int Process32NextW_Zero(LPVOID DllBase) { FARPROC pAddr = GetProcAddress((HMODULE)DllBase, "Process32NextW" ); if (pAddr = = NULL) { dprintf( "Error:GetProcAddress\n" ); return 0 ; } unsigned char buf[ 5 ] = { 0x33 , 0xC0 , 0xC2 , 0x08 , 0x00 }; bool bRet = DbgMemWrite((duint) * pAddr, buf, sizeof(buf)); if (bRet = = false) { dprintf( "Error:DbgMemWrite\n" ); return 0 ; } return 1 ; } void MyCB2(CBTYPE cbType, void * callbackInfo) { switch (cbType) { case CB_MENUENTRY: { PLUG_CB_MENUENTRY * pTemp = (PLUG_CB_MENUENTRY * )callbackInfo; switch (pTemp - >hEntry) { case CMD_1: MessageBox(NULL, TEXT( "CMD_1" ), NULL, NULL); break ; case CMD_2: MessageBox(NULL, TEXT( "CMD_2" ), NULL, NULL); break ; } break ; } } } |
完结啦
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课