让 UDRL 调试(稍微)简单一点

admin 2025年7月9日01:52:55评论5 views字数 4117阅读13分43秒阅读模式

Making the Debugging of UDRLs (a bit) Easier 

免责声明:本博客文章仅用于教育和研究目的。提供的所有技术和代码示例旨在帮助防御者理解攻击手法并提高安全态势。请勿使用此信息访问或干扰您不拥有或没有明确测试权限的系统。未经授权的使用可能违反法律和道德准则。作者对因应用所讨论概念而导致的任何误用或损害不承担任何责任。

对 UDRL-VS 框架的一个简单增强:在运行时支持 loader 中的调试字符串日志输出

引入了这个 Visual Studio 项目作为 Cobalt Strike UDRL 开发的模板后,确实带来了一些小技巧,让恶意代码开发者的生活稍微轻松点。开发 Position Independant Code(位置无关代码,PIC)确实非常烦人,限制也很多。比如,无法用传统方式定义字符串。该项目通过巧妙的预处理器宏(如PIC_STRING())来简化字符串定义。其它有用的特性还包括PRINT()宏、Debug构建模式等。更多细节可参考上面那篇博客。

但实际用下来,调试 UDRL 依然很痛苦。根本上讲,PIC 代码不是一个可执行文件(executable)。虽然 UDRL-VS 模板提供的Debug构建在某些场景下有用,但它生成的.exe 并不等同于你在Release模式下拿到的 UDRL。实际调试时,我发现直接在 WinDbg 里调试Release模式下的 PIC blob 更有价值,而不是去调试Debug模式下的可执行文件。

让 UDRL 调试(稍微)简单一点

调试字符串(Debug Strings)

我的调试手法其实很原始,基本就是到处插printf("Heren");printf("Here2n");。但做恶意代码开发时,很多情况下你根本没有控制台(console)输出的条件。对于 UDRL 来说,printf()本身也不可用。

如果你用Debug模式编译 UDRL,会得到一个可执行文件,里面有PRINT()宏,可以让你把内容printf()到控制台。但这些在Release模式下都用不上,因为你只会生成一个 PIC blob。

这种情况下,替代 console 输出的办法就是用OutputDebugString()API把日志打到 WinDbg 里。只要你把 WinDbg attach 到 payload 所在进程,就能看到这些日志。我的日志内容通常就是一堆Here111Here0Here123这种。

实际上,Sleepmask-VS 就用到了这个特性。如果你定义了ENABLE_LOGGING(参考这里),它会提供类似能力。

你会得到一个DLOGF()宏,封装了这个 API,帮你处理变参、格式化字符串等麻烦事。

但 UDRL-VS 模板里没有类似的 helper。不幸的是,DLOGF的例子不能直接用在 UDRL 里,因为 UDRL 不能像下面这样用传统字符串:

DLOG("I reached here n");

因此你必须使用类似下面的方式,通过前面提到的PIC_STRING辅助宏将所有内容放在.text段中:

PIC_STRING(mystring, "I reached here n"); //expands to constexpr char mystring[] {'I'' ''r''e',... };DLOG(mystring);

如果每个调试输出都要写两行代码,这确实有点烦人。

你不能像这样把这两行语句包装成一个单行宏:

#define DLOGF(format, ...) PIC_STRING(mystring, format); DLOG(mystring)`

因为这样会导致每次调用时都重新定义 mystring

宏魔法 (Macro Magic)

__LINE__宏会自动展开为调用处的行号。我们可以利用这个特性来创建每次调用时都唯一的变量名,从而避免宏重复定义相同的 mystring

以下代码片段展示了如何定义这样的变量:

#define CONCAT(x,y) x ## y#define EXPAND(x,y) CONCAT(x,y)#define VARLINE(x) EXPAND(x, __LINE__)

现在,VARLINE(myvar)宏会展开为 varline123或 varline1337这样的包含行号的唯一变量名。第二行代码看起来可能有些冗余,但这是确保所有宏按正确顺序展开并得到预期结果的必要步骤。

这个图示可能更直观地展示了这个过程:

