首页
社区
课程
招聘
[原创]StackWalk64栈回溯原理解析
发表于: 2021-8-11 23:36 16471

[原创]StackWalk64栈回溯原理解析

2021-8-11 23:36
16471

StackWalk64是用于回溯栈的,32位和64位皆可。本次目标为StackWalk如何回溯32位程序的栈

注:

本次分析以dbghelp.dll,版本为10.0.18362.1139 (WinBuild.160101.0800)。

栈回溯大致流程

栈回溯使用StackWalk64函数,根据有无符号文件,会分别处理。无论有无符号,都会判断是否是回溯第一层栈。

因为一般情况下没有符号文件, 所以这里只重点分析无符号文件的情况,并在合适的时机提出有符号文件的处理流程。

DoDbhUnwind为栈回溯的真正起点,由StackWalk64到DoDbhUnwind的过程都可理解为包装。

重点如下:

回溯第一层栈使用UnwindInternalContextUsingEbp函数,该函数直接从ebp获取第一层栈的ebp和eip。

该函数实现如下:

待UnwindInternalContextUsingEbp返回后,DoUnwindUsingInternalContext函数也直接返回,表示第一层栈已回溯完毕。

注:本例只分析ebp回溯的情况,esp回溯的情况(等下周填充)

从第二层栈回溯开始,都需要走这样的流程。

SearchForReturnAddress获取ebp和eip是通过评定分数来确认的,分数最高的视为上一层的ebp,并从中获取本层的ebp和eip。

分数由ComputeScoreForReturnAddress获取(算法请参考下一小节),鉴定方式如下:

注:前三次base ebp不变,只是传给ComputeScoreForReturnAddress的其中一个参数会变(根据这个参数,获取的分数会不一样)

注:

DoDbhUnwind函数为回溯栈的核心,每执行一次DoDbhUnwind函数,代表回溯完一层栈。

上图为DoDbhUnwind的大致流程,重点如下:

nonFirstStackFlag(*(DWORD *)(a2 + 0x7C))的含义:

根据4.1节和4.2节的描述,我们知道第一层栈回溯是通过UnwindInternalContextUsingEbp实现的,第二层或之后的栈回溯是通过UnwindUsingPrologueSummary实现的,选择执行哪条分支是通过nonFirstStackFlag来判断的,如下图:

注:第二层或以上,为何要调用两次UnwindAndUpdateInternalContext将在之后下一小节讲解。

ImportPreviousFrameSummary函数

说明该函数之前,需要补充一点背景,如下:

从回溯第二层或以上的栈开始,都会调用UnwindAndUpdateInternalContext两次,每调用一次该函数,都能回溯一层栈。

可能大家会有疑问了,上文提及“每执行一次DoDbhUnwind函数,代表回溯完一层栈”,这里DoDbhUnwind调用两次UnwindAndUpdateInternalContext,那不应该是回溯了两层栈吗?

针对这个问题,请查看文末的附录1的实验,观察StackWalk64返回的CONTEXT(上下文)有何变化。

根据附录1的实验,能得出以下结论:

根据逆向分析结果,再补充以下结论:

在分析第二层或以上的栈时,context结构体的ebp和eip与待分析的函数差两层的目的是为了获取上一层的栈帧信息(比如Round2的FuncC栈层的信息),这些栈帧信息在内部使用,并反映在DbsStackUnwinder类的一些标志变量中。猜测这些变量会根据情况改变函数的分析流程(比如Round2的FuncD的分析流程)。在接下来的分析中,大家会看到栈回溯的整个过程用到了很多标志变量,来改变比如FuncD函数的分析路径。

有以上背景后,我们理解了在分析第二层或以上的栈时,为何会调用两次UnwindAndUpdateInternalContext了,接下来回到对ImportPreviousFrameSummary函数的说明。

在回溯第二层栈时(Round1),会像Round0一样先从函数A的ebp获取函数A的返回地址(UnwindInternalContextUsingEbp)。又因为只有nonFirstFlag为FALSE时,才会走UnwindInternalContextUsingEbp分支,所以ImportPreviousFrameSummary的其中一个作用就是在第二层栈回溯中,第一次调用UnwindAndUpdateInternalContext时,将nonFirstFlag置为FALSE(注意在第二层或之后的栈回溯,nonFirstFlag都为TRUE)。第二次调用UnwindAndUpdateInternalContext之前,nonFirstFlag会被恢复为TRUE。

