freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

软件调试详解
红队蓝军 2022-05-31 20:45:41 176026
所属地 四川省


前言

在windows里面调试跟异常息息相关,如果想要对调试得心应手,异常处理的知识是必不可少的,本文主要介绍的是软件调试方面的有关知识,讲解调试程序和被调试程序之间如何建立联系

调试对象

调试器和被调试程序

image-20220331111724999.png

调试器与被调试程序之间建立起联系的两种方式

  • CreateProcess

  • DebugActiveProcess

与调试器建立连接

首先看一下DebugActiveProcess

image-20220331111629799.png

调用ntdll.dllDbgUiConnectToDbg

image-20220331111914219.png

image-20220331112008360.png

再调用ZwCreateDebugObject

image-20220331112048790.png

通过调用号进入0环

image-20220331112119321.png

进入0环创建DEBUG_OBJECT结构体

typedef struct _DEBUG_OBJECT {
     KEVENT EventsPresent;
     FAST_MUTEX Mutex;
     LIST_ENTRY EventList;
     ULONG Flags;
} DEBUG_OBJECT, *PDEBUG_OBJECT;

然后到ntoskrnl里面看一下NtCreateDebugObject

image-20220331113057515.png

然后调用了ObInsertObject创建DebugObject结构返回句柄

image-20220331113136955.png

再回到ntdll.dll,当前线程回0环创建了一个DebugObject结构,返回句柄到3环存放在了TEB的0xF24偏移处

也就是说,遍历TEB的0xF24偏移的地方,如果有值则一定是调试器

image-20220331113252356.png

与被调试程序建立连接

还是回到kernel32.dllDebugActiveProcess,获取句柄之后调用了DbgUiDebugActiveProcess

image-20220331114614705.png

调用ntdll.dllDbgUiDebugActiveProcess

image-20220331114716677.png

跟到ntdll.dll里面的DbgUiDebugActiveProcess,传入两个参数,分别为调试器的句柄和被调试进程的句柄

image-20220331114846279.png

通过调用号进0环

image-20220331115013208.png

来到0环的NtDebugActiveProcess, 第一个参数为被调试对象的句柄,第二个参数为调试器的句柄

image-20220331115116879.png

执行ObReferenceObjectByHandle,把被调试进程的句柄放到第五个参数里面,这里eax本来存储的是调试器的EPROCESS,执行完之后eax存储的就是被调试进程的EPROCESS

image-20220331120028495.png

这里判断调试器打开的进程是否是自己,如果是自己则直接退出

image-20220331143227822.png

也不能调试系统初始化的进程

image-20220331143429325.png

然后获取调试对象的地址,之前是句柄,但是句柄在0环里面是无效的,这里就要找真正的地址

image-20220331143605658.png

获取到调试对象的地址之后还是存到ebp+Process的地方,这里之前是被调试对象的地址,现在存储的是调试对象的地址

image-20220331143902163.png

将调试进程和被调试的PEPROCESS传入_DbgkpSetProcessDebugObject,将调试对象和被调试进程关联起来

image-20220331145648149.png

跟进函数,发现有判断DebugPort是否为0的操作,ebx为0,edi为被调试进程的EPROCESS,那么edi+0bc就是调试端口

image-20220331145849944.png

然后再把调试对象的句柄放到被调试对象的DebugPort里面

image-20220331150347451.png

调试事件的采集

调试事件的种类

typedef enum _DBGKM_APINUMBER
{
    DbgKmExceptionApi = 0,	//异常
    DbgKmCreateThreadApi = 1,	//创建线程
    DbgKmCreateProcessApi = 2,	//创建进程
    DbgKmExitThreadApi = 3,	//线程退出
    DbgKmExitProcessApi = 4,	//进程退出
    DbgKmLoadDllApi = 5,		//加载DLL
    DbgKmUnloadDllApi = 6,	//卸载DLL
    DbgKmErrorReportApi = 7,	//已废弃
    DbgKmMaxApiNumber = 8,	//最大值
} DBGKM_APINUMBER;	

调试事件的采集函数

image-20220331160251734.png

当创建进程或者线程的时候,一定会调用PspUserThreadStartup

image-20220331160332662.png

判断当前线程是否为当前进程的第一个线程,如果是的话就生成一个编号为1的调试事件

image-20220331160457595.png

再看一下退出线程必经的函数PspExitThread

image-20220331172528276.png

判断Debugport是否为0,如果为0则不搜集信息

image-20220331172641988.png

image-20220331173503130.png

进入跳转,判断这个线程是不是当前最后一个线程,如果是则调用DbgkExitProcess

image-20220331173645459.png

如果不是则调用DbgkExitThread

image-20220331174052501.png

DbgkpSendApiMessage

DbgkpSendApiMessage这个api主要就是将各种调试信息封装成一个结构体写到_DEBUG_OBJECT结构里面,无论是哪种事件,最后都会调用DbgkpSendApiMessage,如果想隐藏进程/线程的创建,就可以给DbgkCreateThread挂钩子,如果想隐藏所有的调试事件那么就可以给DbgkpSendApiMessage挂钩子

