windbg调试(200)

64位寻址到16EB (1EB=1000TB)x64控制寻址范围16TB

ps: 退出指令 q 就好了。

dump:https://social.technet.microsoft.com/Forums/scriptcenter/zh-CN/e79d38d9-7360-41e5-8288-24f978877755/dump

vector: https://stackoverflow.com/questions/48938003/windbg-c-how-to-print-vector-contents
https://0cch.com/2014/08/16/dump-stle79a84vectorefbc8clistefbc8cmape79a84e4b889e4b8aawindbge8849ae69cac/

如果只想程序运行时的内存dump,直接从资源管理器;进程tab页上,选中程序右键,就有个产生dump选项。

debug工具一般都有debugger.chm这个文档可以参考
x64:

  1. 拖拽到windbg,lm查看模块状态
  2. 查看时间戳(lmt, 我自己喜欢lmt sm),找到对应pdb,设置pdb路径
  3. 重新加载pdb(.reload),运行分析命令(!analyze -v)
    一般崩溃会有exception,就查找这个字符串,就可以找到对应执行函数。(~N s切换到N号线程,主线程是0号,后面要加个s表示线程)
    主线程没有就切换到其它线程查找(~*e kb, 建议使用~* kb 这样会列出线程号和更多信息)

需要确认指令(需要实践):
托管代码,由托管器管理内存等,c++ clr工程就是。c#就是托管代码

1
2
3
4
5
6
7
8
9
!dumpheap 显示托管堆的信息
!dumpheap -stat 检查当前所有托管类型的统计信息
!dumpheap -type Person –stat 在堆中查找指定类型(person)对象,注意大小写敏感
!clrstack 显示调用栈
!clrstack -p 显示调用栈,同时显示参数
!clrstack 只显示托管代码,而kb只显示非托管代码
!dumpobj(!do) 显示一个对象的内容
!dumparray(!da) 显示数组
!DumpStackObjects(!dso) 当前线程对象分配过程

找到hang住线程:

1
2
3
!threads
~* e!clrstack
!syncblk

x32:
基本相同,但是当32位程序运行在64位系统的时候,产生的dump

  1. 先加载64位的扩展(.load wow64exts)
  2. 切换到32位模式(!sw)
  3. 然后才再像上面那样调试

源码条件语句

int类型的断点

1
bp `WindbgStudyDlg.cpp:198` ".if (poi(as)==3){.echo heheok} .else {.echo MyVar Acceptable; gc}"

官方只给了bp的例子,感觉确实只有它能条件断

  1. 必须用poi指定变量(这里我有个变量as,用这个指令才能指定它)
  2. .echo 输出
  3. gcGo from Conditional Breakpoint,从断点处继续执行
  4. 第二个echo会自动输出变量名

    字符类型的断点

    1
    2
    3
    as /mu ${/v:mystr} @@( MyNest->Nestin.Buffer );
    .block { r $t0 = $spat( "${mystr}", "*11" ); }
    .if( @$t0 != 1 ) { gc } ;
  • 使用: bp mytest!mytest::ttfun "$$<d:\\commands.txt"
  • 我试了很多遍,函数内部真心没找到指令,大多网上的和官网的都是指定函数参数,也就是说我要专门把监视字符放入一个函数的参数里面才行,不然完全没法断下来。
  1. MyNest->Nestin.Buffer是自己指定字符串,必须是字符指针,类就自己获取或指定到指针里面
  2. *11 匹配内容
  3. 上面的是文件格式,不用文件记得把"\"

地址

kb后产生的地址
依次是:ebp地址 返回地址 参数地址
参数地址默认是三个,但是无对应参数时显示的参数地址是随机的。只能根据函数参数来看地址

指令tips