回顾第四节的细节流程图,该函数是栈回溯真正功能的起始点。首先执行完初始化之后,会调用UnwindInternalContextUsingDiaFrame,去寻找返回地址(从context.ebp+4地址处获取)的符号文件,如果找到,则直接通过符号文件回溯栈,然后直接从UnwindAndUpdateInternalContext返回,完成该轮的栈回溯。如果没找到,则UnwindInternalContextUsingDiaFrame返回错误0x80004002,之后做无符号的栈回溯(通过ebp),如下图:

回顾4.2节的第二层栈回溯流程图,该函数先调用CollectFullFunctionInformation和FindStackStateOnFunctionEntry获取一些函数状态信息,之后调用SearchForReturnAddress,通过windows自定义的算法去寻找真正符合要求的返回地址。之后再调用SearchForFramePointer寻找framePointer(帧指针),不过这个过程往往是直接从ebp读取出新的ebp。

找到ebp和eip之后,将其保存到DbhStackServices类的0x388和0x38C偏移处。到这里,UnwindAndUpdateInternalContext就结束返回了,最后将DbhStackServices类获取到的eip交给STACKFRAME64结构体(StackWalk64提供的参数)。

如果在栈回溯时,遇到返回地址不属于任何模块的情况(比如执行的是一段shellcode),winXP和win10的行为如下:

win10:win10会判断一个标志位是否为0,如果为0,则忽略返回地址,ebp继续加4,然后读取ebp指向的内容,继续解析下一个(参考上一节的算法细节);如果为1,则继续解析该返回地址,即强制认为这个地址是有效的。

注:这个标志位在DbsX86StackUnwinder类的构造函数被置为1(写死的),初始化路径为StackWalk64->StackWalkEx->DoUnwindStackFrameUsingServices->New_DbsStackUnwinderhan->DbsX86StackUnwinder。

winXP:winXP直接视该返回地址无效,ebp继续加4,继续解析下一个。

debug版的函数通常会预留一部分栈空间,并初始化为0xCC,方便栈溢出的检测,而release版本没有。这个区别导致栈回溯会出现以下区别:

1.StackWalk64原型(详情可参考MSDN):

该函数有两个重要参数,StackFrame和ContextRecord,其中ContextRecord代表当前待分析的环境,重要的参数为ContextRecord.eip和ContextRecord.ebp。

2.StackWalk64的使用:

可知,一般StackWalk64都是循环使用的,直到frame64.AddrReturn.Offset为0,代表回溯完毕。

每循环一次,context的值会有对应的更新。

3.观察StackWalk64返回的CONTEXT(上下文):

当前的栈(由windbg打印)

程序输出结果

根据以下数据,有以下结论:

第一轮结束后(Round0)

注:StackWalk64回溯栈时,是根据context.ebp和context.eip来分析的。为了分析第二层栈,context.ebp和context.eip应该对应函数B才对,这样下次调用StackWalk64才能获取函数C是在哪调用函数B的,即fram64.eip。目前context.ebp和context.eip对应的是函数A。

第二轮结束后(Round1)

注:context的ebp和eip对应的是函数B,并不是函数C。与下次要获取的函数D差两层

观察之后的层数,其结果与Round1一样。context的ebp和eip对应的函数与下一次要回溯的函数差两层

本来由来:在栈回溯时,发现release版本的栈回溯会跳过一层函数,这层函数是jmp XXXXXXXX。为了明确何时会跳过函数,所以做了一系列分析,于是有了这篇文章。

winXP和win10在栈回溯的差异不大,大致流程基本一样(函数的封装有所变化,比如win10的DbsStackUnwinder::DoDbhUnwind函数在winXP是DbsStackUnwinder::DbhUnwind)。因此win7、win8各位也可以类推,找到其栈回溯的大致流程。

在栈回溯时,有符号与无符号的执行流程是有差别的。有符号的情况下,UnwindInternalContextUsingDiaFrame(win10下)会读取符号,解析符号,直接返回。如果要分析有符号的情况,各位重点查看这个函数即可(winXP下是DbsX86StackUnwinder::ApplyUnwindInfo)。

关于ComputeScoreForReturnAddress的算法,这里简化了参数验证等无关代码,流程上也做了简化。同时这里省略了一些不重要的细节,比如检验是否是热更新代码;检验call [addr A]的情况下,call的地址和addr A是否属于同一个模块。类似这些都属于常规检测,在栈回溯时情况基本相同,可暂时忽略。

64位栈回溯在无符号的情况下,会根据Exception Directory数据节的函数信息进行回溯。在分析winXP时,发现dbghelp.dll会缓存一份Exception Directory数据节进行分析,所以对分析模块的Exception Directory数据节下硬件断点,可能端不下来,找不到dbghelp.dll回溯的代码。虽然64位的分析流程和32位完全不同,但主流程是一样的,都会调用*Unwind函数,然后做一些基础检测,分析call、jmp这些基础操作。

 
 
 
 
