首页 | 安全文章 | 安全工具 | Exploits | 本站原创 | 关于我们 | 网站地图 | 安全论坛
  当前位置:主页>安全文章>文章资料>网络安全>文章内容
另一种WinDbg插件编写方法-Debugger Engine Extension
来源:www.nsfocus.com 作者:Flier 发布时间:2004-07-07  

另一种 WinDbg 插件编写方法 - Debugger Engine Extension

作者:Flier Lu <flier @ nsfocus.com>
出处:http://flier_lu.blogone.net/?id=2178387
主页:http://www.nsfocus.com
日期:2004-07-06

在仔细阅读上期月刊中 scz 的《MSDN系列(11)--给SoftICE写插件》一文后,忍不住自己动手试试 WinDbg 插件的编写,呵呵。不过我选择的是与 scz 不同的另一种 WinDbg 插件编写方法。
WinDbg 最新版本的 sdkhelp 目录下有一个 debugext.chm 文件,里面有很详细的 WinDbg 插件编写文档。其中提到 WinDbg 支持两种类型的插件:DbgEng 扩展和 WdbgExts 扩展。前者是使用在 dbgeng.h 中定义的针对 Debugger Engine API 的调试扩展;后者则是使用在 wdbgexts.h 中定义的专门针对 WinDbg 的调试扩展。小四文章中使用的就是后者的接口,较为简明,也可以被 SoftIce 很好支持;我则选择前一种插件类型,功能更强大,而且可以被除 WinDbg 外的其他支持 Debugger Engine API 的工具,如 Visual Studio.NET 支持。
与 WdbgExts 类型扩展插件类似,DbgEng 类型扩展插件必须实现一个初始化回调函数:

HRESULT CALLBACK DebugExtensionInitialize(OUT PULONG Version, OUT PULONG Flags);

此函数在使用 .load 命令载入插件时被调用,返回插件的版本信息。如

const int EXTS_VERSION_MAJOR = 1;
const int EXTS_VERSION_MINOR = 0;

extern "C" HRESULT CALLBACK DebugExtensionInitialize(OUT PULONG Version, OUT PULONG Flags)
{
*Version = DEBUG_EXTENSION_VERSION(EXTS_VERSION_MAJOR, EXTS_VERSION_MINOR);
*Flags = 0;

return S_OK;
}

定义插件回调函数时,必须使用 extern "C" 指定此函数的函数名使用与 C 兼容的命名格式,并建立一个 .def 文件定义入口名字,如

LIBRARY ClrExts

EXPORTS
DebugExtensionInitialize
DebugExtensionUninitialize
DebugExtensionNotify
KnownStructOutput

help
showcontext

这里建立一个新的 DbgEng 类型插件 ClrExts 完成对 CLR 调试支持扩展功能,并导出四个标准回调函数。除 DebugExtensionInitialize 必须有以外,其他三个回调函数是可选的。


void CALLBACK DebugExtensionNotify(IN ULONG Notify, IN ULONG64 Argument);

DebugExtensionNotify 函数在调试会话的状态转换的时候被调用,以通知插件调整自己的状态。Notify 参数可以有四个值:

DEBUG_NOTIFY_SESSION_ACTIVE: 调试会话被激活
DEBUG_NOTIFY_SESSION_INACTIVE: 没有被激活的调试会话
DEBUG_NOTIFY_SESSION_ACCESSIBLE: 调试会话被中断并可访问
DEBUG_NOTIFY_SESSION_INACCESSIBLE: 调试会话恢复执行并不能访问

调试会话的概念,表示是否正在调试一个进程中;而根据调试状态,中断目标程序运行并由调试器获得控制权时,调试会话被中断并可访问(DEBUG_NOTIFY_SESSION_ACCESSIBLE)。
调试插件可以通过跟踪这几个状态的改变,调整自己对目标调试进程的控制方法。


void CALLBACK DebugExtensionUninitialize(void);

DebugExtensionUninitialize函数则是在插件被 .unload 命令卸载的时候被调用。


