首页
社区
课程
招聘
[原创]觉醒之战Ⅰ:洞察HW程序员的脑洞
2018-6-10 11:30 5724

[原创]觉醒之战Ⅰ:洞察HW程序员的脑洞

2018-6-10 11:30
5724
首先解释下标题,为什么说是觉醒呢,因为逆向需要200%的集中力,而且中途不能中断,否则思路一断就全盘崩溃。
所以必须先有觉悟,进入觉醒状态,方可持续战斗,突破一切艰难险阻。。。

本篇选择的是HW(一个云桌面,具体我也不知道干什么的)软件的一个驱动模块,做注入用的。刚开始小瞧了一下,这个模块主要是64位的APC注入
我之前发过一个x86的,x64虽然也有干货但是因为一些限制因素没办法放出来,这个在手痒的时候用来练练手,历时1天左右,资源在最下方,有写的不好的地方还请指出来。

一. 模块逻辑

      这个注入是我阅读无数代码中,见过比较骚气的实现。这个作者有一定功力,大家拿到代码可以赏析一下,哈哈。

      一般我们在驱动中做注入的时候无外乎是找一个时机,完成诸如下面的操作:

            1. HOOK ntdll导出函数,甚至未导出函数

            2. 申请ShellCode,填写必要的函数地址,路径,UNICODE_STRING结构

            3. 修改导入表/ShimEngine(sdb)等等

      基本上也就一次执行机会,而且因为用到的函数由于模块基址不固定,多数只用ntdll这个固定模块基址的模块里面的函数,或者自己实现一些基本的函数查找,获取模块基地址这类的基本功能。那么下面看看这个模块如何分段进行资源的填充。

      APC的ShellCode很简单,x86/x64用到的也仅仅只有ntdll!LdrLoadDll,从节表位置来看,x86肯定用的是硬编码的静态变量,x64是嵌在代码段里的。

x86:
.rdata:0000000180004570 55				                push    rbp
.rdata:0000000180004571 8B EC			                 mov     ebp, esp
.rdata:0000000180004573 51				                push    rcx
.rdata:0000000180004574 8B 4D 08		                  mov     ecx, [rbp+8]
.rdata:0000000180004577 8D 45 FC		                  lea     eax, [rbp+var_4]
.rdata:000000018000457A 50			                    push    rax
.rdata:000000018000457B 8B 45 0C		                  mov     eax, [rbp+0Ch]
.rdata:000000018000457E 33 D2			                 xor     edx, edx
.rdata:0000000180004580 89 55 FC		                  mov     [rbp+var_4], edx
.rdata:0000000180004583 8D 04 C1		                  lea     eax, [rcx+rax*8]
.rdata:0000000180004586 50				                push    rax
.rdata:0000000180004587 52				                push    rdx
.rdata:0000000180004588 52				                push    rdx
.rdata:0000000180004589 FF 91 20 08 00 00		         call    qword ptr [rcx+820h] ; ntdll!LdrLoadDll
.rdata:000000018000458F 8B E5			                 mov     esp, ebp
.rdata:0000000180004591 5D				                pop     rbp
.rdata:0000000180004592 C2 0C 00			              retn    0Ch