image-20220331174541264.png

这里跟一下DbgkExitThreadDbgkpSendApiMessage的过程,跟进函数直接就可以看到DbgkpSendApiMessage

image-20220331174839809.png

所有搜集调试事件的api都会调用DbgkpSendApiMessage

image-20220331175318674.png

image-20220331175326227.png

DbgkpSendApiMessage(x, x)参数说明:

  1. 第一个参数:消息结构 每种消息都有自己的消息结构 共有7种类型

  2. 第二个参数:要不要把本进程内除了自己之外的其他线程挂起。

有些消息需要把其他线程挂起,比如CC 有些消息不需要把线程挂起,比如模块加载。DbgkSendApiMessage是调试事件收集的总入口,如果在这里挂钩子,调试器将无法调试。

LoadLibrary

首先在kernel32.dll里面调用RtlAllocateHeap

image-20220331175840245.png

然后跟到ntdll.dll调用了NtQueryPerformanceCounter

image-20220331175913433.png

通过调用号进0环

image-20220331175926381.png

image-20220331175949840.png

总结来说,LoadLibrary首先调用CreateMapping创建一块共享内存,再通过NtMapViewOfSection映射到线性地址,调用DbgkMapViewOfSection将结构体发送给DbgkpSendApiMessage

_DEBUG_OBJECT

typedef struct _DEBUG_OBJECT {
     KEVENT EventsPresent;		//+00 用于指示有调试事件发生
     FAST_MUTEX Mutex;			//+10 用于同步互斥对象
     LIST_ENTRY EventList;		//+30 保存调试消息的链表
     ULONG Flags;				//+38 标志 调试消息是否已读取
} DEBUG_OBJECT, *PDEBUG_OBJECT;

调试事件的处理

因为每种事件的调试信息不一样,所以会有很多种类(7种)的api去采集

image-20220331220729871.png

编号的值也是对应的

image-20220331221540163.png

image-20220331221558581.png

// Debug1.cpp : Defines the entry point for the console application.
//


#include "stdafx.h"
#include <iostream>
#include <Windows.h>
#include <stdlib.h>

void TestDebugger()
{
	BOOL nIsContinue = NULL;
    STARTUPINFOA sw = { 0 };
    PROCESS_INFORMATION pInfo = { 0 };
    auto retCP = CreateProcessA("C:\\Dbgview.exe",NULL, NULL, NULL, TRUE,DEBUG_PROCESS|| DEBUG_ONLY_THIS_PROCESS, NULL, NULL, &sw, &pInfo);
    
	if (retCP == 0)
    {
        printf("CreateProcess error : %d\n", GetLastError());
		return;
    }

    while (TRUE)
    {
        DEBUG_EVENT debugEvent = { 0 };
        auto rDebugEvent = WaitForDebugEvent(&debugEvent, -1);

        if (rDebugEvent)
        {
			switch (debugEvent.dwDebugEventCode)
			{
				 case EXCEPTION_DEBUG_EVENT:
					 printf("EXCEPTION_DEBUG_EVENT\n");
					 break;

				 case CREATE_THREAD_DEBUG_EVENT:
					 printf("CREATE_THREAD_DEBUG_EVENT\n");
					 break;

				 case CREATE_PROCESS_DEBUG_EVENT:
					 printf("CREATE_PROCESS_DEBUG_EVENT\n");
					 break;

				 case EXIT_THREAD_DEBUG_EVENT:
					 printf("EXIT_THREAD_DEBUG_EVENT\n");
					 break;

				 case EXIT_PROCESS_DEBUG_EVENT:
					 printf("EXIT_PROCESS_DEBUG_EVENT\n");
					 break;

				 case LOAD_DLL_DEBUG_EVENT:
					 printf("LOAD_DLL_DEBUG_EVENT\n");
					 break;

				 case UNLOAD_DLL_DEBUG_EVENT:
					 printf("UNLOAD_DLL_DEBUG_EVENT\n");
					 break;

				 case OUTPUT_DEBUG_STRING_EVENT:
					 printf("OUTPUT_DEBUG_STRING_EVENT\n");
					 break;
			} 
        }
		//在发送事件event给调试器debugger时,被调试进程会被挂起,直到调试器调用了continueDebugEvent函数
        ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId,DBG_CONTINUE);
    }
}

int main()
{
    TestDebugger();
    system("pause");
    return 0;
}

这里用调试模式启动windbg

image-20220401105959845.png

可以发现这里有一个异常,这里先打印一下异常处理返回的代码

printf("EXCEPTION_DEBUG_EVENT : %x %x %x\n",debugEvent.u.Exception.ExceptionRecord.ExceptionAddress,debugEvent.u.Exception.ExceptionRecord.ExceptionCode,debugEvent.u.Exception.ExceptionRecord.ExceptionFlags);

image-20220401110739388.png

将程序拖入OD看到系统有一个int3断点

image-20220401111013210.png

那么为什么会有一个异常处理的事件呢?这里首先看一下进程的创建过程

