调试实战:记一次有教益的递归栈查看



前言


之前介绍了在windbg中如何查看非常深的调用栈 —— 使用kN命令指定栈帧数。kN虽好,但最多只能查看0xffff个栈帧。如果栈帧数量比0xffff还多,该如何查看呢?本文将介绍几种查看方法,在介绍查看方法前,需要对线程栈的特点有个基本的认识。





线程栈的关键特性


◆线程栈是从高向低扩展的,当向栈上push一个值的时候,栈底指针esp的值会减小。
◆函数返回地址会保存到栈上:

根据以上几点可以得到一个非常重要的结论:如果A调用了BB又调用了CC又调用了D。那么B返回到A的地址在线程栈的高处,C返回到B的地址在线程栈的低处,D返回到C的地址在线程栈的最低处。

有了以上的基本认识,就可以使用以下几种方法查看调用栈了。




查看方法



方法1:**使用.kframes设置默认显示的栈帧数量
增大默认显示数量,这样就可以一次性显示更多的调用栈

方法2:**使用dps,自己识别调用栈
可以灵活高效的从指定的位置开始查找

方法3:**使用k命令的时候指定StackPtr

可以从指定位置开始显示调用栈,不必从头开始显示。为了方便验证每种方法的可行性,我写了一个非常简单的递归调用测试程序,为了让调用栈可以更深一些,我修改了工程设置中的堆栈保留大小0x70800000(大概1.75 GB)。

示例代码

#include <iostream>

void Recursive(int depth)
{
if (depth > 0)
{
Recursive(--depth);
}
}

void CallRecursive()
{
Recursive(0x7fffffff);
}

int main()
{
CallRecursive();
}

测试工程可以到这里下载,接下来依次介绍每种查看方法。





方法1:使用.kframes设置默认显示的栈帧数量


windbg的帮助文档中发现可以通过.kframes命令来设置k命令默认显示的栈帧数量。但是也不是可以显示无限多个栈帧。

那么通过.kframes可以设置的最大栈帧数是多少呢?通过几次尝试,我发现.kframes可以接受的最大值是32位的带符号整数的最大值,也就是0x7fffffff(对应的十进制是2147483647)。

调试实战:记一次有教益的递归栈查看

但是,如果通过.kframes命令把栈帧数设置为0x7fffffff后,再执行k命令,发现windbg会直接提示内存分配失败。

调试实战:记一次有教益的递归栈查看

尝试把栈帧数设置为0x1000000,再执行k命令,发现windbg的内存占用非常高,高峰期大概消耗了20GB的物理内存(下图中的Working Set列),经过将近两分钟的努力,最终还是以内存分配失败告终。

调试实战:记一次有教益的递归栈查看

又试了几个更小的值,发现在我的机器上(24GB物理内存)设置为0x1000000时可以输出结果,但是因为数据量太大了,等了半个多小时也没执行完。
~
虽然这次.kframes没能成功,但这绝对是一个非常值得了解的命令,可以处理绝大多数情况下的线程栈查看问题。

优点:
1.操作非常简单
2.可以处理绝大多数情况

缺点:
1.会影响后续k命令默认显示效果(仅限当前调试会话,windbg重启后会自动失效)
2.当调用栈过深的时候,k命令可能会非常非常非常慢(对于示例程序,半个小时还没执行完)
3.内存占用可能会非常高(需要分配内存来显示对应的信息)
4.不能解决调用栈过深的问题(受到物理内存的限制)
5.很难找到一个合适的值(设置的太大,可能消耗过多的资源,运行慢;设置的太小,调用栈可能显示不全)





方法2:使用.kframes设置默认显示的栈帧数量

windbg中可以通过dps以指针长度为单位打印出指定内存范围的值,同时会输出匹配的符号。

操作步骤:
1.通过!teb指令找到栈顶(StackBase)的位置,然后减去一定的值(比如64kb)得到一个较低的地址A

2.执行dps A StackBase。如果输出结果中没有包含感兴趣的函数,可以减去一个更大的值,再次执行dps并查看输出结果,直到输出结果中包含感兴趣的函数为止。

3.根据dps的输出内容手动识别调用栈。

实战:
通过!teb命令获取栈顶位置(0000002a33800000)然后减去64KB0x10000,也可以换成其它值,一般情况下64KB足够了)得到地址0000002a337f0000,然后执行dps 0000002a337f0000 0000002a33800000。或者可以直接直接输入dps 0000002a33800000-0x10000 0000002a33800000

调试实战:记一次有教益的递归栈查看

根据dps的结果可知,已经包含了关键的递归函数 ——TestDeepRecursive!Recursive,可以根据此次dps的输出结果手动识别调用栈。拉到输出结果的最下方,可以看到输出结果如下图:

调试实战:记一次有教益的递归栈查看

从上图可以看到main函数,CallRecursive函数,Recursive函数。而且与vs中的调用栈完美匹配。