x64:
.text:00000001800023AC 48 83 EC 28			           sub     rsp, 28h
.text:00000001800023B0 48 83 64 24 30 00		         and     [rsp+28h+arg_0], 0
.text:00000001800023B6 4C 63 C2			              movsxd  r8, edx
.text:00000001800023B9 48 8B C1			              mov     rax, rcx
.text:00000001800023BC 49 C1 E0 04			           shl     r8, 4
.text:00000001800023C0 4C 8D 4C 24 30                  lea     r9, [rsp+28h+arg_0]
.text:00000001800023C5 33 D2                           xor     edx, edx
.text:00000001800023C7 4C 03 C1                        add     r8, rcx
.text:00000001800023CA 33 C9                           xor     ecx, ecx
.text:00000001800023CC FF 90 40 08 00 00               call    qword ptr [rax+840h] ; ntdll!LdrLoadDll
.text:00000001800023D2 48 83 C4 28                     add     rsp, 28h
.text:00000001800023D6 C3                              retn

      分阶段填充的显然不是ShellCode用到的资源,其实从代码细节上看也有,只不过都是一些UNICODE_STRING还有字符串的填充是分段的; 分阶段 主要是为了64位系统下的x86进程。WOW64的执行原理也有很多人解释过了,这里我着重说下: 从内核返回时一切都是从x64执行层开始(因为CPU在内核时是64位模式,到应用层要经过wow64的转化才能到x86模式的CPU)


       因此x86进程其实也可以加载64位模块,但是必须要在CPU处于64模式时使用64位ntdll。但是就注入来说肯定是要在x86进程中注入32位模块,那么就必须到x86执行层去做这些事情。驱动无论何时往哪个进程中的哪个线程插入APC那都是64位模式的APC,所以这里需要做个中转。
      调试过64位ntdll!KiUserApcDispatcher的都应该知道,32位模式的APC地址经过混淆计算,并通过wow64!wow64ApcRoutinue转化为x86执行层的APC。所以知道这个就可以直接在驱动中插入一个NormalRoutinue为 wow64!wow64ApcRoutinue 的64位模式APC,将x86 APC地址逆着混淆计算一遍,作为 wow64! wow64ApcRoutinue 的NormalContext传入即可。
       那么剩下就是一些细节问题,首先我们不确定wow64.dll基址是否固定,所以最好是在wow64模块加载的时候动态获取 wow64!wow64ApcRoutinue 并进行填充;x86的ntdll.dll其实也不敢说基址固定,所以x86的ntdll!LdrLoadDll最好也是正在该模块加载的时候动态获取。实际上HW也是这么做的。为了确保同一个进程ShellCode资源的唯一性以及内核地址可操作性,他使用了在内核中映射RING3内存的物理内存的方法,原理类似MDL。

NTSTATUS HWMapSection(PHWProcMem pProcMem)
{
    NTSTATUS Status;
    HANDLE SectionHandle;
    OBJECT_ATTRIBUTES ObjectAttributes;
    LARGE_INTEGER MaximumSize;
    LARGE_INTEGER SectionOffset = { 0 };
    SIZE_T ViewSize = 0;
    PVOID pMappedInUserAddr = NULL;
    PVOID SectionObject;

    PVOID pMappedInSysAddr;
    SIZE_T ulMappedSize;

    InitializeObjectAttributes(
        &ObjectAttributes,
        NULL,
        OBJ_KERNEL_HANDLE, 
        NULL,
        NULL
        );

    // 4KB 普通小页面
    MaximumSize.QuadPart = 0x1000;

    // 无名无文件句柄Section
    Status = ZwCreateSection(&SectionHandle, 0xF001F, &ObjectAttributes, &MaximumSize, PAGE_EXECUTE_READWRITE, SEC_COMMIT, NULL);
    DbgPrint("[%s] ZwCreateSection(%x)", __FUNCTION__, Status);

    if (NT_SUCCESS(Status))
    {
        Status = ZwMapViewOfSection(
            SectionHandle,
            NtCurrentProcess(),
            &pMappedInUserAddr,
            0,
            MaximumSize.LowPart,
            &SectionOffset,
            &ViewSize,
            ViewShare,
            0,
            PAGE_EXECUTE_READWRITE
            );

        DbgPrint("[%s] ZwMapViewOfSection(%x) ViewSize(%x)", __FUNCTION__, Status, ViewSize);

        if (NT_SUCCESS(Status))
        {
            pProcMem->pMappedInUserAddr = pMappedInUserAddr;

            Status = ObReferenceObjectByHandle(
                SectionHandle,
                0xF001F,
                0, 
                0, 
                &SectionObject,
                0);

            DbgPrint("[%s] ObReferenceObjectByHandle(%x) SectionObject(%x)", __FUNCTION__, Status, SectionObject);

            if (NT_SUCCESS(Status))
            {
                Status = MmMapViewInSystemSpace(SectionObject, &pMappedInSysAddr, &ulMappedSize);
                if ( NT_SUCCESS(Status) )
                {
                    pProcMem->SectionHandle = SectionHandle;
                    pProcMem->pMappedInSysAddr = pMappedInSysAddr;
                    pProcMem->dwMappedInSysSize = ulMappedSize;
                    pProcMem->SectionObject = SectionObject;

                    DbgPrint("[%s] pMappedInSysAddr(%I64x) dwMappedInSysSize(%x) pMappedInUserAddr(%I64x)", 
                        __FUNCTION__, pMappedInSysAddr, ulMappedSize, pMappedInUserAddr);
                    return STATUS_SUCCESS;
                }

                ZwUnmapViewOfSection(NtCurrentProcess(), pProcMem->pMappedInUserAddr);
                ObDereferenceObject(SectionObject);
            }
        }
    }

    return Status;
}
      这个函数在LoadImage回调加载ntdll.dll时进行调用,并将映射成功的地址存入AVL Tree中,在LoadImage加载wow64.dll时在AVL Tree中查找到对应结构,然后获取wow64!wow64ApcRoutinue填入其中,内核中映射的物理地址主要做一些UNICODE_STRING结构的调整......