0:000> k
 # ChildEBP RetAddr 
00 0019e98c 72f92d16 dbghelp!DbsX86StackUnwinder::UnwindInternalContextUsingEbp
01 0019e9ac 72f929ee dbghelp!DbsX86StackUnwinder::DoUnwindUsingInternalContext+0x169
02 0019ea14 72f92925 dbghelp!DbsX86StackUnwinder::UnwindAndUpdateInternalContext+0x7e
03 0019eb44 72f92e5a dbghelp!DbsStackUnwinder::DoDbhUnwind+0x179
04 0019eb74 72f925ea dbghelp!DbsStackUnwinder::DbhUnwind+0x59
05 0019ec90 72f91dd2 dbghelp!PickX86Walk+0x107
06 0019f94c 72f91ba8 dbghelp!DoUnwindStackFrameUsingServices+0xbb
07 0019f998 730a22c9 dbghelp!StackWalkEx+0x1b8
08 0019fb04 00404c1e dbghelp!StackWalk64+0x89
WARNING: Stack unwind information not available. Following frames may be wrong.
09 0019ff24 00406865 cpp_test_ano+0x4c1e
0a 0019ff70 76336359 cpp_test_ano+0x6865
0b 0019ff80 76fb8944 KERNEL32!BaseThreadInitThunk+0x19
0c 0019ffdc 76fb8914 ntdll!__RtlUserThreadStart+0x2f
0d 0019ffec 00000000 ntdll!_RtlUserThreadStart+0x1b
0:000> k
 # ChildEBP RetAddr 
00 0019e98c 72f92d16 dbghelp!DbsX86StackUnwinder::UnwindInternalContextUsingEbp
01 0019e9ac 72f929ee dbghelp!DbsX86StackUnwinder::DoUnwindUsingInternalContext+0x169
02 0019ea14 72f92925 dbghelp!DbsX86StackUnwinder::UnwindAndUpdateInternalContext+0x7e
03 0019eb44 72f92e5a dbghelp!DbsStackUnwinder::DoDbhUnwind+0x179
04 0019eb74 72f925ea dbghelp!DbsStackUnwinder::DbhUnwind+0x59
05 0019ec90 72f91dd2 dbghelp!PickX86Walk+0x107
06 0019f94c 72f91ba8 dbghelp!DoUnwindStackFrameUsingServices+0xbb
07 0019f998 730a22c9 dbghelp!StackWalkEx+0x1b8
08 0019fb04 00404c1e dbghelp!StackWalk64+0x89
WARNING: Stack unwind information not available. Following frames may be wrong.
09 0019ff24 00406865 cpp_test_ano+0x4c1e
0a 0019ff70 76336359 cpp_test_ano+0x6865
0b 0019ff80 76fb8944 KERNEL32!BaseThreadInitThunk+0x19
0c 0019ffdc 76fb8914 ntdll!__RtlUserThreadStart+0x2f
0d 0019ffec 00000000 ntdll!_RtlUserThreadStart+0x1b
 
BOOL DbsX86StackUnwinder::UnwindInternalContextUsingEbp(){
    BOOL result;
 
    this[0xE3] = *(PDWORD)((PBYTE)this[0xE2] + 4); // 0xE2偏移处保存上一层的ebp,0xE3偏移处保存上一层的eip
    this[0xE2] = *this[0xE2];
    if (read memory failed)
        result = TRUE;
    else
        result = FALSE;
 
    return result;
}
BOOL DbsX86StackUnwinder::UnwindInternalContextUsingEbp(){
    BOOL result;
 
    this[0xE3] = *(PDWORD)((PBYTE)this[0xE2] + 4); // 0xE2偏移处保存上一层的ebp,0xE3偏移处保存上一层的eip
    this[0xE2] = *this[0xE2];
    if (read memory failed)
        result = TRUE;
    else
        result = FALSE;
 
    return result;
}
 
 
 
 
 
 
 