HRESULT CALLBACK KnownStructOutput(IN ULONG Flag, IN ULONG64 Address, IN PSTR StructName, OUT PSTR Buffer, IN OUT PULONG BufferSize);

最后一个 KnownStructOutput 回调函数较少被用到,用于提供此调试插件支持打印的结构列表,并可打印指定地址的指定结构内容。

与 WdbgExts 类型插件不太一样,DbgEng 类型插件的调试接口可以通过 DebugCreate 函数,自行获取其 COM 接口

HRESULT DebugCreate(IN REFIID InterfaceId, OUT PVOID *Interface);

也可以通过插件命令的参数获得。插件的通用命令接口如下:

HRESULT CALLBACK (* PDEBUG_EXTENSION_CALL)(IN IDebugClient *Client, IN OPTIONAL PCSTR Args);

第一个参数 Client 就是调试接口,另外一个则是命令的参数字符串。

可以使用一个简单的包装类 CDebugClient 对 IDebugClient 接口就行包装,其构造函数自动获取调试接口

class CDebugClient
{
private:
HRESULT m_hr;

CComPtr<IDebugClient> m_spDebugClient;
CComQIPtr<IDebugControl> m_spDebugControl;

WINDBG_EXTENSION_APIS32 m_extensionApis;
public:
CDebugClient(void);
};

CDebugClient::CDebugClient(void)
{
m_hr = DebugCreate(__uuidof(IDebugClient), (PVOID *)&m_spDebugClient);

if(SUCCEEDED(m_hr))
{
m_spDebugControl = m_spDebugClient;

m_extensionApis.nSize = sizeof(m_extensionApis);

m_hr = m_spDebugControl->GetWindbgExtensionApis32(&m_extensionApis);
}
}

DebugCreate 函数构造一个新的 IDebugClient 接口实例,并放入 ATL 接口包装类 CComPtr<IDebugClient> 的对象 m_spDebugClient 中,并可从此接口查询获取 IDebugControl 接口实例。IDebugControl::GetWindbgExtensionApis32 则可以获取与 WdbgExts 类型插件兼容的调试接口函数集。不过我们后面将看到,DbgEng 的相应接口,比 WinDbg 的传统函数集功能要强大得多。
对于插件命令的入口直接给出的 IDebugClient 实例,则可以省去构造过程,如

CDebugClient::CDebugClient(IDebugClient *dbg)
: m_outLevel(olDefault), m_hr(S_OK), m_spDebugClient(dbg)
{
if(dbg)
{
m_spDebugControl = m_spDebugClient;

m_extensionApis.nSize = sizeof(m_extensionApis);

m_hr = m_spDebugControl->GetWindbgExtensionApis32(&m_extensionApis);
}
}

在了解了调试接口的创建和包装方法后,可以建立第一个插件命令,help,显示一个帮助字符串给调试器

extern "C" HRESULT CALLBACK help(IN IDebugClient *Client, IN OPTIONAL PCSTR Args)
{
UNREFERENCED_PARAMETER(Args);

CDebugClient DebugClient(Client);

if(FAILED(DebugClient.getLastHResult())) return DebugClient.getLastHResult();

DebugClient.Info("Help for %s "
" help - Show this help ", EXTS_NAME);

return DebugClient.getLastHResult();
}

UNREFERENCED_PARAMETER 是一个宏,用于显式引用一次不会用到的函数参数,避免编译器警告;
然后使用命令参数构造 CDebugClient 实例,并判断其构造过程是否有效;
接着调用 CDebugClient 封装的 Info 函数打印一堆帮助字符串;
最后返回 DebugClient 的最后调用状态。

函数逻辑非常简单,就不罗嗦了,下面看看对字符串的输出

enum OutputLevel
{
olAll,
olDebug,
olInfo,
olWarning,
olError,
#ifdef _DEBUG
olDefault = olAll
#else
olDefault = olInfo
#endif
};

class CDebugClient
{
private:
OutputLevel m_outLevel;
};

首先定义了5个缺省的输出级别,所有、调试、信息、警告和错误;然后定义调试接口的信息显示级别。