让 UDRL 调试(稍微)简单一点

通过这种在宏中创建唯一变量名的能力,我们可以定义一个 DLOGF()宏,它会自动展开成我们想要的形式。

#define DLOGF(format, ...) PIC_STRING(); DLOG()
LOGF("I reached here n");

现在确实会展开为以下两行代码:

PIC_STRING(mystring123, "I reached here n");DLOG(mystring123);

代码实现

由于 UDRL-VS 不是开源项目,我无法直接分享完整代码,但以下是我修改的代码片段。

我在 Utils.h中添加了上述宏定义,用于实现自定义的 DLOGF宏。该宏会在调用 DLOG之前自动处理 PIC_STRING相关操作:

#ifdef ENABLE_DEBUGSTRING// this uses some Preprocessor Macro magic to essentially be able to use dlog// with a PIC_STRING, which requires a new variable everytime, otherwise two invocations// of DLOGF() will result in a variable redefinition.// The solution here is to use __LINE__ to create a variable containing the line number,// ensuring that each PIC_STRING relies on a unique variable name#define CONCAT(x, y) × ## y#define EXPAND(x, y) CONCAT(x, y)#define VARLINE(x) EXPAND(x, __LINE__)// add the line number to x to create a unique name#define DLOG(format, ...) dlog (format, _VA_ARGS__)#define DLOGF(format, ...) PIC_STRING(VARLINE(myvar), format); DLOG(VARLINE(myvar), __VA_ARGS__)voiddlog(constchar* format, ...);#else#define DLOG(format, ...);#endif

dlog()函数与前面提到的 Sleepmask-VS 中的实现非常相似。不过在这里,我们需要调用 OutputDebugStringA,因此在运行时(在 Utils.cpp中)会这样解析:

#ifdef ENABLE_DEBUGSTRING#include"FunctionResolving.h"voiddlog(constchar* format, ...){ va_list arglist; va_start(arglist, format); char buff[1024]; typedefint(WINAPI* VSPRINTF_S)(char*, size_tconstchar*, va_list)typedefvoid(WINAPI* OUTPUTDEBUGSTRINGA)(LPCSTR); constexpr DWORD NTDLL_HASH = CompileTimeHash("ntdll.dll"); constexpr DWORD KERNEL32_HASH = CompileTimeHash("kernel32.dll"); constexpr DWORD sprintf_s_hash = CompileTimeHash("vsprintf_s"); constexpr DWORD OutputDebugStringA_hash = CompileTimeHash("OutputDebugStringA"); #ifdef _WIN64  _PPEB pebAddress = (_PPEB) _readgsqword (0x60) ; #elif _WIN32  _PPEB pebAddress = (_PPEB)__readfsdword(0x30); #endif VSPRINTF_S fnVsprintf_s = (VSPRINTF_S)GetProcAddressByHash(pebAddress, NTDLL_HASH, vsprintf_s_hash); OUTPUTDEBUGSTRINGA fnOutputDebugStringA = (OUTPUTDEBUGSTRINGA) GetProcAddressByHash(pebAddress, KERNEL32_HASH, OutputDebugStringA_hash); int len = fnVsprintf_s(buff, 1024, format, arglist); if (len > 0) {  fnOutputDebugStringA(buff); } va_end (arglist);}#endif ENABLE_DEBUGSTRING

结论

现在你可以在最终形态下运行你的 UDRL (User Defined Reflective Loader) 并通过 WinDbg 进行观察。当 UDRL 被注入到 Notepad 中时,打印出如下 3 条语句的效果如下:

DLOGF("Test DLOGF: 0x%pn", anInterestingAddress);

让 UDRL 调试(稍微)简单一点

我发现这种方法比在 Debug和 Release构建之间来回切换,并试图找出为什么某个功能在一个版本中工作而在另一个版本中不工作要方便得多!

原文始发于微信公众号(securitainment):让 UDRL 调试(稍微)简单一点

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年7月9日01:52:55
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   让 UDRL 调试(稍微)简单一点http://cn-sec.com/archives/4232751.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息