JS逆向反调试和反反调试
写在最前面
本文转载于CSDN大佬: @花爷
已获得其本人允许
根据《中华人民共和国著作权法》规定
1、已在报刊上刊登或者网络上传播的作品,除著作权人声明或者上载该作品的网络服务提供者受著作权人的委托声明不得转载、摘编的以外,网站予以转载、摘编,并按有关规定支付报酬、注明出处的,不构成侵权。
2、为个人学习、研究或者欣赏,使用他人已经发表的作品而转载的不属于侵权,可以不经著作权人许可,不向其支付报酬,但应当指明作者姓名、作品名称,并且不得侵犯著作权人依照本法享有的其他权利。
切入JS逆向反调试
现在调试JS各种反调,既然有反调,那我们就肯定有过这个反调试的方法。这里给大家推荐一个JS逆向练习平台。
1.我们这里讲的是第5题,它这里的反调试是不让你打开开发者人员工具。打开开发者人员工具,就自动返回主页面。
2.思路:a:它不让我打开开发者人员工具,肯定是在打开开发者人员工具之后它检测了什么玩意
b:.那他就是调用了浏览器内置的东西,我这个就解决方法是下页面事件监听断点,如图:
这里的意思:
事件侦听器断点 event listener breakpoints
事件突变 Dom mutation
下面还有一些我就不翻译了,有兴趣可以去看看,翻译一下,小弟英语不好。
下这个断点之后,跳转完页面,加载ok之后他就会断下来,如图,我们就可以得到这道题的答案了。
答案大佬打了一下马赛克,这边大佬不公布答案,大家可以尝试一下。
切入JS反反调试
你千万别跟任何人谈任何事情。你只要一谈起,就会想念起每一个人来,我只知道我很想念我所谈到的每一个人。——J·D·塞林格《麦田里的守望者》
[JS逆向]过无限debugger调试
在JS逆向过程当中,获取用发F12抓取XHR的时候,常常会发现网页不让我们打开F12开发人员工具。如果这个时候我们开启工具中禁止断点之后,虽然我们可以抓取xhr,但是这样我们无法调试代码部分了,开启了禁止断点之后,我们自己也无法在代码当中下断点了
需求
在采集某些网站时,目标网站为了防止别人分析调试前端代码,采取了反调试措施。其中一种做法是当你按F12进入浏览器控制台后,浏览器会自动命中debugger断点,并且无限循环,导致无法调试。以食品药品监督管理总局数据查询网站为例。如下图:
按F12进入控制台
解决方法
禁用浏览器断点
点击图中按钮,之后将不会再命中任何断点。这种方法虽然可以防止无限循环命中debugger断点,但是也存在很大的缺陷,因为对于其他代码,我们还是需要断点调试功能的。所以这个方法仅限于静态分析。
利用中间人修改响应代码
用Fiddler删除响应代码中的debugger即可达到目的
实现的核心代码很简单:如下
FiddlerApplication.BeforeRequest += delegate(Fiddler,Session oS)
{
oS.bBufferResponse = true;
}
FiddlerApplication.BeforeResponse += (oS) =>
{
oS.utilDecodeResponse();
oS.utilReplaceLnResponse("debugger", String.Empty);
/*
*
Code 实在是找不到FiddlerCode的代码格式,只能用Python代替一下
*
*/
}
实际使用中发现,位于响应html页的debugger被删除了,但是仍然会弹出断点。分析页面得到,debugger断点位置一共有2处 第一处位于”http://qy1.sfda.gov.cn/datasearch/face3/dir.html"debugger以明文形式存在,Fiddler删除的就是这部分。通过分析另一处debugger位置,发现debugger是通过eval去实现的,响应中并没有直接出现debugger字段,所以没有被替换掉。
在Console输入
> _$uj()
<· "eval"
> _$dQ()
<· "(function() {var a = new Date(); debugger; return new Date() - a > 100;}())"
> _
代码经过强混淆,读者看到的函数名称是和我不一样的。
利用浏览器插件修改响应代码
具体原理和使用Fiddler是相同的,通过浏览器插件将请求重定向以达到修改代码的目的。也存在相同的问题
手动替换代码
既然修改响应结果无法满足需求,那只能从代码中寻找突破了。
以本文的网站为例,查看debugger断点处的调用栈堆,找到调用位置。其实在上文中间人方式结尾处已经发现了。是通过eval去实现断点的。我们先构造一个空方法
将目标网站的方法偷梁换柱
由于网站代码强混淆,所以函数名称会不一样。下面放个GIF图
完美解决 但是注意不要刷新,页面刷新后需要重新替换。
傻瓜式技巧
上文的几种方法,要么是存在缺陷,要么是步骤较为繁琐,我这边还有个压箱底的神技,不需要写任何代码,鼠标点点点就能够满足需求,为了避免伸手党,所以不放了。
总结
1-DebugPort
2-KdDisableDebugger
3-IsDebuggerPresent和CheckRemoteDebuggerPresent
4-hook
http://www.moguizuofang.com/bbs/thread-3235-1-1.html
http://bbs.pediy.com/showthread.php?t=126802
http://bbs.pediy.com/showthread.php?t=129810
DebugPort是进程EPROCESS结构里的一个成员,指向了一个用于进程调试的对象,如果一个进程不在被调试的时候那么就是NULL,否则他是一个指针。该对象负责在调试器与被调进程之间进行调试事件传递,因此被称为调试端口。被调试程序的事件由这个端口发送到调试器进程的。
HOOK系统中一些与调试相关的函数,也可以防止被各种调试器调试。比如某款程序在内核中就HOOK了下面这些函数:
NtOpenThread():防止调试器在程序内部创建线程
NtOpenProcess():防止OD(OllyDbg)等调试工具在进程列表中看到
KiAttachProcess():防止被附加上
NtReadVirtualMemory():防止被读内存
NtWriteVirtualMemory():防止内存被写
KdReceivePacket():KDCOME.dll 中Com串口接收数据函数
KdSendPacket():KDCOME.dll 中Com串口发送数据函数,可以HOOK这2个函数用来防止双机调试。
反反调试的思路也就出来了。针对清零DebugPort来防止调试的方法,可以通过对DebugPort内存地址下内存断点:
ba w4 debugport_addr
这样一旦有程序代码在修改DebugPort,就会被断下,从而找到对应的清零DebugPort的反调试代码,然后对这部分代码进行patch(用机器码0×90(nop)或者0xC3(ret)取代),从而让它失去作用,当然有的程序会对代码进行校验,一旦发现代码被篡改,就会采取保护措施,比如抛出异常或者退出程序。
针对调用系统函数如KdDisableDebugger()来检测调试器存在从而禁止被调试的方法,可以在对应的这些函数的地址下断点,然后对相关的代码进行patch,然后使该函数判断失效。比如:
bp KdDisableDebugger、eb xxx
针对通过HOOK系统函数来防止进程被调试的方法,可以直接将这些系统函数的钩子直接恢复,可以通过内核驱动程序或者借助一些ARK工具(比如Pchunter)就可以直接检测和恢复这些函数钩子。
总结 -2
在调试一些病毒程序的时候,可能会碰到一些反调试技术,也就是说,被调试的程序可以检测到自己是否被调试器附加了,如果探知自己正在被调试,肯定是有人试图反汇编啦之类的方法破解自己。为了了解如何破解反调试技术,首先我们来看看反调试技术。
一、Windows API方法
Win32提供了两个API, IsDebuggerPresent和CheckRemoteDebuggerPresent可以用来检测当前进程是否正在被调试,以IsDebuggerPresent函数为例,例子如下:
BOOL ret = IsDebuggerPresent();
printf("ret = %d\n", ret);
破解方法很简单,就是在系统里将这两个函数hook掉,让这两个函数一直返回false就可以了,网上有很多做hook API工作的工具,也有很多工具源代码是开放的,所以这里就不细谈了。
二、查询进程PEB的BeingDebugged标志位
当进程被调试器所附加的时候,操作系统会自动设置这个标志位,因此在程序里定期查询这个标志位就可以了,例子如下:
bool PebIsDebuggedApproach()
{
char result = 0;
__asm
{
// 进程的PEB地址放在fs这个寄存器位置上
mov eax, fs:[30h]
// 查询BeingDebugged标志位
mov al, BYTE PTR [eax + 2]
mov result, al
}
return result != 0;
}
三、查询进程PEB的NtGlobal标志位
跟第二个方法一样,当进程被调试的时候,操作系统除了修改BeingDebugged这个标志位以外,还会修改其他几个地方,其中NtDll中一些控制堆(Heap)操作的函数的标志位就会被修改,因此也可以查询这个标志位,例子如下:
bool PebNtGlobalFlagsApproach()
{
int result = 0;
__asm
{
// 进程的PEB
mov eax, fs:[30h]
// 控制堆操作函数的工作方式的标志位
mov eax, [eax + 68h]
// 操作系统会加上这些标志位FLG_HEAP_ENABLE_TAIL_CHECK,
// FLG_HEAP_ENABLE_FREE_CHECK and FLG_HEAP_VALIDATE_PARAMETERS,
// 它们的并集就是x70
//
// 下面的代码相当于C/C++的
// eax = eax & 0x70
and eax, 0x70
mov result, eax
}
return result != 0;
}
四、查询进程堆的一些标志位
这个方法是第三个方法的变种,只要进程被调试,进程在堆上分配的内存,在分配的堆的头信息里,ForceFlags这个标志位会被修改,因此可以通过判断这个标志位的方式来反调试。因为进程可以有很多的堆,因此只要检查任意一个堆的头信息就可以了,所以这个方法貌似很强大,例子如下:
bool HeapFlagsApproach()
{
int result = 0;
__asm
{
// 进程的PEB
mov eax, fs:[30h]
// 进程的堆,我们随便访问了一个堆,下面是默认的堆
mov eax, [eax + 18h]
// 检查ForceFlag标志位,在没有被调试的情况下应该是
mov eax, [eax + 10h]
mov result, eax
}
return result != 0;
}
五、使用NtQueryInformationProcess函数
NtQueryInformationProcess函数是一个未公开的API,它的第二个参数可以用来查询进程的调试端口。如果进程被调试,那么返回的端口值会是-1,否则就是其他的值。由于这个函数是一个未公开的函数,因此需要使用LoadLibrary和GetProceAddress的方法获取调用地址,示例代码如下:
// 声明一个函数指针。
typedef NTSTATUS (WINAPI *NtQueryInformationProcessPtr)(
HANDLE processHandle,
PROCESSINFOCLASS processInformationClass,
PVOID processInformation,
ULONG processInformationLength,
PULONG returnLength);
bool NtQueryInformationProcessApproach()
{
int debugPort = 0;
HMODULE hModule = LoadLibrary(TEXT("Ntdll.dll "));
NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hModule, "NtQueryInformationProcess");
if ( NtQueryInformationProcess(GetCurrentProcess(), (PROCESSINFOCLASS)7, &debugPort, sizeof(debugPort), NULL) )
printf("[ERROR NtQueryInformationProcessApproach] NtQueryInformationProcess failed\n");
else
return debugPort == -1;
return false;
}
六、NtSetInformationThread方法
这个也是使用Windows的一个未公开函数的方法,你可以在当前线程里调用NtSetInformationThread,调用这个函数时,如果在第二个参数里指定0x11这个值(意思是ThreadHideFromDebugger),等于告诉操作系统,将所有附加的调试器统统取消掉。示例代码:
// 声明一个函数指针。
typedef NTSTATUS (*NtSetInformationThreadPtr)(HANDLE threadHandle,
THREADINFOCLASS threadInformationClass,
PVOID threadInformation,
ULONG threadInformationLength);
void NtSetInformationThreadApproach()
{
HMODULE hModule = LoadLibrary(TEXT("ntdll.dll"));
NtSetInformationThreadPtr NtSetInformationThread = (NtSetInformationThreadPtr)GetProcAddress(hModule, "NtSetInformationThread");
NtSetInformationThread(GetCurrentThread(), (THREADINFOCLASS)0x11, 0, 0);
}
七、触发异常的方法
这个技术的原理是,首先,进程使用SetUnhandledExceptionFilter函数注册一个未处理异常处理函数A,如果进程没有被调试的话,那么触发一个未处理异常,会导致操作系统将控制权交给先前注册的函数A;而如果进程被调试的话,那么这个未处理异常会被调试器捕捉,这样我们的函数A就没有机会运行了。
这里有一个技巧,就是触发未处理异常的时候,如果跳转回原来代码继续执行,而不是让操作系统关闭进程。方案是在函数A里修改eip的值,因为在函数A的参数_EXCEPTION_POINTERS里,会保存当时触发异常的指令地址,所以在函数A里根据这个指令地址修改寄存器eip的值就可以了,示例代码如下:
// 进程要注册的未处理异常处理程序A
LONG WINAPI MyUnhandledExceptionFilter(struct _EXCEPTION_POINTERS *pei)
{
SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)
pei->ContextRecord->Eax);
// 修改寄存器eip的值
pei->ContextRecord->Eip += 2;
// 告诉操作系统,继续执行进程剩余的指令(指令保存在eip里),而不是关闭进程
return EXCEPTION_CONTINUE_EXECUTION;
}
bool UnhandledExceptionFilterApproach()
{
SetUnhandledExceptionFilter(MyUnhandledExceptionFilter);
__asm
{
// 将eax清零
xor eax, eax
// 触发一个除零异常
div eax
}
return false;
}
八、调用DeleteFiber函数
如果给DeleteFiber函数传递一个无效的参数的话,DeleteFiber函数除了会抛出一个异常以外,还是将进程的LastError值设置为具体出错原因的代号。然而,如果进程正在被调试的话,这个LastError值会被修改,因此如果调试器绕过了第七步里讲的反调试技术的话,我们还可以通过验证LastError值是不是被修改过来检测调试器的存在,示例代码:
bool DeleteFiberApproach()
{
char fib[1024] = {0};
// 会抛出一个异常并被调试器捕获
DeleteFiber(fib);
// 0x57的意思是ERROR_INVALID_PARAMETER
return (GetLastError() != 0x57);
}