class CDebugClient
{
public:
void OutputString(OutputLevel lvl, const char *fmt, va_list args) const;
void OutputString(OutputLevel lvl, const char *fmt, ...) const;

void DoLog(OutputLevel level, const char *fmt, va_list args) const
{
if(m_outLevel <= level) OutputString(level, fmt, args);
}

#define DEF_LOG_LEVEL(name) void name(const char *fmt, ...) const
{
va_list args;
va_start(args, fmt);

DoLog(ol ## name, fmt, args);

va_end(args);
}

DEF_LOG_LEVEL(Debug);
DEF_LOG_LEVEL(Info);
DEF_LOG_LEVEL(Warning);
DEF_LOG_LEVEL(Error);
};

实际的信息输出放在 OutputString 函数中完成,而 DoLog 则根据当前调试接口的信息级别判断是否需要输出信息。并使用 DEF_LOG_LEVEL 宏定义四种常用的信息输出函数。

void CDebugClient::OutputString(OutputLevel lvl, const char *fmt, va_list args) const
{
#if 1
static ULONG OutputMask[] = {
0,
DEBUG_OUTPUT_VERBOSE,
DEBUG_OUTPUT_NORMAL,
DEBUG_OUTPUT_WARNING,
DEBUG_OUTPUT_ERROR
};

m_spDebugControl->OutputVaList(OutputMask[lvl], fmt, args);
#else
std::string str;

str.resize(_vscprintf(fmt, args)+1, 0);
_vsnprintf(const_cast<char *>(str.c_str()), str.size(), fmt, args);

m_extensionApis.lpOutputRoutine(str.c_str());
#endif
}

void CDebugClient::OutputString(OutputLevel lvl, const char *fmt, ...) const
{
va_list args;
va_start(args, fmt);

OutputString(lvl, fmt, args);

va_end(args);
}

OutputString 可以通过 IDebugControl 的 OutputVaList 方法输出,也可以通过传统的 WdbgExts 调试接口的 lpOutputRoutine 函数输出。前者的优点是可以根据信息输出级别,设定相应的输出掩码。如 olDebug 对应于 DEBUG_OUTPUT_VERBOSE,此类型信息只有在 WinDbg 打开了 Verbose 模式(菜单 View/Verbose Output)时才会显示,非常适合对插件就行调试跟踪。

在了解了调试接口函数的大致使用流程后,接着编写一个有实际意义的功能,也就是 scz 文章中的 showcontext 函数,代码如下:

#define OFFSETOF(TYPE, MEMBER) ((size_t)&((TYPE)0)->MEMBER)

extern "C" HRESULT CALLBACK showcontext(IN IDebugClient *Client, IN OPTIONAL PCSTR Args)
{
CDebugClient DebugClient(Client);

if(FAILED(DebugClient.getLastHResult())) return DebugClient.getLastHResult();

DebugClient.Debug("%s: call externsion function showcontext with arguments - %s ", EXTS_NAME, Args);

std::string buf;

DWORD dwSize = OFFSETOF(PCONTEXT, ExtendedRegisters);

buf.resize(dwSize);

DWORD dwAddress = DebugClient.Evaluate(Args);

DebugClient.Debug("%s: get expression "%s" 's value %x ", EXTS_NAME, Args, dwAddress);

if(DebugClient.ReadMemory(dwAddress, buf) == dwSize)
{
PCONTEXT pCtxt = (PCONTEXT)buf.c_str();

DebugClient.Info("EAX=%08X EBX=%08X ECX=%08X EDX=%08X ESI=%08X "
"EDI=%08X EBP=%08X ESP=%08X EIP=%08X EFLAGS=%08X "
"CS=%04X DS=%04X SS=%04X ES=%04X FS=%04X GS=%04X ",
pCtxt->Eax, pCtxt->Ebx, pCtxt->Ecx, pCtxt->Edx, pCtxt->Esi,
pCtxt->Edi, pCtxt->Ebp, pCtxt->Esp, pCtxt->Eip, pCtxt->EFlags,
(WORD)pCtxt->SegCs, (WORD)pCtxt->SegDs, (WORD)pCtxt->SegSs,
(WORD)pCtxt->SegEs, (WORD)pCtxt->SegFs, (WORD)pCtxt->SegGs);
}
else
{
DebugClient.Warning("%s: Cannot read process memory @ %x ", EXTS_NAME, dwAddress);
}

return DebugClient.getLastHResult();
}

代码逻辑很简单:首先获取调试接口;然后调用 DebugClient.Evaluate 函数分析命令参数的表达式,获取目标地址;然后调用 DebugClient.ReadMemory 函数从指定地址读取 CONTEXT 结构的部分内容;最后调用 DebugClient.Info 函数输出信息。
OFFSETOF 宏是一个获取结构部分内容长度的小技巧,通过将 0 地址强制转换为结构指针,来获得指定字段在结构中的相对偏移。

size_t CDebugClient::Evaluate(const char *lpExpression)
{
DEBUG_VALUE value;

m_hr = m_spDebugControl->Evaluate(lpExpression, DEBUG_VALUE_INT32, &value, NULL);

return value.I32;
}

CDebugClient::Evaluate 函数简单调用 IDebugControl 接口的 Evaluate 函数,完成表达式的计算工作。例如敲入 "showcontext *(esp+4)"这条命令,命令行参数 Args 的内容就是 "*(esp+4)",而 Evaluate 函数可以将这个表达式计算得到一个确定的地址。DEBUG_VALUE_INT32参数指定需要获取一个 32 位整数;DEBUG_VALUE 则是一个类似 VARIANT 的联合类型,用户保存各种可能类型的参数。

size_t CDebugClient::ReadMemory(size_t offset, void *buf, size_t size) const
{
ULONG readBytes;

#if 1
CComQIPtr<IDebugDataSpaces> spDebugDataSpaces(m_spDebugClient);

spDebugDataSpaces->ReadVirtual(offset, buf, size, &readBytes);
#else
m_extensionApis.lpReadProcessMemoryRoutine(offset, buf, size, &readBytes);
#endif

return readBytes;
}

size_t CDebugClient::ReadMemory(size_t offset, std::string& buf) const
{
return ReadMemory(offset, const_cast<char *>(buf.c_str()), buf.size());
}

而 ReadMemory 则比较简单,通过 IDebugDataSpaces 接口或 WdbgExts 兼容接口都能读取目标进程的虚拟内存。

至此,编写一个 DbgEng 类型插件的必要内容已经基本上介绍完了,以后有机会再详细介绍调试接口的具体使用方法,呵呵

参考引用:

MSDN系列(11)--给SoftICE写插件,scz,http://www.nsfocus.net/index.php?act=magazine&do=view&mid=2204


 
[推荐] [评论(0条)] [返回顶部] [打印本页] [关闭窗口]  
匿名评论
评论内容:(不能超过250字,需审核后才会公布,请自觉遵守互联网相关政策法规。
 §最新评论:
  热点文章
·一句话木马
·samcrypt.lib简介
·教你轻松查看QQ空间加密后的好友
·web sniffer 在线嗅探/online ht
·SPIKE与Peach Fuzzer相关知识
·asp,php,aspx一句话集合
·Cisco PIX525 配置备忘
·用Iptables+Fedora做ADSL 路由器
·检查 Web 应用安全的几款开源免
·Md5(base64)加密与解密实战
·NT下动态切换进程分析笔记
·风险评估中的渗透测试
  相关文章
·三种禁用FileSystemObject(FSO)
·谈php+mysql注射语句构造-Okphp
·错误的网络访问控制策略导致PMTU
·另外一种隐藏LKM的方法
·追踪垃圾邮件来源
·某大型企业局域网安全解决方案
·SQL Server密码对照表
·风险评估应用技术和工具初探
·榨干MS SQL最后一滴血
·利用系统fingerprint 协助网络事
·IPSec打造FreeBSD下信息安全传输
·全面分析黑客常用的九种攻击方法
  推荐广告
CopyRight © 2002-2022 VFocuS.Net All Rights Reserved