[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打开看:原程序的机器码完好无损,完全看不出被改过!

在这里插入图片描述

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

在这里插入图片描述

原文地址:https://www.cnblogs.com/csnd/p/15613312.html