bp 在.unload会消失 bu则不会,bu当模块变化时会自动偏移,bp不会
bl 罗列断点
bc 清除断点(跟bl上面的编号)
p (f10)
t (f11)
lm sm //按文件名排序
lm vm {module name}//查看模块详细信息,ocx加载老是没有完整模块名,只有通过这个来查看
lmv 罗列模块详细信息
lmf 罗列模块image path(终于找到了,这个指令,上次那个加载错误路径的模块,下次就可以用这个指令来解决了)

!sym noisy 符合老是加载不上时,用这个开启模式;这样加载符号过程就会罗列详细信息

有一个数组我知道地址
dq 0x2721aae0 把数组展现出来
dt 2721aaf0 et_commonctrl::tag_ETControlData来查看第二个数组信息
dt this m_arrScenarioSheets 查看成员变量m_arrScenarioSheets (如果要详细信息加-r)
dt -v std::vector,std::allocator > > 查看结构体占用字节数

符号tips

今天又测试了下,指定符号、源、exe后,lm居然没找到对应符号。。。

只好强制加载了。
后来发现居然要多个后缀才行
.reload /i simpledoc1.exe这样才行。。。

lm会全部显示,太多了,如果只想显示能加载符号的模块的话用:lml就可以了

栈破坏第一个例子


今天认真看了调试的第一个例子,做下记录,调试的是一个内存破坏的例子:
1、先用k命令来显示异常前的函数执行堆栈(后面跟个数字就表示显示几行, k 3:显示最后三行)

我喜欢用kpn。因为p会列举出函数的调用参数;n会对他们排号,这样方便我用frame来切换上下文作用域;(另外说下b会列举参数地址,反正我也不咋习惯看,就不用)

2、找到我方代码函数后,用.frame <number>的形式切换到函数的上下文里面。(这个 number 就是我上面说的n所列出的排号)

为何要切换呢?因为我要找变量呀!! 默认的X找的是全局变量,我要找局部变量就只好切进函数里去。
X Mytest!g* //在Mytest模块中找 g 开头的变量
切进去,我直接 X g_ 就可以找到

3、 X指令或dv,找出变量。

我喜欢X,虽然都差不多。找的的变量会自动给出此变量的地址

4、dt转换

拿到变量地址后,就 dt CAppInfo 0x0047d768 它会把此地址转换成设置的类型,并变量的成员内存一一的列举出来。
简单来说这就是个c式的强制转换,只是为了方便研究成员变量的有效性

5、dd查看成员变量内存

如上面所说,成员变量被全部列出来了,就可以一个一个查看成员变量的内存dd 0x72726f43。这样就判断有效和无效!


断点

bp ConsoleApplication1!main+4c 函数第4行
bp ConsoleApplication1.cpp:4
t 运行单步

k命令

kb 显示的参数 从左到右分别为 ebp, ret ,arg1, arg2

内存溢出

r eip 查看下一指令地址
u eip 查看下一指令的汇编代码

约定不一致

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef int(__cdecl *MYPROC)(DWORD dwOne, DWORD dwTwo);
VOID CallProc(MYPROC pProc);
int __cdecl wmain()
{
HMODULE hMod = LoadLibrary(L"Win32Dll.dll");
if (hMod)
{
MYPROC pProc = (MYPROC)GetProcAddress(hMod, "InitModule");
if (pProc)
}
}
//这里InitModule本阿里是个__stadcall的约定,由于获取时用 __cdecl。导致函数返回前就清理了栈,使得堆栈破坏。

自动错误

以前把程序放入windbg,指定pdb,然后运行。如果有空指针调用它会自动跳转到代码。
后来有几次测试,又不成功了。
今天我无意中发现,.frame <number>到错误的函数后,它又能自动加载跳转到代码了。

堆错误-使用未初始化状态

此类报错,会在加载后,给出问题代码的,如果有pdb则会直接指出源代码。先看windbg显示的:
ConsoleApplication1!wmain+0x87:
00265107 c7000a000000 mov dword ptr [eax],0Ah ds:002b:baadf00d=????????

eax 的值是baadfood 填充模式时分配成功但没初始化。释放填充是feeefeee

