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注入的一些看法和思想。花了我星期五整天。唉郁闷。

此致,失望版主给以鼓励!小弟我感激不尽!

上传的附件 两种插APC法实现 DLL注入.rar