前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >驱动里执行应用层代码之KeUserModeCallBack,支持64位win7(包括WOW64)

驱动里执行应用层代码之KeUserModeCallBack,支持64位win7(包括WOW64)

作者头像
战神伽罗
发布2019-11-19 15:00:11
2.1K0
发布2019-11-19 15:00:11
举报

在驱动层(ring0)里执行应用层(ring3)代码,这是个老生常谈的技术,而且方法也挺多。

这种技术的本质:其实就是想方设法在驱动层里把应用层代码弄到应用层去执行。

比如在APC异步调用中,KeInsertQueueApc,KeInitializeApc等函数中可设置一个在ring3层执行一个回调函数,这样就可以回到应用层去执行代码了。

再比如在驱动中查找某个进程的一个线程,然后挂起它,把他的EIP指向需要执行的一段代码(把驱动层需要注入的这段代码叫ShellCodde),

执行完之后再回到线程原来的地方继续执行。

或者HOOK某些经常被调用的系统函数,比如NtCreateThread等,然后把ShellCode注入到当前进程去执行。

方法不下七八种之多。

无非就是在驱动层里主动把ShellCode注入到某个进程执行,或者被动的当某个进程进入驱动之后,然后调用ShellCode。

虽然办法挺多,但是由于出现得比较早,而且又不是微软提倡的,也没得到微软的支持,所以兼容性很差。

往往大部分办法只对WINXP支持得挺好,到了win7之后就会出现各种各样的问题,尤其是 64位的 win7系统,能用的办法就非常少了。

我没试过上边提到的办法能不能在64位win7是否成功,一开始接触这个问题的时候,使用的是 KeUserModeCallBack。

使用它是因为这函数虽然没被微软文档化,但是过了10多年,它的接口都不曾变化过,而且被windows内部大量使用。

KeUserModeCallback函数原型如下:

代码语言:javascript
复制
NTSTATUS KeUserModeCallback (
       IN ULONG ApiNumber,
       IN PVOID   InputBuffer,
       IN ULONG InputLength,
       OUT PVOID *OutputBuffer,
       IN PULONG OutputLength
       );

在KeUserModeCallback里,调用 KiServiceExit进入到ring3,在应用层接着调用KiUserCallbackDispatcher,

这个函数里,会通过传递的ApiNumber,计算出应用层回调函数地址,然后调用这个回调函数.

计算公式是 FuncAddr= KernelCallbackTable + ApiNumber*sizeof(PVOID); //同样适用64位系统.

KernelCallbackTable 存储回调函数基地址,非GUI进程KernelCallbackTable为NULL。

回调函数的第一个参数是 KeUserModeCallback的第二个参数InputBuffer, 回调函数的第二个参数是InputLength。

回调函数调用完成之后,通过触发int 2B调用KiCallbackReturn再次进入到内核,最后从 KeUserModeCallback 返回。

以下演示了如何使用这个函数的伪代码:

代码语言:javascript
复制

 struct USERDATA
{
   ........
};
NTSTATUS WINAPI  UserCallback(PVOID Arguments, ULONG ArgumentLength)
{
      USERDATA* user = (USERDATA*)Arguments;
      ....//实现在应用层调用的代码
      return STATUS_SUCCESS;
}
void UserCallbackEnd(){}

 //分配内存,KeUserModeCallback 第一个参数是 ULONG, 所以 64位系统的分配策略从基址开始寻找 4G范围内的空闲空间
 static NTSTATUS getProcessMemory(HANDLE proc_handle,PVOID baseAddr, PVOID* ppMem, SIZE_T* pSize)
 {
     NTSTATUS status = STATUS_UNSUCCESSFUL;
 #ifdef _WIN64
     const ULONG COUNT = 1000; const ULONG SepSize = 1024 * 1024 * 3 / 2; const ULONG_PTR Base = 1024 * 1024 * 50;
     ULONG i;
     for (i = 0; i < COUNT; ++i){
         ULONG_PTR pMem = (ULONG_PTR)baseAddr + Base + i*SepSize;
         SIZE_T size = *pSize;
         status = ZwAllocateVirtualMemory(proc_handle, (PVOID*)&pMem, 0, &size, MEM_COMMIT | MEM_RESERVE , PAGE_EXECUTE_READWRITE);
         if (NT_SUCCESS(status)){
             *pSize = size;
             *ppMem = (PVOID)pMem;
             break;
         }
     }
 #else
     status = ZwAllocateVirtualMemory(proc_handle, ppMem, 0, pSize, MEM_COMMIT | MEM_RESERVE | MEM_TOP_DOWN, PAGE_EXECUTE_READWRITE);
 #endif
     return status;
 }