堆错误-堆句柄不匹配

1
2
3
4
hSmallHeap = GetProcessHeap();//默认堆
hLargeHeap = HeapCreate(0, 0, 0);//私有堆
_DPH_BLOCK_INFORMATION有个Heap会辨识堆句柄,以此来判断堆句柄是否匹配。

去找结构体,自己声明,还要自己定义(因为它定义了才会有符号,才能找到自己声明的那个结构体)

死锁情况

~*e kb 显示真进线程信息很有用 (~* s切线程 切进程要加s)

先看它们第一行信息,Unfrozen表示它们都在运行。

1
2
3
4
0 Id: 2384.f68 Suspend: 1 Teb: 7efdd000 Unfrozen//主进程
1 Id: 2384.14f4 Suspend: 1 Teb: 7efda000 Unfrozen//子线程
//这个id很有用,特别是小数点后面的数字代表EBP。
//后面的OwningThread上的值就是此数字。

从堆栈看,俩个线程都在NtWaitForSingleObject结束。找到执行函数RtlEnterCriticalSection的参数
在分别!cs 00d9a138 !cs 00d9a150(这个命令由于涉及到了内核对象,必须去服务器那边下符号)
发现这俩个临界区都锁住了,LOCKED LockCount = 0x1 WaiterWoken = No
而它们的OwningThread则正好分别是彼此,说明是互相锁住了。

锁中产生异常

临界区的OwningThread会被分配给一个找不到ID的地址(Windows异常模块接管),会导致接下来不会释放临界区。
且问题栈会有:DbgBreakPoint DbgUiRemoteBreakin

微软建议,封装个类,在析构函数进行解锁。(所以建议使用CCriticalSection而不是windows对象)

线程结束

当主线程结束工作者线程是,工作者线程正处于分配内存或释放内存的过程中。
在这些操作中,堆管理器通常会获得一个临界区。
当工作者线程被强行结束时,堆管理器将永远无法离开临界区。
意思就是说你terminal时正在开辟堆时(new),可能会导致堆管理器死锁。

DllMain的死锁问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
BOOL bRet=FALSE;
switch(fdwReason)
{
case DLL_PROCESS_ATTACH:
{
DWORD dwId=0;
HANDLE hThread=NULL;
hThread = CreateThread(NULL, 0, InitDllProc, NULL, 0, &dwId);
if(hThread)
{
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
bRet=TRUE;
}
}
break;
}
return bRet;
}
//如上此代码会陷入卡死状态。
  1. 进入dllmain后创建线程。

    当windows创建线程时,线程并不会自动从kernel32!BaseThreadStart开始执行;
    而是首先由APC分发器分发一个APC到新线程,然后作为apc进行初始化完成后才会执行线程。
    然而APC的分发有一个加载器锁来控制的,以此保护分发访问的冲突问题。
    然而对于DllMain来说,它本身就是动态库的加载和创建也会出现新线程进程,它本身也会用到APC管理器
    ps:apc分发和回收用的是同一个锁

  2. dllmain等待线程

    dllmain等待子线程的创建和设置事件。
    然而子线程等待dllmain的APC管理器锁解锁。
    如此就造成了死锁的情况。
    apc的死锁导致调试线程的注入发生中断,超过30秒后;调试线程会自动挂起进程中所有线程。

临界区的判断

1
2
3
4
5
6
+0x000 DebugInfo : 0x0085c560 _RTL_CRITICAL_SECTION_DEBUG
+0x004 LockCount : 0n-1
+0x008 RecursionCount : 0n0
+0x00c OwningThread : (null)
+0x010 LockSemaphore : (null)
+0x014 SpinCount : 0xfa0

dt CRITICAL_SECTION!cs都行。
当处理一个尚未被初始化的临界区,看的值都是随机的
当被删除的临界区,看到的值是零
过度释放的临界区,会导致程序挂起,LockCount < -1 || RscursionCount < 0

64位

64位运行32位时,加载WOW(windows on windows)子系统模块