二. Banned API

      熟悉APC的人大多都知道,apc在插入后是可以被立即触发的,因为微软的一些API就是干这些事情的,比如:

BOOLEAN
NTAPI
KeTestAlertThread(IN KPROCESSOR_MODE AlertMode)
{
    PKTHREAD Thread = KeGetCurrentThread();
    BOOLEAN OldState;
    KLOCK_QUEUE_HANDLE ApcLock;
    ASSERT_THREAD(Thread);
    ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);

    /* Lock the Dispatcher Database and the APC Queue */
    KiAcquireApcLock(Thread, &ApcLock);

    /* Save the old State */
    OldState = Thread->Alerted[AlertMode];

    /* Check the Thread is alerted */
    if (OldState)
    {
        /* Disable alert for this mode */
        Thread->Alerted[AlertMode] = FALSE;
    }
    else if ((AlertMode != KernelMode) &&
             (!IsListEmpty(&Thread->ApcState.ApcListHead[UserMode])))
    {
        /* If the mode is User and the Queue isn't empty, set Pending */
        Thread->ApcState.UserApcPending = TRUE;  // 然后返回用户空间时就立即执行了
    }

    /* Release Locks and return the Old State */
    KiReleaseApcLock(&ApcLock);
    return OldState;
}

NTSTATUS
NTAPI
KeDelayExecutionThread(IN KPROCESSOR_MODE WaitMode,
                       IN BOOLEAN Alertable,
                       IN PLARGE_INTEGER Interval OPTIONAL)
{
    PKTIMER Timer;
    PKWAIT_BLOCK TimerBlock;
    PKTHREAD Thread = KeGetCurrentThread();
    NTSTATUS WaitStatus;
    BOOLEAN Swappable;
    PLARGE_INTEGER OriginalDueTime;
    LARGE_INTEGER DueTime, NewDueTime, InterruptTime;
    ULONG Hand = 0;

    if (Thread->WaitNext)
        ASSERT(KeGetCurrentIrql() == DISPATCH_LEVEL);
    else
        ASSERT(KeGetCurrentIrql() <= APC_LEVEL);

    /* If this is a user-mode wait of 0 seconds, yield execution */
    if (!(Interval->QuadPart) && (WaitMode != KernelMode))
    {
        /* Make sure the wait isn't alertable or interrupting an APC */
        if (!(Alertable) && !(Thread->ApcState.UserApcPending))
        {
            /* Yield execution */
            return NtYieldExecution();
        }
    }

    /* Setup the original time and timer/wait blocks */
    OriginalDueTime = Interval;
    Timer = &Thread->Timer;
    TimerBlock = &Thread->WaitBlock[TIMER_WAIT_BLOCK];

    /* Check if the lock is already held */
    if (!Thread->WaitNext) goto WaitStart;

    /*  Otherwise, we already have the lock, so initialize the wait */
    Thread->WaitNext = FALSE;
    KxDelayThreadWait();

    /* Start wait loop */
    for (;;)
    {
        /* Disable pre-emption */
        Thread->Preempted = FALSE;

        /* Check if a kernel APC is pending and we're below APC_LEVEL */
        if ((Thread->ApcState.KernelApcPending) && !(Thread->SpecialApcDisable) &&
            (Thread->WaitIrql < APC_LEVEL))
        {
            /* Unlock the dispatcher */
            KiReleaseDispatcherLock(Thread->WaitIrql);
        }
        else
        {
            /* Check if we have to bail out due to an alerted state */
            WaitStatus = KiCheckAlertability(Thread, Alertable, WaitMode);  // Alertable为TRUE以及UserMode时置位
            if (WaitStatus != STATUS_WAIT_0) break;

            /* Check if the timer expired */
            InterruptTime.QuadPart = KeQueryInterruptTime();
            if ((ULONGLONG)InterruptTime.QuadPart >= Timer->DueTime.QuadPart)
            {
                /* It did, so we don't need to wait */
                goto NoWait;
            }

            /* It didn't, so activate it */
            Timer->Header.Inserted = TRUE;

            /* Handle Kernel Queues */
            if (Thread->Queue) KiActivateWaiterQueue(Thread->Queue);

            /* Setup the wait information */
            Thread->State = Waiting;

            /* Add the thread to the wait list */
            KiAddThreadToWaitList(Thread, Swappable);

            /* Insert the timer and swap the thread */
            ASSERT(Thread->WaitIrql <= DISPATCH_LEVEL);
            KiSetThreadSwapBusy(Thread);
            KxInsertTimer(Timer, Hand);
            WaitStatus = (NTSTATUS)KiSwapThread(Thread, KeGetCurrentPrcb());

            /* Check if were swapped ok */
            if (WaitStatus != STATUS_KERNEL_APC)
            {
                /* This is a good thing */
                if (WaitStatus == STATUS_TIMEOUT) WaitStatus = STATUS_SUCCESS;

                /* Return Status */
                return WaitStatus;
            }

            /* Recalculate due times */
            Interval = KiRecalculateDueTime(OriginalDueTime,
                                            &DueTime,
                                            &NewDueTime);
        }

WaitStart:
        /* Setup a new wait */
        Thread->WaitIrql = KeRaiseIrqlToSynchLevel();
        KxDelayThreadWait();
        KiAcquireDispatcherLockAtDpcLevel();
    }

    /* We're done! */
    KiReleaseDispatcherLock(Thread->WaitIrql);
    return WaitStatus;

NoWait:
    /* There was nothing to wait for. Did we have a wait interval? */
    if (!Interval->QuadPart)
    {
        /* Unlock the dispatcher and do a yield */
        KiReleaseDispatcherLock(Thread->WaitIrql);
        return NtYieldExecution();
    }

    /* Unlock the dispatcher and adjust the quantum for a no-wait */
    KiReleaseDispatcherLockFromDpcLevel();
    KiAdjustQuantumThread(Thread);
    return STATUS_SUCCESS;
}

