• [Rootkit] 无痕 hook 硬件断点


    hook方式有多种,这里做了一个系统性的总结对比,如下:
    在这里插入图片描述

    https://www.cnblogs.com/theseventhson/p/14324562.html 之前这里做了接受消息的hook,用的就是最初级的hook方式: jmp到我们自己的处理逻辑。上面也分析了,这种方式缺点非常明显;最牛逼的神级hook:VT读写分离前面已经介绍过了,今天继续介绍高级的hook方式:硬件断点;

    现代软件开发,尤其是大型软件开发,绝对不可能一步到位,开发期间肯定存在各种bug。为了方便找到这些bug,软件上有专门的调试机制,比如在某行代码下软件断点,然后步过、步进等。这里软件断点本质就是在用户指定的地址改写成0xCC,也就是int 3指令,cpu执行到这里后就产生异常,然后由中断向量表的3号routine来处理这个异常。除了软件断点,x86架构的cpu也支持设置硬件断点,整个图示图下:

    在这里插入图片描述

    和硬件调试相关的寄存器一共有7个:DR0-DR3分别设置需要断下的地址,DR7可以控制DR0-DR3是否有效。如果需要启用这4个调试寄存器,DR7要设置为0b01010101,也就是L0\L1L2\L3都要为1;

    正式介绍代码前,先介绍一个重要的结构体:PCONTEXT,如下:

    typedef struct DECLSPEC_NOINITALL _CONTEXT {
    
        //
        // The flags values within this flag control the contents of
        // a CONTEXT record.
        //
        // If the context record is used as an input parameter, then
        // for each portion of the context record controlled by a flag
        // whose value is set, it is assumed that that portion of the
        // context record contains valid context. If the context record
        // is being used to modify a threads context, then only that
        // portion of the threads context will be modified.
        //
        // If the context record is used as an IN OUT parameter to capture
        // the context of a thread, then only those portions of the thread's
        // context corresponding to set flags will be returned.
        //
        // The context record is never used as an OUT only parameter.
        //
    
        DWORD ContextFlags;
    
        //
        // This section is specified/returned if CONTEXT_DEBUG_REGISTERS is
        // set in ContextFlags.  Note that CONTEXT_DEBUG_REGISTERS is NOT
        // included in CONTEXT_FULL.
        //
    
        DWORD   Dr0;
        DWORD   Dr1;
        DWORD   Dr2;
        DWORD   Dr3;
        DWORD   Dr6;
        DWORD   Dr7;
    
        //
        // This section is specified/returned if the
        // ContextFlags word contians the flag CONTEXT_FLOATING_POINT.
        //
    
        FLOATING_SAVE_AREA FloatSave;
    
        //
        // This section is specified/returned if the
        // ContextFlags word contians the flag CONTEXT_SEGMENTS.
        //
    
        DWORD   SegGs;
        DWORD   SegFs;
        DWORD   SegEs;
        DWORD   SegDs;
    
        //
        // This section is specified/returned if the
        // ContextFlags word contians the flag CONTEXT_INTEGER.
        //
    
        DWORD   Edi;
        DWORD   Esi;
        DWORD   Ebx;
        DWORD   Edx;
        DWORD   Ecx;
        DWORD   Eax;
    
        //
        // This section is specified/returned if the
        // ContextFlags word contians the flag CONTEXT_CONTROL.
        //
    
        DWORD   Ebp;
        DWORD   Eip;
        DWORD   SegCs;              // MUST BE SANITIZED
        DWORD   EFlags;             // MUST BE SANITIZED
        DWORD   Esp;
        DWORD   SegSs;
    
        //
        // This section is specified/returned if the ContextFlags word
        // contains the flag CONTEXT_EXTENDED_REGISTERS.
        // The format and contexts are processor specific
        //
    
        BYTE    ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
    
    } CONTEXT;
    
    typedef CONTEXT *PCONTEXT;
    

    这个结构体保存了常用的所有寄存器,OD、x32dbg、x64dbg等常见的调试器都用了这个结构体读取某个时间点时进程的寄存器,分析人员也可以直接在调试器的界面更改寄存器的值,非常方便。这些功能都是通过读写PCONTEXT结构体实现的。那么问题来了,怎么才能得到PCONTEXT结构体了?

    PCONTEXT为调试而生,为了得到这个结构体,就要想办法产生异常;windwos操作系统专门针对异常的处理有一整套完整的机制,这里为了理解,简单介绍一下:windwos下3环进程运行时,如果遇到异常(比如除0),大致的处理顺序如下:

    • 先看看有没有调试器(通过编译器运行exe也算),如果有,就发消息给调试器让其处理;
    • 如果没有调试器,或则调试器没处理,进入进程自己的VEH继续处理。VEH本质是个双向链表,存储了异常的handler代码,此时windwos会挨个遍历这个链表执行这些handler(感觉原理和vmp很像,估计vmp借鉴了这里的思路)
    • 如果VEH还没处理好,接着由线程继续处理。线程同样有个异常接管的链表,叫SEH;windows同样会遍历SEH来处理异常
    • 如果SEH还没处理好,继续给线程的UEH传递,UEH只有一个处理函数了
    • 如果UEH还没处理好,就回到进程的VCH处理;

    基于windwos开发的应用数以万计,微软绝对不可能出厂时就考虑到所有的异常,其各种handler不太可能处理所有的异常,所以微软又开放了接口,让开发人员自定义异常的handler;对于开发人员来说,肯定是越靠前越好,所以这里选择VEH来添加自定义的handler(调试器是最先收到异常通知的,但外挂在正常使用时不太可能有调试的功能,除非开发人员自己单独开发调试器的功能,这样成本太高了)。windwos开放了一个API,叫AddVectoredExceptionHandler,可以给VEH添加用户自定义的异常处理handler,如下:

    AddVectoredExceptionHandler(1, PvectoredExceptionHandler)
    

    函数有两个参数:第一个参数如果不是0,那么自定义的handler最先执行;如果是0,那么自定义的handler最后执行。这里我们当然希望自己的handler最先执行了,所以设置成1;另一个参数就是自定义的handler了,这个函数的原型:

    LONG PvectoredExceptionHandler(
      _EXCEPTION_POINTERS *ExceptionInfo
    )
    {...}
    

    继续追踪这个函数的参数,如下:

    typedef struct _EXCEPTION_POINTERS {
      PEXCEPTION_RECORD ExceptionRecord;
      PCONTEXT          ContextRecord;
    } EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
    

    这里终于得到了我们想要的PCONTEXT;这个PCONTEXT只有在程序出异常时windwos才会在VEH暴露出来,开发人员才能进一步修改DR寄存器地值,所以这里要先人为产生软件异常(比如设置0xCC),让后由我们自定义的handler接管,得到PCONTEXT后就能愉快的修改DR寄存器组了;

    由于各种原因,完整的代码就不展示了,这里展示核心的片段:

    • 这里先自定义一个函数,然后给函数的开始地址设置0xCC,当CPU执行到这里时,产生异常,被我们自定的PvectoredExceptionHandler接管,同时暴露了PCONTEXT,这时再在openDbg函数设置硬件断点和开启硬件调试功能!

    • 硬件断点只有4个,都存放在数组中,每个元素又封装了一层DbgPoint;

    LONG  _stdcall PvectoredExceptionHandler(PEXCEPTION_POINTERS val)
    {
        
        //CString wTxt;
        //wTxt.Format(L"%X", val->ExceptionRecord->ExceptionCode);
        //AfxMessageBox(wTxt);
        unsigned _eip = val->ContextRecord->Eip;
        if (val->ExceptionRecord->ExceptionCode == STATUS_BREAKPOINT)//0x80000003是int 3
        {
            /*根据hook的地址,在链表中查找回调函数和返回地址*/
            PHOOKPOINT point = htdHook2Ptr->Points.FindPoint((LPVOID)_eip);
            if (point)
            {
                /*注意这里我们自定义回调函数的调用方法:
                1、先执行point->GetHookBack2(),得到得到addPoint时设置的回调函数地址(赋值给了成员变量DestCall);
                2、再执行DestCall(val->ContextRecord),这才是真正执行我们回调函数的地方
                */
                if (point->GetHookBack2()(val->ContextRecord))//如果回调函数返回true,修复代码;
                {
                    val->ContextRecord->Eip = (unsigned)point->CodeFix;
                }
                else
                {
                    val->ContextRecord->Eip = (unsigned)point->AddressRet;//回调函数返回false,跳转到我们人为指定的地方
                }
                /*这个异常我已经搞定,源程序可以继续执行了*/
                return EXCEPTION_CONTINUE_EXECUTION;
            }
            /*hook链表中没找到这个eip,说明不是我们自己的hook点,继续search异常的接管代码*/
            else return EXCEPTION_CONTINUE_SEARCH;
        }
    
        if (val->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP)//0x80000004是cpu异常
        {
            //
            //AfxMessageBox(L"1");
            auto point=htdHook2Ptr->DbgPoints.FindPoint((LPVOID)_eip);//看看当前地址是不是我们事先设置好的硬件断点
            if (point)//hook的点是存在的,说明就是我们事先设置好的硬件断点,先执行我们的回调函数
            {
                
                //AfxMessageBox(L"2");
                if (point->DestCall(val->ContextRecord))//这里就直接显式用回调函数了。回调函数返回true,需要继续回到hook下一行单步
                {
                    //AfxMessageBox(L"3");
                    val->ContextRecord->Dr7 = 0; //这里取消所有的硬件断点,不仅仅是当前的
                    /*TF位设置成1,cpu进入单步调试模式;执行下一行指令时,同样会触发STATUS_SINGLE_STEP异常,会继续进入现在的这个if条件;
                      上面刚取消所有硬件断点,如果这里不设置单步模式,后续的硬件断点都会失效*/
                    val->ContextRecord->EFlags |= 0x100;
                }
                else //回调函数返回false,eip采用执行的地址(setHook的时候传入的),没必要再单步了,这时候可以告诉CPU,我已经把异常处理掉了,你可以继续!
                {
                    return EXCEPTION_CONTINUE_EXECUTION;//
                }    
            }
            else//hook点不存在,大概率是上面设置了TF=1单步调试,但这些地址并不是我们设置的断点,所以不需要执行回调,直接继续设置硬件断点后继续执行
            {
                htdHook2Ptr->DbgPoints.OpenDbg(val->ContextRecord);//给当前的DR0-DR3分别设置4个point的断点(不一定是当前地址,而是我们setHook时指定的),执行到任何一个都能断下来
            }
    
            return EXCEPTION_CONTINUE_EXECUTION;
        }
    
        return EXCEPTION_CONTINUE_SEARCH;//既不是单步,也不是0xCC,说明异常和我们没关系,丢给OS继续处理
    }
    
    
    
    void ThreadTrap()
    {
            
         _asm  //防止编译器把函数优化掉
          {
             mov eax,eax
             mov eax,eax
             mov eax,eax
         }
    }
    
    
    bool InThread(HOOKREFS2)
    {
        htdHook2Ptr->UnHook((LPVOID)_EIP);
        htdHook2Ptr->DbgPoints.OpenDbg(val);//启用硬件调试
        return false;
    }
    /*设置线程劫持环境*/
    void htdHook2::Init()
    {
        /*
          1、对ThreadTrap函数设置0xCC,让其产生异常,然后被我们接管,从而得到context
          2、InThread是回调函数,return是false,让eip继续指向ThreadTrap,这不成死循环了么?
          3、所以回调函数InThread要恢复被挂钩的地方
          4、这里设置0xCC的是自己的函数,不是目标进程函数,所以CRC32检测是无效的;
          5、核心目的是进入InThread设置硬件断点(直接调用OpenDbg函数是不行的,必须人为制造异常后才行,因为需要PCONTEXT)
         */
        SetHook(ThreadTrap, 1, InThread, ThreadTrap);
        /*注意函数的调用方式:
        1、让ThreadTrap执行,触发我们事先设置好的异常
        2、如果直接ThreadTrap(),可能会被编译器优化成内联函数,也就是3行mov eax,eax直接放入Init函数,就不走函数调用了,避免push压栈的操作,效率更高
        3、所以这里用汇编call显式调用
        */
        DWORD dRet=(DWORD)ThreadTrap;
        _asm call dRet;
    }
    
    /*
    1、注册异常的接管函数。这个是构造函数,生成对象时自动调用了
    */
    htdHook2::htdHook2()
    {
        htdHook2Ptr = this;
        PPointLast = &Points;
        AddVectoredExceptionHandler(1, PvectoredExceptionHandler);//完成了异常的接管
        
    }
    
    /*这里只破坏一个字节,就算被用pchunter类的ark工具检测到挂钩点,由于没有call地址,也不好被handler函数*/
    void htdHook2::SetHook(LPVOID Address, uchar len, HOOKBACK2 hookBack,LPVOID AddressRet)
    {
        DWORD dOld;
        DWORD dNew;
    
        VirtualProtect(Address, 0x1, PAGE_EXECUTE_READWRITE, &dOld); //修改HookFactroy内存属性为可以执行
        PPointLast = PPointLast->AddPonit(Address, AddressRet, hookBack, len);
        char* code = (char*)(Address);
        code[0] = 0xCC;
        VirtualProtect(Address, 0x1, dOld, &dNew);
    }
    
    bool htdHook2::SetHook(LPVOID Address, HOOKBACK2 hookBack, LPVOID AddressRet)
    {
        return DbgPoints.AddHookPoint(Address, hookBack, AddressRet);
    }
    
    /*先恢复address原来的代码,再将现在的point从链表取出*/
    void htdHook2::UnHook(LPVOID Address)
    {
        //卸载HOOK
        PHOOKPOINT _point=Points.FindPoint(Address);
        if (_point)
        {
            _point->Recover();
            _point->BackPoint->NextPoint = _point->NextPoint;
            if (_point->NextPoint)_point->NextPoint->BackPoint = _point->BackPoint;
            delete _point;
        }
    }
    

    这里设置context的各个关键寄存器的值。为了给调试寄存器设置值,需要先得到PCONTEXT

    void DBGPOINT::OpenDbg(PCONTEXT _context)
    {
        _context->Dr0 = (DWORD)Point[0].Address;//即使address是0也不影响
        _context->Dr1 = (DWORD)Point[1].Address;
        _context->Dr2 = (DWORD)Point[2].Address;
        _context->Dr3 = (DWORD)Point[3].Address;
        _context->Dr7 = 0b01010101;//这才真正启用硬件断点
    }
    

    自定义的消息hook代码:由于并未破坏机器码,所以不需要到处跳转和修复,代码少了很多,逻辑也明晰了很多!拦截到的消息直接在DiologBox的Edit打印出来:

    CString GetMsgByAddress(DWORD memAddress)
    {
        CString tmp;
        DWORD msgLength = *(DWORD*)(memAddress + 4);//每个消息下面都有2个4byte的正数保存了这个字符串的长度
        if (msgLength > 0) {
            WCHAR* msg = new WCHAR[msgLength + 1]{ 0 };
            wmemcpy_s(msg, msgLength + 1, (WCHAR*)(*(DWORD*)memAddress), msgLength + 1);
            tmp = msg;
            delete[]msg;
        }
        return  tmp;
    }
    
    CWndMain* pCWndMain{};
    bool hookMsg(HOOKREFS2) {
    
        CTime time = CTime::GetCurrentTime();
        CString strTime = time.Format(_T("%Y-%m-%d %H:%M:%S"));
    
        DWORD** msgAddress = (DWORD**)(val->Esp);
        CString wid = GetMsgByAddress(**msgAddress + 0x40);
        CString fullmsg = GetMsgByAddress(**msgAddress + 0x68);
        CString isWid = GetMsgByAddress(**msgAddress + 0x164);
        CString md5 = GetMsgByAddress(**msgAddress + 0x178);
        msg = wid + fullmsg + isWid + md5;
    
        msg.Format(_T("\r\nwid=%s, msg=%s,isWid=%s, md5=%s, time=%s\r\n"), wid, fullmsg, isWid, md5, strTime);
        pCWndMain->EDIT_SHOWMSG.SetSel(-1,-1);//FALSE表示会随光标位置改变而滚动滚动条
        pCWndMain->EDIT_SHOWMSG.ReplaceSel(msg);
        //pCWndMain ->EDIT_SHOWMSG.SetWindowTextW(msg);
        //AfxMessageBox(msg);
        return true;//如果返回false,dr7不会被置0,回调函数会不停被执行,导致卡死
    }
    
    

    用x32dbg打开看:原程序的机器码完好无损,完全看不出被改过!

    在这里插入图片描述

    之前调试时人为下了软件断点,这里完全看不到硬件断点!

    在这里插入图片描述

  • 相关阅读:
    创龙TMS320C6748开发板串口和中断学习笔记
    RTL8195AM开发板使用
    CC3100BoosterPack和CC31XXEMUBOOST板子的测试
    利尔达NB-IOT的PSM和eDRX低功耗模式笔记
    【原创】大数据基础之Zookeeper(3)选举算法
    【原创】大数据基础之Zookeeper(2)源代码解析
    【原创】大数据基础之Zookeeper(1)介绍、安装及使用
    【原创】论码农的财富修养
    【原创】大叔案例分享(2)处理大批量数据时如何实现“高效”同时实现“断点续传”功能
    【原创】大数据基础之Spark(1)Spark Submit即Spark任务提交过程
  • 原文地址:https://www.cnblogs.com/csnd/p/15613312.html
Copyright © 2020-2023  润新知