Mini dump代码直接实例初始化处调用RunCrashHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
void CreateDumpFile(LPCTSTR lpstrDumpFilePathName, EXCEPTION_POINTERS *pException)
{
// 创建Dump文件
//
HANDLE hDumpFile = CreateFile(lpstrDumpFilePathName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
// Dump信息
//
MINIDUMP_EXCEPTION_INFORMATION dumpInfo;
dumpInfo.ExceptionPointers = pException;
dumpInfo.ThreadId = GetCurrentThreadId();
dumpInfo.ClientPointers = TRUE;
// 写入Dump文件内容
//
MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hDumpFile, MiniDumpNormal, &dumpInfo, NULL, NULL);
CloseHandle(hDumpFile);
}
LONG WINAPI UnhandledExceptionFilterEx(struct _EXCEPTION_POINTERS *pException)
{
TCHAR szMbsFile[MAX_PATH] = { 0 };
::GetModuleFileName(NULL, szMbsFile, MAX_PATH);
TCHAR* pFind = _tcsrchr(szMbsFile, _T('\\'));
if (pFind)
{
*(pFind + 1) = 0;
SYSTEMTIME Time = { 0 };
TCHAR szGuid[512] = { 0 };
GUID guid;
if (S_OK == ::CoCreateGuid(&guid))
{
_sntprintf_s(szGuid, sizeof(szGuid),
_T("%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X.dmp"),
guid.Data1, guid.Data2, guid.Data3,
guid.Data4[0], guid.Data4[1],
guid.Data4[2], guid.Data4[3],
guid.Data4[4], guid.Data4[5],
guid.Data4[6], guid.Data4[7]);
}
_tcscat_s(szMbsFile, MAX_PATH, szGuid );
CreateDumpFile(szMbsFile, pException);
}
// TODO: MiniDumpWriteDump
FatalAppExit(-1, _T("Fatal Error"));
return EXCEPTION_CONTINUE_SEARCH;
}
void RunCrashHandler()
{
SetUnhandledExceptionFilter(UnhandledExceptionFilterEx);
}

sobey的调试