// KeUserModeCallback一定是在用户进程线程上下文环境中才能执行成功,为了保证KernelCallbackTable不为空,必须是加载user32.dll的GUI进程。
void CallKeUserModeCallback()
{
        PVOID pMem = NULL;
        PROCESS_BASIC_INFORMATION    pbi;
        PVOID KernelCallbackTable;
        ULONG ApiNumber;
         HANDLE proc_handle = NtCurrentProcess();
         //
        ZwQueryInformationProcess( proc_handle, ProcessBasicInformation, &pbi, sizeof(PROCESS_BASIC_INFORMATION), NULL);
        KernelCallbackTable = pbi.PebBaseAddress->KernelCallbackTable; //获得用户层回调函数基地址
        ////// 为当前进程分配一段用户空间内存,目的是为了把回调函数UserCallback, 以及回调函数需要用到的参数, 复制到用户空间内存中。
        ////// ApiNumber的计算办法 ApiNumber = (((ULONG_PTR)pMem - (ULONG_PTR)KernelCallbackTable) / sizeof(ULONG_PTR));
        //////因为ApiNumber是ULONG类型, 可以看出,对于64位系统pMem和KernelCallbackTable的差值不能超过4G范围,否则计算出的ApiNumber就是错误的。
       getProcessMemory(proc_handle, KernelCallbackTable, &pMem, &size);
       ApiNumber = (((ULONG_PTR)pMem - (ULONG_PTR)KernelCallbackTable) / sizeof(ULONG_PTR));
       PVOID ShellCodeAddr = (PVOID)((ULONG_PTR)pMem + sizeof(ULONG_PTR));
       ULONG ShellCodeSize = (ULONG_PTR)UserCallbackEnd - (ULONG_PTR)UserCallback;
       *(ULONG_PTR*)pMem = (ULONG_PTR)UserCallback;   /// 等同 KernelCallbackTable[ApiNumber] = UserCallback;
 
         USERDATA ud; //初始化用户栈结构,这个结构会被传递给UserCallback函数
        ....
        PVOID OutBuffer; ULONG OutLen;
         ////调用KeUserModeCallback, 直到用户层的UserCallback函数调用完成之后才返回。
         KeUserModeCallback( ApiNumber, &ud, sizeof(USERDATA), &OutBuffer, &OutLen);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

调用 KeUserModeCallback有个最大的限制,

他必须在 用户GUI进程的线程上下文环境中被调用才能成功,

简单的说吧,如果你在DriverEntry中或在PsCreateSystemThread 创建的线程中调用 KeUserModeCallback ,会失败。

因为他们都没有用户堆栈空间。而且为了保证KernelCallbackTable不为空,进程必须是调用user32.dll的GUI进程。

windows大部分都是调用user32.dll的进程,这个条件不难满足。

关键是如何进入到某个进程的执行上下文环境中。

一开始想到的就是 PsSetCreateProcessNotifyRoutine 和 PsSetCreateThreadNotifyRoutine。

这两个函数设置的回调函数,确实能进入到被创建的进程上下文环境中,但是在win7下,

KeUserModeCallback调用更加严格,他只能运行在 PASSIVE_LEVEL级别,同时是 APC Enables的状态。

否则就会蓝屏,条件可参考

http://thisissecurity.net/2014/04/08/how-to-run-userland-code-from-the-kernel-on-windows/

上边有KeUserModeCallback函数的详细阐述。

得另想办法来进入用户进程上下文环境,一个比较通用,而且几乎所有用户进程都会进入的就是文件过滤驱动。

在文件过滤驱动的IRP_MJ_CREATE派遣函数中,能确保处于PASSIVE_LEVEL和APC Enables状态。

可以直接使用minifilter驱动。如下:

代码语言:javascript
复制

 PFLT_FILTER gFilterHandle;
FLT_PREOP_CALLBACK_STATUS
 NPPreCreate(
 __inout PFLT_CALLBACK_DATA Data,
 __in PCFLT_RELATED_OBJECTS FltObjects,
 __deref_out_opt PVOID *CompletionContext
 )
 {
     FLT_PREOP_CALLBACK_STATUS retStatus = FLT_PREOP_SUCCESS_NO_CALLBACK;
     NTSTATUS status;
     /////已经进入到某个进程的上下文执行环境中
     cbk_execute();  /////////////////////  在这里调用KeUserModeCallback函数。
     ////
     return retStatus;
 }
//  operation registration
 const FLT_OPERATION_REGISTRATION Callbacks[] = {
     { IRP_MJ_CREATE,
     0,
     NPPreCreate,
     0},
    { IRP_MJ_OPERATION_END }
 };
//  This defines what we want to filter with FltMgr
 const FLT_REGISTRATION FilterRegistration = {
    sizeof(FLT_REGISTRATION),           //  Size
     FLT_REGISTRATION_VERSION,           //  Version
     0,                                  //  Flags
    NULL,                               //  Context
     Callbacks,                          //  Operation callbacks
    NPUnload,                           //  MiniFilterUnload
    NULL,                                //  InstanceSetup
     NULL,                                //  InstanceQueryTeardown
     NULL,                                //  InstanceTeardownStart
     NULL,                                //  InstanceTeardownComplete
    NULL,                               //  GenerateFileName
     NULL,                               //  GenerateDestinationFileName
     NULL                                //  NormalizeNameComponent
};
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING reg)
{
    status = FltRegisterFilter(DriverObject, &FilterRegistration, &gFilterHandle );
     if (NT_SUCCESS(status)){
         ///
         status = FltStartFiltering( gFilterHandle);
         if (!NT_SUCCESS(status)){
             FltUnregisterFilter(gFilterHandle);
             return status;
         }
    }
    。。。。
    return   status;
}

