-
-
[旧帖]
[原创]两种插APC法实现 DLL注入
0.00雪花
-
发表于:
2010-6-6 13:20
8177
-
[旧帖] [原创]两种插APC法实现 DLL注入
0.00雪花
hi,各们前辈好。
小弟执迷看雪很久,对着逆向技术和WINDOWS内核编程有着痴迷的向往。
但因技术有限,一直以来都是在[新人交流区]默默无闻。
特写此文,希望看雪版主给以小弟前进学习的源动力,也希望各大牛拍砖,小弟感激不尽!
a)内核方式投递APC.
异步过程调用(APC)是NT异步处理体系结构中的一个基础部分。Alertable IO(告警IO)提供了更有效的异步通知形式,当IO请求完成后,一旦线程进入可告警状态,回调函数将会执行,也就是一个APC的过程.
线程进入告警状态时,内核将会检查线程的APC队列,如果队列中有APC,将会按FIFO方式依次执行。如果队列为空,线程将会挂起等待事件对象。以后的某个时刻,一旦APC进入队列,线程将会被唤醒执行APC.
投递APC是一个线程动作,最终由系统调用KiDeliverApc完成。所以,我们可以填充一个APC(KeInitializeapc,KeInsertQueueApc)插入到线程Alertable为TRUE的APC对列中。
任意一个DLL插入到进程执行的是用户空间代码,so,必定要使用LoadLibrayA加载才可访问用户地址空间。so这是个用户模式下的APC。用户模式可以传递(ULONG)LoadLibrayA地址,内核里就可使用这个地址作文章咯。
以下介绍下_KAPC_STATE结构,加深对APC队列的理解。
// _KTHREAD +0x034 ApcState : _KAPC_STATE
nt!_KAPC_STATE
+0x000 ApcListHead : [2] _LIST_ENTRY
+0x010 Process : Ptr32 _KPROCESS
+0x014 KernelApcInProgress : UChar
+0x015 KernelApcPending : UChar
+0x016 UserApcPending : Uchar
APC队列头ApcListHead是数组指针,1组是存入用户模式下APC队列,另一组是存在内核模块APC队列.
// 还有一个_KTHREAD +0x138 ApcStatePointer : [2] Ptr32 _KAPC_STATE结构。这个是APC队列状态点,简单地说是当APC队列乱套时,WINDOWS用以备份或恢复的。
InsertApcDll(..)函数讲解如下:
// 由PID得到EPROCESS结构地址
ntStatus = PsLookupProcessByProcessId(UlongToHandle(ulPid),&eProcess);
ASSERT_STATUS(ntStatus);
/*
线程头
nt!_KPROCESS
+0x000 Header : _DISPATCHER_HEADER
...
...
+0x050 ThreadListHead : _LIST_ENTRY (_ETHREAD)
*/
ulThreadHead = (ULONG)eProcess + 0x050;
ulThNextEntry = *(ULONG *)ulThreadHead;
// 遍历eProcess的线程
while(ulThNextEntry != ulThreadHead)
{
/*
lkd> dt _KTHREAD
nt!_KTHREAD
+0x000 Header : _DISPATCHER_HEADER
...
+0x1b0 ThreadListEntry : _LIST_ENTRY
EHREAD = ETHREAD - 0x1b0
*/
ulAlertablekThread = ulThNextEntry - 0x1b0;
/*
找到这个进程中Alertable是TRUE的线程
nt!_KTHREAD
+0x000 Header : _DISPATCHER_HEADER
...
+0x164 Alertable : UChar
呵呵,有种暴力的插APC注入方式,是不检测不这种状态的,
只要找到Alertable就把它设为TRUE。
或者原本UserApcPending为TRUE的的线程时,被设置为FALSE,这样是很隐藏的。
*/
if(*(char *)(ulAlertablekThread + 0x164))
{
// 找到时,将APC插入该线程的APC队列
InsertApcByUserMode(szDllFullPath,ulAlertablekThread,(ULONG)eProcess);
break;
}
ulThNextEntry = *(ULONG *)ulThNextEntry;
}
InsertApcByUserMode(…)
插入用户模式的APC。
最主要是用户模式的LoadLibraryA(..)函数的使用,如何把代码映射到用户空间。
// -----------这里相当于应用层调用LoadLibraryA(szDllFullPath);
// 分配用户模块地址空间。(PUCHAR)pMappedAddress + 0x14是唯一参数地址。
// 已在FillApc中分配,共个字节.
memset((PUCHAR)pMappedAddress + 0x14,0,300);
// 空间填充DLL名
memcpy((PUCHAR)pMappedAddress + 0x14, szDllFullPath,strlen(szDllFullPath));
plDataAddress = (ULONG*)((char*)pMappedAddress + 0x09);
*plDataAddress = ulMappedAddress + 0x14;
// FillApc裸函数分配APC空间并填充。
// --------------------------------------------------------------
// 使线程处于警告状态.
if(!*(char *)(uleThread + 0x4a))
*(char *)(uleThread + 0x4a) = TRUE;
或使用KAPC_STATE ks;(这个可在遍历线程时保存0
ks.UserApcPending = TRUE;插入的DLL就顺利执行了。
完整代码请看附件project.c
b)应用层方式插入APC.
使用DWORD QueueUserAPC(
PAPCFUNC pfnAPC, // APC function
HANDLE hThread, // handle to thread
ULONG_PTR dwData // APC function parameter)
函数可以在应用层插入DLL。比CreateRemoteThread强多了。且CreateRemoteThread
已泛滥成灾了,很多杀软都有所对策。
从流程上看QueueUserAPC直接转入了系统服务NtQueueApcThread从而利用KeInsertQueueApc向给出的目标线程的 APC队列插入一APC对象。倘若KiDeliverApc顺利的去构造apc环境并执行我们的代码那一切就OK了,只可惜没有那么顺利的事, ApcState中UserApcPending是否为TRUE有重要的影响,结果往往是你等到花儿都谢了你的代码还是没得到执行。在核心态往往不成问 题,自己动手赋值.
本例并没有等待UserApcPending = TRUE才执行,而是把所有线程全都QueueUserAPC。这样确实影响效率。解决方法是,使用目标线程调用 SleepEx(.,TRUE),然后QueueUserAPC插入DLL。
使用QueueUserAPC的内核实现方式与a)是一致的。呵呵,这看起来比a)更有优势。
看看吧。
应用层的InsertApcDll(..)函数如下。
hProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE,ulPid);
if(hProcess == NULL)
{
// 打开进程失败。
// 呵呵,一般系统进程是打不开的。可以使用内核的NtOpenProcess,
// 炉子的LzOpenProcess,LzOpenThread。
// 在这就不搞这上啦。
return bRet;
}
// 在进程空间中分配内存,存放Shellcode,就是DLL的进程空间.
// 此内存块可读可写.
PVOID pAlloc = VirtualAllocEx(…)
// DLL写入内存空间.
WriteProcessMemory(hProcess,pAlloc,(LPVOID)szFullDllPath,ulDllFileLen,&ulRet);
POSITION ps = cThreadId.GetHeadPosition();
while(ps != NULL)
{
ulCurThreadId = cThreadId.GetNext(ps);
// 打开进程失败。
// 呵呵,一般系统进程是打不开的。可以使用内核的NtOpenThread,
// 炉子的LzOpenProcess,LzOpenThread。
// 在这就不搞这上啦。
HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE,ulCurThreadId) ;
if(hThread != NULL)
{
//
// 注入DLL到指定进程
//
// 虽然是每个线程HANDLE都调用这函数。
// 但内核原理是和a)一致的,
// 只会查找UserApcPending = TRUE的线程插入。
QueueUserAPC((PAPCFUNC)LoadLibraryA,hThread,(ULONG_PTR)pAlloc);
}
}
``// 不要释放呀。进程退出时系统就释放了。
``// VirtualFreeEx(pAlloc)
完整代码请看附件。
c)总结。
更多的思路可以参考sudami的<N种内核注入DLL的思路及实现>.
我这并没有Hook ZwmapviewOfSection等函数。一是我对ShellCode不熟,
二是对Hook本身就是很暴力的行为,很多杀软都会检测到的,再说在兼容x64
下处理内核hook,就是自找麻烦,没事找事。X64下应尽量走应用层道路。
不过,像SetWindowsHook,CreateRemoteThread这些都是泛滥成灾了。
用起来也没意思了。呵呵。
以上是我个人对DLL注入的一些看法和思想。花了我星期五整天。唉郁闷。
此致,失望版主给以鼓励!小弟我感激不尽!
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)