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

                                                                                           by Fanxiushu            引用或转载请注明原作者  2014-07-26

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

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

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

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

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

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

方法不下七八种之多。

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

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

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

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

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

KeUserModeCallback函数原型如下:

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 返回。

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


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驱动。如下:


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 。

     看起来的伪代码如下:

        

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函数里。

      

       KeUserModeCallback( ApiNumber64 , &user, sizeof(USERDATA),  &OutBuffer, &OutLen);

      

}

     

挺麻烦的WOW64处理吧。

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





你可能感兴趣的:(windows,驱动开发)