最后说说这种技术有什么作用。

其实如果从一般的角度去理解,非常简单的就可以在应用层执行一段代码,何必那么麻烦非要从驱动里执行一段用户层代码。

所以好像是没什么用处。确实,一般在开发驱动程序中,都是开发配套的应用层程序跟驱动通讯来处理事务。

既然有了配套的应用层程序,就没必要再驱动中执行用户层代码了。

可是从另一个角度去想,驱动中执行一段用户层代码,没有看得见的程序(其实是寄宿在某个系统进程比如explorer,winlogon等)参与其中。

用户就很难发现有什么玩意执行过。

所以这个可以为某些木马或者恶意软件提供一个方便之门。比如像360tray.exe这种顽固的连ARK工具都无法结束的进程,

可以在驱动里把一个DLL注入到 360tray.exe里,然后再DLL里调用ExitProcess把自己给束掉

(这个办法也许有效,也许也没效,有兴趣的可以试试)。

也为某些人开发了驱动和DLL,就是不想开发EXE应用程序,而且也不想使用svchost来启动这个DLL,提供了一个另外的途径。

源代码下载地址:

http://download.csdn.net/detail/fanxiushu/7681759

补充:

以上讨论的都是执行原生程序的情况,也就是32位系统执行32位程序,64位系统执行64位程序。

但是在64位系统有个特殊的情况,即64位系统执行32位程序。

现在要补充说明的,就是如何在 win7 64位系统中,在驱动中调用KeUserModCallback函数,把代码注入到 32位进程去执行。

首先简单说说32位进程如何在64位系统中运行。

微软在用户模式实现了一个叫WOW64的子系统,用来为32位进程提供32位的模拟环境。

WOW64作为ntdll.dll和内核之间的一个层,它起到了欺上瞒下的作用, 在32位进程来看,他们愉快的以为是运行在32位系统中,

但是对内核来说,他们却以为上边运行的是64位进程。

WOW64是由三个动态库来实现,

wow64.dll 实现核心部分

wow64win.dll 实现一些我不知道的功能,

wow64cpu.dll 实现CPU在 32位和64位模式之间转换。

接着再看看KeUserModeCallback在32位进程和64位进程的处理有何不同,其实并没有本质的区别。

KeUserModeCallback返回到应用层之后,都会调用ntdll里边的KiUserCallbackDispatcher 函数,然后KiUserCallbackDispatcher根据ApiNumber

找到我们设置的回调函数UserCallback。因此,不管是32位进程和64位进程,我们设置的 UserCallback 函数都会被调用。

但是有点必须注意,那就是 UserCallback函数 不管是32位进程中,还是64位进程中,他都处于CPU是64位的执行环境中。

对于64位进程,这没有任何问题,所以可以获得某个模块的API函数,然后执行之。

但是对于32位进程,这就是大问题了,32位进程处于 32位CPU模式,所以32位进程中,我们无法调用跟32位模块相关的任何函数,

这样做的结果就只有一个,32位进程崩溃,然后KeUserModeCallback永远无法返回。

那如果我们想办法让我们的UserCallback函数进入到 CPU 32位模式,不就可以执行任何函数了吗?

事实确实如此,可是关键如何进入呢? 这就是问题所在。

因为我并不熟悉WOW64的CPU模式切换过程(或者说我并不熟悉CPU的模式切换),所以想自己写代码把UserCallback切换到32位模式,是无能为力了。

好在通过查看WOW64的三个动态库的导出函数,发现wow64.dll中有个函数 Wow64KiUserCallbackDispatcher,

跟ntdll.dll导出的 KiUserCallbackDispatcher是那么相近,就多了一个Wow64前缀。