以下是详细过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
0:028> .cxr
Resetting default scope
0:028> kn
# Child-SP RetAddr Call Site
00 000000f7`7912b898 00007fff`755d13ed ntdll!NtWaitForMultipleObjects+0xa
01 000000f7`7912b8a0 00007fff`772f132f KERNELBASE!WaitForMultipleObjectsEx+0xed
02 000000f7`7912bb80 00007fff`773f444b kernel32!WaitForMultipleObjects+0xf
03 000000f7`7912bbc0 00007fff`773f3e6f kernel32!WerpLaunchAeDebug+0x2373
04 000000f7`7912c130 00007fff`756b1c1f kernel32!WerpLaunchAeDebug+0x1d97
05 000000f7`7912c160 00007fff`782ff1b3 KERNELBASE!UnhandledExceptionFilter+0x23f
06 000000f7`7912c250 00007fff`782e1e26 ntdll!memset+0xaaf3
07 000000f7`7912c290 00007fff`782f349d ntdll!_C_specific_handler+0x96
08 000000f7`7912c300 00007fff`782b48d7 ntdll!_chkstk+0x9d
09 000000f7`7912c330 00007fff`782f262a ntdll!RtlRaiseException+0xf67
0a 000000f7`7912ca00 00000000`00000000 ntdll!KiUserExceptionDispatcher+0x3a
0:028> .frame /c 5
05 000000f7`7912c160 00007fff`782ff1b3 KERNELBASE!UnhandledExceptionFilter+0x23f
rax=000000000000005a rbx=0000000000000000 rcx=0000000000000003
rdx=000000f77912bcb0 rsi=0000000000000001 rdi=0000000000000000
rip=00007fff756b1c1f rsp=000000f77912c160 rbp=000000f77912fbd0
r8=0000000000001000 r9=0000000000000000 r10=0000000000000040
r11=0000000000000286 r12=ffffffffffffffff r13=0000000000000001
r14=000000f77912c2c0 r15=00007fff6c3f182c
iopl=0 nv up ei pl zr na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
KERNELBASE!UnhandledExceptionFilter+0x23f:
00007fff`756b1c1f 448bf8 mov r15d,eax
0:028> dq @rsp+0x48 L2
000000f7`7912c1a8 000000f7`7912c2c0 00000000`00000000
0:028> dq 000000f7`7912c2c0 L2
000000f7`7912c2c0 000000f7`7912cef0 000000f7`7912ca00
0:028> .cxr 000000f7`7912ca00
rax=000000f775ba1020 rbx=0000000000000000 rcx=000000f77912d170
rdx=0000000000000000 rsi=000000f7704aa750 rdi=000000f7010182a0
rip=0000000000000000 rsp=000000f77912d128 rbp=000000f77912db50
r8=ffffffffffffffff r9=0000000000000001 r10=0000000000000000
r11=000000f77912d170 r12=00000000000000b4 r13=000000f701013728
r14=000000f701011898 r15=0000000000000000
iopl=0 nv up ei pl nz na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010206
00000000`00000000 ?? ???
0:028> kn
*** Stack trace for last set context - .thread/.cxr resets it
# Child-SP RetAddr Call Site
00 000000f7`7912d128 00007fff`5474bcd3 0x0
01 000000f7`7912d130 00007fff`5474e32f ETHiveMLU!et_commonclass::_SEDL_ITEM_EXTENTION::~_SEDL_ITEM_EXTENTION+0x93a3
02 000000f7`7912d290 00007fff`54698dc0 ETHiveMLU!et_commonclass::_SEDL_ITEM_EXTENTION::~_SEDL_ITEM_EXTENTION+0xb9ff
03 000000f7`7912d380 00007fff`5469729e ETHiveMLU!CETHiveMLPlugin::InitInstance+0x80
04 000000f7`7912d4d0 000000f7`6a3cce5e ETHiveMLU!ETMLCreatePlug+0x7e
05 000000f7`7912d530 000000f7`6a3ccaea ETContainerMLU!CETComInterface::~CETComInterface+0x560e
06 000000f7`7912d7e0 000000f7`6a396357 ETContainerMLU!CETComInterface::~CETComInterface+0x529a
07 000000f7`7912d840 000000f7`6a36cdf5 ETContainerMLU!CETContainerManage::Init+0x7c7
08 000000f7`7912da80 00007fff`0cd1d661 ETContainerMLU!ETCOM_InitSystem+0x35
09 000000f7`7912dab0 00007fff`0cd1d280 0x7fff`0cd1d661
0a 000000f7`7912db60 00007fff`0cf4023f 0x7fff`0cd1d280
0b 000000f7`7912dbc0 00007fff`0cbc4f5c 0x7fff`0cf4023f
0c 000000f7`7912dc40 00007fff`0cbc46c1 0x7fff`0cbc4f5c
0d 000000f7`7912dc90 00007fff`0cbc4017 0x7fff`0cbc46c1
0e 000000f7`7912dcf0 00007fff`0cbc3eef 0x7fff`0cbc4017
0f 000000f7`7912dd60 00007fff`0cbc3e3f 0x7fff`0cbc3eef
10 000000f7`7912de10 00007fff`0cbc3935 0x7fff`0cbc3e3f
11 000000f7`7912def0 00007fff`0cbc16fc 0x7fff`0cbc3935
12 000000f7`7912df20 00007fff`0cbc14bf 0x7fff`0cbc16fc
13 000000f7`7912dfc0 00007fff`0cbc1416 0x7fff`0cbc14bf

前面的都好说,dq L2 ,dq是四字方式,L2是2个四字,所以就是8字。刚好是一个指针的大小

0x48是函数内部的一个栈变量,至于为啥我也不知道,反正这是死命令,记住就ok了。

// //