FORCEINLINE
NTSTATUS
KiCheckAlertability(IN PKTHREAD Thread,
                    IN BOOLEAN Alertable,
                    IN KPROCESSOR_MODE WaitMode)
{
    /* Check if the wait is alertable */
    if (Alertable)
    {
        /* It is, first check if the thread is alerted in this mode */
        if (Thread->Alerted[WaitMode])
        {
            /* It is, so bail out of the wait */
            Thread->Alerted[WaitMode] = FALSE;
            return STATUS_ALERTED;
        }
        else if ((WaitMode != KernelMode) &&
                (!IsListEmpty(&Thread->ApcState.ApcListHead[UserMode])))
        {
            /* It's isn't, but this is a user wait with queued user APCs */
            Thread->ApcState.UserApcPending = TRUE; // 前提是队列里一定有未决APC
            return STATUS_USER_APC;
        }
        else if (Thread->Alerted[KernelMode])
        {
            /* It isn't that either, but we're alered in kernel mode */
            Thread->Alerted[KernelMode] = FALSE;
            return STATUS_ALERTED;
        }
    }
    else if ((WaitMode != KernelMode) && (Thread->ApcState.UserApcPending))
    {
        /* Not alertable, but this is a user wait with pending user APCs */
        return STATUS_USER_APC;
    }

    /* Otherwise, we're fine */
    return STATUS_WAIT_0;
}


       但是调用这些API,尤其是在进程在PE导入表初始化阶段,跟其他产商都使用APC的时候非常容易出现兼容问题,因为这些API一调用,PE初始化过程立即被抢占转而执行APC,而ShellCode调用的API 可能有LdrGetProcedureAddress这些默认在PE初始化完成时才可以调用的API。 LdrGetProcedureAddress提前执行那么即将引起系统DLL的DllMain的提前初始化...... 
       而且有些情况,不小心代码执行到了FreeLibrary,他会检查当前链表中是否有引用计数为0的Dll,如果有那么就会将这些Dll全部卸载。在PE初始化未完成时,系统Dll一般都会有引用计数,但是一些软件自己的Dll以导入表的方式被其他模块引用的情况下很可能在FreeLibrary检查时被卸载。
      其实像这类API都应该被禁用,因为太不稳定了,而且APC只要是在进程的PE初始化阶段被插入就可以执行起来,因为PE初始化完成后会自行调用ZwTestAlert。线程刚刚处于可觉醒状态那么就标志着他初始化工作已经完毕。