猜想应该是在32位模式中执行回调函数。通过查找各种资料,证实了我的猜测。此函数声明如下,因为不是文档化的函数,未来极有可能被微软修改:

void NTAPI Wow64KiUserCallbackDispatcher( OUT PCONTEXT Contex, ULONG Wow64_APiNumber, PVOID Arguments, ULONG ArgumentLength);

最后两个参数就是系统传递给我们的UserCallback 函数的参数,也是 KeUserModeCallback函数的第二个和第三个参数。

第一个参数是个CONTEXT,就是线程上下文环境,测试发现,我们只需分配一块CONTEXT内存传递进去,无需填写任何参数。

第二个参数Wow64_APiNumber 是干嘛的呢? 他的作用跟 ApiNumber一样,只是他是在32位环境中,

因为32位进程都两个进程环境块,一个 PEB64,一个 PEB32。

PEB32中同样有个 KernelCallbackTable 基址,他是所有32位回调函数的基地址。

PEB32如何获得呢? 使用 PPEB32 PsGetProcessWow64Process(PEPROCESS ep); 又是一个未文档化的函数。

我们首先要做个32位环境的UserCallback32,然后写到32位进程空闲内存中,然后计算 Wow64_ApiNumber,

Wow64_ApiNumber = ((ULONG)pMem32 - (ULONG)KernelCallbackTable32) / sizeof(ULONG);

至于如何制作32位UserCallback32,其实就是如何制作ShellCode,网上有介绍如何制作。

简单的说就是编译一个包含UserCallback32函数的程序,然后把这个函数当成数组写到某个文件中,然后就是纯数组形式的ShellCode了。

有了这些准备,就可以在我们的UserCallback64中调用 Wow64KiUserCallbackDispatcher , 这样WOW64子系统自动帮我们切换到 32位环境,

然后执行我们的32位的Usercallback32函数, 然后返回,最后回到KeUserModeCallback 。

看起来的伪代码如下:

代码语言:javascript
复制
struct USERDATA
{
      WOW64Func  Wow64KiUserCallbackDispatcher;
      PCONTEXT   pContext;
      ULONG         Wow64_ApiNumber;
      ............
      其他参数,提供给32环境的UserCallback32使用
};
NTSTATUS WINAPI  UserCallback64 (PVOID Arguments, ULONG ArgumentLength)
{
      USERDATA* user = (USERDATA*)Arguments;
      ///此函数帮我们转换到32位模式,并且执行32位环境中的 回调函数
      user-> Wow64KiUserCallbackDispatcher( user->pContext, user->Wow64_ApiNumber,  Arguments,  ArgumentLength );
      .....
      return STATUS_SUCCESS;
}
void UserCallbackEnd(){}

 ///下边的函数,需要在32位编译环境中制作出 ShellCode数组,因为64编译环境无法编译出32位代码来。
NTSTATUS WINAPI  UserCallback32 (PVOID Arguments, ULONG ArgumentLength)
{
       USERDATA* user = (USERDATA*)Arguments;
       ........执行32位环境的代码
       return  STATUS_SUCCESS;
}
void CallKeUserModeCallbackWow64()
{

获取peb64; 获取 KernelCallbackTable64

获取peb32;获取 KernelCallbackTable32

给当前32位进程分配内存pMem64,用来存储 UserCallback64代码,CONTEXT等

给当前进程分配内存pMem32, 用来存储UserCallback32及其一些参数等

计算ApiNumber64,提供给 KeUserModeCallback用,

计算Wow64_ApiNumb32,提供给 Wow64KiUserCallbackDispatcher用

遍历 peb64的所有模块,找到wow64.dll,并且获取导出函数Wow64KiUserCallbackDispatcher地址,

(至于如何获得这些信息,可参考我提供的愿代码,除了WOW64部分未提供外,其他都是全的)

遍历peb32的所有模块,找到需要执行某个API函数的模块,然后获取此API函数在32位环境中的地址。

USERDATA user;

user.Wow64KiUserCallbackDispatcher = 获得的Wow64KiUserCallbackDispatcher地址,

user.pContext = pMem64指向的其中一部分内存

user.Wow64_ApiNumber=Wow64_ApiNumber32;

.....初始化其他参数

这个user 最终会被KeUserModeCallback传递到 我们在32位环境中设置的UserCallback32函数里。

代码语言:javascript
复制
       KeUserModeCallback( ApiNumber64 , &user, sizeof(USERDATA),  &OutBuffer, &OutLen);
}

挺麻烦的WOW64处理吧。

就为了执行那么一小段代码,搞的这么复杂,所以没事还是别做这么逆天的玩意。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档