标题 | 简介 | 类型 | 公开时间 | ||||||||||
|
|||||||||||||
|
|||||||||||||
详情 | |||||||||||||
[SAFE-ID: JIWO-2024-3117] 作者: 大猪 发表于: [2022-06-05]
本文共 [472] 位读者顶过
第二篇 《AudioSrv音频服务故障》
起因最近换了HDMI显示器后,提示正在寻找音频设备,随后系统没声音了。右下角喇叭出现红叉,自动修复提示音频服务未响应,重装音频驱动也没用,系统是Windows 10 1803 64位。 多次尝试修复无果,于是打开调试器一探究竟。
寻找突破口从何处入手?这不由得让我想起《How to solve it》一书:问题是什么?之间的关联?有哪些已知线索? 系统的音频面板中列出了本机所有接口,操作一番发现设置为默认的功能不生效。 我意识到这可能跟音频服务无响应有关,于是决定从这个点入手。 可以看到设置默认选项是一个PopMenu,因此打算先找到该菜单的响应函数,进而分析后续的代码实现。打开procexp,查找该窗口对应的进程,发现是rundll32,如下: "C:windowssystem32rundll32.exe" Shell32.dll,Control_RunDLL mmsys.cpl,,sounds
mmsys.cpl是一个控制面板程序,CPL是PE文件,导出了CPlApplet函数,该函数是程序的逻辑入口,原型如下: __declspec(dllexport) long __stdcall CPlApplet(HWND hwndCPL,UINT uMsg,LPARAM lParam1,LPARAM lParam2);
为了找到PopMenu窗口的消息处理过程(WndProc),首先通过spy++找到菜单所属窗口的句柄wnd,接着写一段代码注入到rundll32进程中获取: LONG_PTR ptr = NULL;
HWND wnd = ***; //https://blogs.msdn.microsoft.com/oldnewthing/20031201-00/?p=41673 if (IsWindowUnicode(wnd))
ptr = GetWindowLongPtrW((HWND)wnd, GWLP_WNDPROC); else ptr = GetWindowLongPtrA((HWND)wnd, GWLP_WNDPROC);
找到WndProc是ntdll!NtdllDialogWndProc_W,接着就需要条件断点,PopMenu的菜单响应是WM_COMMAND(0x0111)消息,WndProc原型如下: LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg,
WPARAM wParam,
LPARAM lParam
);
可知rcx是窗口句柄,rdx是消息ID,因此设置条件断点如下: bp ntdll!NtdllDialogWndProc_W ".if(@rcx==句柄 and @rdx==0x0111){.printf "%x %xn",@rcx,@rdx;.echo}.else{gc}"
中断下来之后使用pc命令找到对应的调用,根据符号名能大致知道函数的功能,最后找到PolicyConfigHelper::SetDefaultEndpoint函数,调用栈如下所示: 00 audioses!PolicyConfigHelper::SetDefaultEndpoint 01 audioses!CPolicyConfigClient::SetDefaultEndpointForPolicy 02 mmsys!CEndpoint::MakeDefault 03 mmsys!CPageDevices::ProcessWindowMessage 04 mmsys!CDevicesPageRender::ProcessWindowMessage 05 mmsys!ATL::CDialogImplBaseT<ATL::CWindow>::DialogProc 06 atlthunk!AtlThunk_0x01 07 USER32!UserCallDlgProcCheckWow 08 USER32!DefDlgProcWorker 09 USER32!DefDlgProcW 10 ntdll!NtdllDialogWndProc_W
uf /c 查看audioses!PolicyConfigHelper::SetDefaultEndpoint调用函数如下: 0:000> uf /c audioses!PolicyConfigHelper::SetDefaultEndpoint
audioses!PolicyConfigHelper::SetDefaultEndpoint (00007ffc`6c3adc7c)
call to audioses!GetAudioServerBindingHandle (00007ffc`6c387be4)
call to RPCRT4!NdrClientCall3 (00007ffc`94e706f0)
call to audioses!FreeAudioServerBindingHandle (00007ffc`6c387b78)
call to audioses!WPP_SF_D (00007ffc`6c3643d4)
RPC调试方法查看GetAudioServerBindingHandle函数: audioses!GetAudioServerBindingHandle (00007fff`d2c07be4) call to RPCRT4!RpcStringBindingComposeW (00007ff8`07882e60)
call to RPCRT4!RpcBindingFromStringBindingW (00007ff8`0788d8b0) call to RPCRT4!RpcStringFreeW (00007ff8`0787ab40)
可知在连接RPC服务端,得到端口句柄。接下来的NdrClientCall3便是执行RPC客户端调用。 RPC全称Remote Procedure Call(远程过程调用),主要是实现客户端的函数在服务端上下文调用,对客户端来说像在调用本地函数一样,为此这里会涉及几个点: 函数原型一致 序列化/反序列化 同步异步 数据交换 内存分配 异常处理 注册发现 传输方式 ... 关于RPC,可以讲很多东西,因篇幅有限,我将重心放在Windows的RPC,同时讲一些调试技巧。对RPC感兴趣的可以去看看gRPC、brpc(有很多研究资料)、Thrift,以及一些序列化协议(pb、json、mp)等。 Windows对RPC使用无处不在,COM的跨进程通信便是用的RPC,还有许多服务都提供了RPC调用接口,例如LSA、NetLogon等等。 读者需要理清COM、RPC、LPC/ALPC之间的关联,这里可以分三个层次: COM -- ole*.dll、combase.dll RPC -- rpcrt4.dll LPC/ALPC -- ntdll!Zw*Port/ntdll!ZwAlpc*
COM在垮进程通信时会调用到RPC,RPC在本地调用时会用到LPC(本地过程调用Local Procedure Call)(也有可能是Socket/NamedPipe,大部分应该都是LPC,因为效率最高),LPC是NT旧时代的产物,Vista之后LPC升级成了ALPC,A是Advanced高级的意思,ALPC通信速度、安全性、代码规范,可伸缩性都有提升,这些概念可以参考Windows Internals。 大致了解这些概念之后,我们来通过调试讲解,这些调用关系从栈回溯能很清晰看到。 回到文中的问题,我们执行到RPC运行时这一层,RpcStringBindingComposeW函数,原型如下: RPC_STATUS RPC_ENTRY RpcStringBindingCompose(
TCHAR* ObjUuid,
TCHAR* ProtSeq,
TCHAR* NetworkAddr,
TCHAR* EndPoint,
TCHAR* Options,
TCHAR** StringBinding
);
关于调试RPC运行时这一层(RPC有文档、ALPC没文档化),建议参看MSDN https://docs.microsoft.com/en-us/windows/desktop/rpc/rpc-start-page ,写点代码,使用MIDL生成stub,然后查看NDR是如何序列化接口、参数等信息,也就是Marshall部分,以及RPC如何实现同步异步、内存分配、异常处理,理清各个结构,这对调试大有裨益。 接下来使用du rdx查看ProtSeq值是”ncalrpc”,说明使用的LPC,后续的NdrClientCall3会调入ALPC,因此来到ntdll!NtAlpcSendWaitReceivePort,查看栈如下: NtAlpcSendWaitReceivePort调入内核,然而在应用层仅通过这个函数提供的信息,我们并不能很快定位到服务端的调用。当然可以调试RPC运行时层(rpcrt4)来定位,不过走到这一步,我们接下来的重点是调试ALPC,因此不作考虑。 PrcessHacker里有不少逆向过的代码,直接查看NtAlpcSendWaitReceivePort原型下: NTSTATUS
NtAlpcSendWaitReceivePort(
__in HANDLE PortHandle,
__in ULONG Flags,
__in_opt PPORT_MESSAGE SendMessage,
__in_opt PALPC_MESSAGE_ATTRIBUTES SendMessageAttributes,
__inout_opt PPORT_MESSAGE ReceiveMessage,
__inout_opt PULONG BufferLength,
__inout_opt PALPC_MESSAGE_ATTRIBUTES ReceiveMessageAttributes,
__in_opt PLARGE_INTEGER Timeout
);
找到PortHandle,用livekd来查看ALPC Port信息: //启动livekd查看内核信息 livekd -k windbg路径 //根据进程ID找到rundll32的EPROCESS !process {进程ID} 0 //找到PortHandle对应的ALPC端口对象 !handle {PortHandle} 3 {EPROCESS} //查看ALPC对象信息 !alpc /p {AlpcPortObject}
通过ALPC信息可知ConnectionPort是AudioClientRpc。 继续查看ConnectionPort,可知Server端是svchost,并且开了两个IOCP Worker线程在处理ALPC通信,正处于Wait状态,同时能得到进程线程ID。 //切换到svchost进程地址空间 .process {svchost EPROCESS} //重新加载符号和模块 .reload //查看两个IOCP线程信息 !thread {ETHREAD} //正在等待IOCP,线程栈如下: nt!KiSwapContext+0x76 nt!KiSwapThread+0x501 nt!KiCommitThreadWait+0x13b nt!KeRemoveQueueEx+0x262 nt!IoRemoveIoCompletion+0x99 nt!NtWaitForWorkViaWorkerFactory+0x334 nt!KiSystemServiceCopyEnd+0x13 (TrapFrame @ ffffca89`c2661b00)
ntdll!NtWaitForWorkViaWorkerFactory+0x14 ntdll!TppWorkerThread+0x536 KERNEL32!BaseThreadInitThunk+0x14 ntdll!RtlUserThreadStart+0x21
另开windbg挂起对应的svchost.exe,在上述线程的ntdll!NtWaitForWorkViaWorkerFactory+0x14返回处下断点: ~~[1118] bp 7ff8`07dd6866
断下来后,使用wt跟踪,可大致知道调用关系,继续在NtAlpcSendWaitReceivePort下断点: ~~[1118] bp ntdll!NtAlpcSendWaitReceivePort
查看栈回溯,可知IOCP的Callback在处理ALPC调用: 面对频繁的ALPC调用,如何才能定位是我们的Client发过来的? 必然要找到Client和Server之间的关联,然后设置条件断点,那么关联在哪里? NtAlpcSendWaitReceivePort函数的参数ReceiveMessage是PPORT_MESSAGE,其包含MessageId和对端的进程线程ID,结构如下: //from: https://github.com/processhacker/processhacker/blob/master/phnt/include/ntlpcapi.h typedef struct _PORT_MESSAGE { union { struct { CSHORT DataLength;
CSHORT TotalLength;
} s1;
ULONG Length;
} u1; union { struct { CSHORT Type;
CSHORT DataInfoOffset;
} s2;
ULONG ZeroInit;
} u2; union {
CLIENT_ID ClientId; double DoNotUseThisField;
};
ULONG MessageId; union {
SIZE_T ClientViewSize; // only valid for LPC_CONNECTION_REQUEST messages ULONG CallbackId; // only valid for LPC_REQUEST messages };
} PORT_MESSAGE, *PPORT_MESSAGE;
通过结构体推算ClientId结构偏移是+0x08,ReceiveMessage是第5个参数(上一篇讲过如何获取x64的参数值) 可知rdi是ReceiveMessage,rdi是non volatile寄存器,因此设置条件断点: //Tips:先让Client执行到NdrClientCall时再启用断点,防止中断到其它RPC函数。
bp {NtAlpcSendWaitReceivePort调用后} ".if(poi(@rdi+8)=={Client进程ID} and poi(@rdi+10)=={Client线程ID}){}.else{gc}"
此时Client单步走过NdrClientCall,svchost会中断下来,由于这个RPC接口是阻塞的,因此Client会等到Server端的返回。 接下里就是进入rpcrt4运行时,执行各种反序列化、内存分配拷贝等操作,最后通过rpcrt4!Invoke进入真正的接口函数,通过调用栈一目了然。
调试音频服务找到Server端函数audiosrv!PolicyConfigSetDefaultEndpoint: RPCRT4!Invoke+0x70: 00007ff8`078e4410 41ffd2 call r10 {audiosrv!PolicyConfigSetDefaultEndpoint (00007fff`f81d2d10)}
取消Client所有断点,开始跟踪audiosrv!PolicyConfigSetDefaultEndpoint,该函数调用失败返回80070005h(拒绝访问)。 通过调试不难发现MMDevAPI!CSubEndpointDevice::SetRegValue调用失败(CFG导致截图看到的函数不直观,查看rax即可)。 然而根本原因是因为操作注册表失败,如下图所示:
参数信息如下:[出自:jiwo.org] !token查看当前token信息,使用Sysinternals的PsGetsid查看GroupOwner的sid对应NT ServiceAudiosrv。 NT ServiceAudiosrv
NT ServiceAudioEndpointBuilder
查看注册表键值,上面两个服务虚拟用户只有读取权限,删除自有权限,启用继承父键权限。Render键下还有几个设备也按同样的方式处理。
关于MMDev可参考: 再次调试,注册表操作成功,audiosrv!PolicyConfigSetDefaultEndpoint返回S_OK,重启AudioService服务,问题解决。
结束最后,每次解决问题后,应该反思每一个细节,目标是否明确,思路是否清晰,是否有更好的方式,不断总结优化。 例如对于这类问题,可从符号入手定位问题,可从RPC运行时(rpcrt4)这一层去分析,亦可在rpcrt4!Invoke的监视,等等。。。 Thanks for reading。 参考资料:
Google
MSDN
ProcessHacker
Windows Internals
Windbg Help WRK |