调试实战:记一次有教益的递归栈查看
说明:输出结果中极有可能包含很多无关的信息(比如上图中的黄色高亮部分),需要仔细甄别。

优点:
1.输出结果速度非常快
2.非常灵活,强大

缺点:
1.需要对线程栈有一定的认识;
2.需要人肉识别调用栈,有一定难度;
3.比较依赖调试符号,如果没有调试符号,只根据地址信息,很难找出关联关系;
4.容易出错,因为栈上的内容比较杂,可能包含很多无关的信息。




方法3:使用k命令的时候指定StackPtr


前两种方法都有各自的优缺点,可以在前两种方法的基础上使用本方法——使用k命令的时候指定正确的StackPtrwindbg会自动帮我们识别调用栈。

使用本方法时需要传递一个正确StackPtr(调试x64程序时需要传递rsp,调试x86程序时需要传递ebp,也叫BasePtr),也可以同时指定要显示的栈帧数量。

关于k命令的帮助文档可以参考下图(截取自windbg帮助文档):

调试实战:记一次有教益的递归栈查看

如果传递的StackPtr不对,那么输出结果很可能是错误的。比如,我使用一个错误的值执行k=0x0000002a337ff938输出结果如下:

调试实战:记一次有教益的递归栈查看

所以,传递一个正确的StackPtr是必须的。那么,该如何获取一个正确的StackPtr呢?有两个方法:

1、执行k命令的时候,最左侧那一列就是rspx86程序对应着ebp)。可以这样处理:先通过.kframes设置一个相对合理的值,然后执行k命令,等命令执行完,取最后一条输出结果的rsp的值,假设是00000029c3004040,然后执行k=00000029c3004040 3,就可以继续显示后续的三条调用栈了。重复此过程即可。

调试实战:记一次有教益的递归栈查看

2、在dps的输出结果中“猜”一个ebp或者rsp的值。说是猜,其实是有规律的。

调试实战:记一次有教益的递归栈查看

根据上图可知,一个合法的rsp的值是0x0000002a337ffbd0。在windbg中输入k=0x0000002a337ffbd0 0x100,可以得到下图完美的调用栈:

调试实战:记一次有教益的递归栈查看

优点:
1.输出效率高,只需要显示关心的栈帧即可
2.不用自己识别调用栈,可以像普通的k命令一样输出调用栈

缺点:
1.需要指定一个合法的StackPtr,不能随便指定
2.需要非常了解x86/x64程序的调用栈,这样才能比较快速准确的找到合法的StackPtr

所以,dps+k=StackPtr [FrameCount]是最高效,最优雅的解决方案。
说明:如果知道了一个合法的StackPtr,也可以先通过r rsp = StackPtr修改rsp寄存器的值,然后再执行k命令显示调用栈。但是这个方法有一个特别不好的地方,rsp会被修改,后续用到rsp寄存器的命令都会受影响。因此,不推荐使用。





总结


◆使用.kframes可以设置默认显示的栈帧数,可以突破默认最多显示0xffff个栈帧的限制,但是注意如果设置的值太大,会非常消耗内存;
dps可以按指针打印一系列的值,并且会显示匹配的符号。务必记住此命令,非常有用;
◆使用k命令时,可以指定StackPtr来从指定位置开始显示调用栈;
◆对于x86的程序,ebp保存了调用者的ebpebp+4的位置保存了返回地址。
◆对于x64的程序,rsp-8的位置保存了返回地址。

参考资料

https://devblogs.microsoft.com/oldnewthing/20130906-00/?p=3303
https://blog.aaronballman.com/2011/07/reconstructing-a-corrupted-stack-crawl/



调试实战:记一次有教益的递归栈查看


看雪ID:编程难

https://bbs.kanxue.com/user-home-873494.htm

*本文为看雪论坛优秀文章,由 编程难 原创,转载请注明来自看雪社区

调试实战:记一次有教益的递归栈查看



# 往期推荐

1、区块链智能合约逆向-合约创建-调用执行流程分析

2、在Windows平台使用VS2022的MSVC编译LLVM16

3、神挡杀神——揭开世界第一手游保护nProtect的神秘面纱

4、为什么在ASLR机制下DLL文件在不同进程中加载的基址相同

5、2022QWB final RDP

6、华为杯研究生国赛 adv_lua


调试实战:记一次有教益的递归栈查看



调试实战:记一次有教益的递归栈查看

球分享

调试实战:记一次有教益的递归栈查看

球点赞

调试实战:记一次有教益的递归栈查看

球在看



调试实战:记一次有教益的递归栈查看

点击阅读原文查看更多

原文始发于微信公众号(看雪学苑):调试实战:记一次有教益的递归栈查看

版权声明:admin 发表于 2024年2月2日 下午6:03。
转载请注明:调试实战:记一次有教益的递归栈查看 | CTF导航

相关文章