1.映射exe文件
2.创建内核对象EPROCESS
3.映射系统dll(ntdll.dll)
4.创建线程内核对象ETHREAD
5.系统启动线程
    映射dll(ntdll.LdrInitializeThunk)
    线程开始执行

在映射dll的过程中调用了LdrInitializeThunk这个api,LdrInitializeThunk会调用LdrpInitializeProcess初始化进程

首先找到TEB,然后找TEB的0x30偏移的PEB放入ebx

image-20220401111818596.png

DbgBreakPoint其实就是int3的封装

image-20220401111950688.png

看一下交叉引用,可以看到LdrpRunInitializeRoutines引用了DbgBreakPoint

image-20220401112037459.png

这里只有当程序处于调试模式的时候才会启动

image-20220401112129503.png

在内核文件里面看一下NtDebugActiveProcess

image-20220401113546366.png

会发送线程和模块的加载信息

image-20220401113811656.png

但是这个信息是不靠谱的,因为这个api是通过遍历PEB链表的方式来寻找模块

在PEB的Ldr结构里面有三个模块,DbgkpPostFakeProcessCreateMessages这个api就是通过查询这个结构来判断加载了哪些模块

也就是说当程序加载完成之后,这个api才会去链表里面找模块,但是这个时候可能信息已经被摘除,所以如果要想更准确的获取信息,就可以通过遍历vad树的方式来获取1

异常的处理流程

处理流程

image-20220401135907896.png

正常的异常处理流程

image-20220401140234726.png

产生异常的时候首先会将异常传递给调试器,如果调试器不处理则继续寻找异常处理函数

这里设置为异常为忽略的话就会执行自己的异常处理函数

image-20220401140635618.png

image-20220401141003958.png

如果设置为不忽略的情况下就会一直断在某一行

image-20220401141053693.png

UnhandledExceptionFilter

相当于编译器为我们生成了一段伪代码

__try
{

}
__except(UnhandledExceptionFilter(GetExceptionInformation())
{
	//终止线程
	//终止进程
}

只有程序被调试时,才会存在未处理异常

UnhandledExceptionFilter的执行流程:

1) 通过NtQueryInformationProcess查询当前进程是否正在被调试,如果是,返回EXCEPTION_CONTINUE_SEARCH,此时会进入第二轮分发 

2) 如果没有被调试: 

查询是否通过SetUnhandledExceptionFilter注册处理函数 如果有就调用 

如果没有通过SetUnhandledExceptionFilter注册处理函数 弹出窗口 让用户选择终止程序还是启动即时调试器 

如果用户没有启用即时调试器,那么该函数返回EXCEPTION_EXECUTE_HANDLER

SetUnhandledExceptionFilter

如果没有通过SetUnhandledExceptionFilter注册异常处理函数,则程序崩溃

image-20220401142104425.png

image-20220401142146258.png

测试代码如下,我自己构造一个异常处理函数callback并用SetUnhandledExceptionFilter注册,构造一个除0异常,当没有被调试的时候就会调用callback处理异常,然后继续正常运行,如果被调试则不会修复异常,因为这是最后一道防线,就会直接退出,起到反调试的效果

// SEH7.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <windows.h>

long _stdcall callback(_EXCEPTION_POINTERS* excp)
{
	excp->ContextRecord->Ecx = 1;
	return EXCEPTION_CONTINUE_EXECUTION;
}

int main(int argc, char* argv[])
{
	SetUnhandledExceptionFilter(callback);

	_asm
	{
		xor edx,edx
		xor ecx,ecx
		mov eax,0x10
		idiv ecx
	}

	printf("Run again!");
	getchar();
	return 0;
}

直接启动可以正常运行

image-20220329113645787.png

使用od打开则直接退出

image-20220329113851211.png

image-20220329113905022.png

// Debug3.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <windows.h>
DWORD g_Test = 0;

LONG NTAPI TopLevelExceptFilter(PEXCEPTION_POINTERS pExcepinfo)
{
	printf("The top_function fix the exception!\n");
	g_Test = 1;
	return EXCEPTION_CONTINUE_EXECUTION;

}

int main(int argc, char* argv[])
{
	int x = 0;
	int y = 100;

	SetUnhandledExceptionFilter(&TopLevelExceptFilter);

	x = y/g_Test;

	printf("正常逻辑开始执行\n");

	for (int i=0;i<10;i++)
	{
		::Sleep(1000);
		printf("%d\n", i);
	}

	getchar();
	return 0;
}

正常情况下执行程序

image-20220401144025331.png

如果是调试程序则直接退出

image-20220401144253203.png

# 渗透测试 # 网络安全 # 系统安全 # 漏洞分析 # 网络安全技术
本文为 红队蓝军 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
红队蓝军 LV.4
这家伙太懒了,还未填写个人描述!
  • 13 文章数
  • 9 关注者
通过硬件断点对抗hook检测
2022-04-25
bypass Bitdefender
2022-04-18
绕过360实现lsass转储
2022-04-18
文章目录