// psudo code
int DbsX86HeuristicTool::SearchForReturnAddress(DWORD ebp){
 
    DWORD dwScore = 0;
    BYTE flag[3] = { 0 };
 
    for (int i =0 ;i < 0x42; i++) {
        if (i == 1)
            flag[1] = 1;
        else if (i == 2) {
            flag[1] = 0;
            flag[2] = 1;
        }
        else if (i > 2) {
            ebp = ebp + 4;
        }
 
        DWORD eip = *(DWORD*)(ebp+4);
        DWORD dwComputedScore = ComputeScoreForReturnAddress(eip, &flag);
        if (dwScore ==0xFFFF)
            return 0;
        if (dwComputedScore > dwScore)
            dwScore = dwComputedScore;
    }
 
    return dwScore != 0 ? 1 : 2;
}
 
 
DWORD DbsX86HeuristicTool::ComputeScoreForReturnAddress(DWORD eip, PBYTE pFlag){
    DWORD dwScore = 0;
    PVOID pImageBase = NULL;
    DWORD rs = FALSE;
 
    PBYTE content[8] = {0};
    DWORD dwbytesRead = 0;
    rs = ReadMemory(eip - 8, content, 8, &dwbytesRead);
    if (!rs) {
        // After some search, 0xC4C4 may be a invalid opcode.
        if (content[0] == 0xC4 && content[1] == 0xC4 && dwbytesRead==8)
            return 0xFFFF;
    }
 
    dwBytesRead = 0;
    memset(content, 0, 7);
    rs = ReadMemory(eip - 7, content, 7, &dwbytesRead);
 
    if (content[2]==0xE8) {
        //call imm16/imm32(E8 xxxx/E8 xxxxxxxx)
        rs = IsCodeReachableViaDirectCall(...);
        if (!rs)
            // IsCodeReachableViaDirectCall always returns FALSE
            if (pFlag[3] == 1)
                return 0x9000;
            else
                dwScore = 0x3000;
    }
 
    for (PBYTE pNow = content; pNow - content <7; pNow++) {
        if (pNow == 0xFF && (pNow[1] & 0x30 != 0x10)) {
            if (pNow - content == 1) {
                // call [mem16/mem32] -> FF15[xxxxxxxx]/FF15[xxxx]
            }
            else if (pNow - content == 5) {
                // call reg16/reg32   -> (FF xx)
            }
            else
                continue;
 
            if (pFlag[3] && dwScore <= 0x9000)
                dwScore = 0x9000;
            else if (pFlag[2] && dwScore <= 0xA000)
                dwScore = 0xA000;
 
            if (dwScore < 0x6000)
                dwScore = 0x6000;
 
            break;
        }
    }
 
    return dwScore;
}
// psudo code
int DbsX86HeuristicTool::SearchForReturnAddress(DWORD ebp){
 
    DWORD dwScore = 0;
    BYTE flag[3] = { 0 };
 
    for (int i =0 ;i < 0x42; i++) {
        if (i == 1)
            flag[1] = 1;
        else if (i == 2) {
            flag[1] = 0;
            flag[2] = 1;
        }
        else if (i > 2) {
            ebp = ebp + 4;
        }
 
        DWORD eip = *(DWORD*)(ebp+4);
        DWORD dwComputedScore = ComputeScoreForReturnAddress(eip, &flag);
        if (dwScore ==0xFFFF)
            return 0;
        if (dwComputedScore > dwScore)
            dwScore = dwComputedScore;
    }
 
    return dwScore != 0 ? 1 : 2;
}
 
 
DWORD DbsX86HeuristicTool::ComputeScoreForReturnAddress(DWORD eip, PBYTE pFlag){
    DWORD dwScore = 0;
    PVOID pImageBase = NULL;
    DWORD rs = FALSE;
 
    PBYTE content[8] = {0};
    DWORD dwbytesRead = 0;
    rs = ReadMemory(eip - 8, content, 8, &dwbytesRead);
    if (!rs) {
        // After some search, 0xC4C4 may be a invalid opcode.
        if (content[0] == 0xC4 && content[1] == 0xC4 && dwbytesRead==8)
            return 0xFFFF;
    }
 
    dwBytesRead = 0;
    memset(content, 0, 7);
    rs = ReadMemory(eip - 7, content, 7, &dwbytesRead);
 
    if (content[2]==0xE8) {
        //call imm16/imm32(E8 xxxx/E8 xxxxxxxx)
        rs = IsCodeReachableViaDirectCall(...);
        if (!rs)
            // IsCodeReachableViaDirectCall always returns FALSE
            if (pFlag[3] == 1)
                return 0x9000;
            else
                dwScore = 0x3000;
    }
 
    for (PBYTE pNow = content; pNow - content <7; pNow++) {
        if (pNow == 0xFF && (pNow[1] & 0x30 != 0x10)) {
            if (pNow - content == 1) {
                // call [mem16/mem32] -> FF15[xxxxxxxx]/FF15[xxxx]
            }
            else if (pNow - content == 5) {
                // call reg16/reg32   -> (FF xx)
            }
            else
                continue;
 
            if (pFlag[3] && dwScore <= 0x9000)
                dwScore = 0x9000;
            else if (pFlag[2] && dwScore <= 0xA000)
                dwScore = 0xA000;
 
            if (dwScore < 0x6000)
                dwScore = 0x6000;
 
            break;
        }
    }
 
    return dwScore;
}
BOOL IMAGEAPI StackWalk64(
  DWORD                            MachineType,
  HANDLE                           hProcess,
  HANDLE                           hThread,
  LPSTACKFRAME64                   StackFrame,
  PCONTEXT                         ContextRecord,
  PREAD_PROCESS_MEMORY_ROUTINE64   ReadMemoryRoutine,
  PFUNCTION_TABLE_ACCESS_ROUTINE64 FunctionTableAccessRoutine,
  PGET_MODULE_BASE_ROUTINE64       GetModuleBaseRoutine,
  PTRANSLATE_ADDRESS_ROUTINE64     TranslateAddress
);
BOOL IMAGEAPI StackWalk64(
  DWORD                            MachineType,

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

最后于 2021-8-12 10:25 被coneco编辑 ,原因:
收藏
免费 5
支持
分享
最新回复 (6)
雪    币: 4650
活跃值: (5286)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
2

dbghelp.dll中StackWalk64等符号相关函数的内部实现是diasdk库的静态链接,x86下貌似会对各种寄存器进行各种调用分析来决定下一帧,而且遇到一些汇编编译的代码还会分析错误而返回错误的结果,如使用了LuaJIT的程序,而且有符号和无符号的返回结果还可能存在差异。这个库目前还不完美,微软也在不断地升级更新。x64时就简单多了,不会出错。 下面是两个分析出错的例子。

 1.使用了luaJIT,导致返回调用堆栈出错 

 2.当c++代码编译为 x86时,运行时发生死锁或者win32异常时,vs2017 和 vs2019 无法显示完整的调用堆栈。 https://developercommunity.visualstudio.com/t/vs2017-and-vs2019-cant-show-full-call-s/1182801?port=1027&fsid=25582dbc-025e-4385-8030-499e0f49f28b&entry=myfeedback&ref=native&refTime=1628699401741&refUserId=de2ab958-7e07-4345-8566-53bf5ff7c32d

最后于 2021-8-12 00:49 被sonyps编辑 ,原因:
2021-8-12 00:47
0
雪    币: 1230
活跃值: (1770)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
3
好文章,谢谢分享.. 看到这个老是想起异常中的展开。
2021-8-12 10:11
0
雪    币: 5029
活跃值: (4798)
能力值: ( LV10,RANK:171 )
在线值:
发帖
回帖
粉丝
4
sonyps dbghelp.dll中StackWalk64等符号相关函数的内部实现是diasdk库的静态链接,x86下貌似会对各种寄存器进行各种调用分析来决定下一帧,而且遇到一些汇编编译的代码还会分析错误而返回错 ...

1.从附件中,可以知道栈回溯需要的寄存器是esp,ebp,eip
2.汇编编译的代码,因为prolog和epilog可能不规则,在没有符号文件的情况下,通过ebp寻找下一层会出错是很有可能的,详细原因可查看5.5小节
3.有符号和无符号在栈回溯时会走不同的分支,详情可见第4小节

另外关于你说的第二点,你可以尝试用StackWalk64或者windbg去查看堆栈,不知是否能看到正确的栈

最后于 2021-8-19 10:11 被coneco编辑 ,原因:
2021-8-19 10:11
0
雪    币: 4650
活跃值: (5286)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
5
coneco sonyps dbghelp.dll中StackWalk64等符号相关函数的内部实现是diasdk库的静态链接,x86下貌似会对各种寄存器进行各种 ...


另外关于你说的第二点,你可以尝试用StackWalk64或者windbg去查看堆栈,不知是否能看到正确的栈

windbg及dbghelp的StackWalk64函数能正常显示调用堆栈,可是VS2017及VS2019中却不能,我是想VS能完美点,毕竟这个问题在之前的VS版本中是不存在的。


感谢热心回复。

2021-8-20 01:08
0
雪    币: 287
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
6

其他线程在对某一个线程StackWalk64,需不需SuspendThread要这个线程让它挂起?

最后于 2021-9-30 11:26 被mb_hgrbqfun编辑 ,原因:
2021-9-30 11:24
0
雪    币: 5029
活跃值: (4798)
能力值: ( LV10,RANK:171 )
在线值:
发帖
回帖
粉丝
7
需要的,不管是32位还是64位,都会从栈获取信息,比如eip(rip)。因此需要挂起目标线程,获取栈上的内容。如果线程在跑,那栈上的数据就可能被修改。
2021-9-30 12:36
0
游客
登录 | 注册 方可回帖
返回
//