三. 吐槽

      细看反汇编代码,真的是漏洞百出,实在不忍心吐槽了,反正需要改的地方我能帮他改的都改了。。

      不知道你们有没有升级到Win10 RS4 正式版,这个Device Guard跟vmware冲突,搞死我了,还好看了vmware官网的解决办法关了DeviceGuard。




[培训]《安卓高级研修班(网课)》月薪三万计划,掌 握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

最后于 2018-6-16 20:24 被FaEry编辑 ,原因:
上传的附件:
收藏
点赞1
打赏
分享
最新回复 (7)
雪    币: 95
活跃值: (124)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
isdebug 2018-6-10 12:23
2
0
比较详细+1
雪    币: 58
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
zaimongli 2018-6-10 13:56
3
0
代码都是这样的,原理大家都大概的知道,
要处理的是细节.
雪    币: 12839
活跃值: (9013)
能力值: ( LV9,RANK:280 )
在线值:
发帖
回帖
粉丝
hzqst 3 2018-6-11 18:39
4
0
 
最后于 2018-6-14 14:22 被hzqst编辑 ,原因:
雪    币: 2215
活跃值: (5935)
能力值: ( LV13,RANK:409 )
在线值:
发帖
回帖
粉丝
xwtwho 1 2018-6-12 09:06
5
0
楼主,你这个IDA文件中,汇编和c代码一起显示咋弄的,有插件把c代码转成对应行的comment?
雪    币: 5023
活跃值: (2521)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
FaEry 6 2018-6-12 09:58
6
0
xwtwho 楼主,你这个IDA文件中,汇编和c代码一起显示咋弄的,有插件把c代码转成对应行的comment?
汇编状态下,c大法呀,没有插件
雪    币: 12
活跃值: (388)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
MaMy 2018-9-18 11:56
7
0
huawei?
雪    币: 5023
活跃值: (2521)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
FaEry 6 2018-9-19 15:15
8
0
MaMy huawei?
yeah
游客
登录 | 注册 方可回帖
返回