首页
社区
课程
招聘
[原创]脱壳实践笔记-反硬件断点
2020-2-25 13:33 13685

[原创]脱壳实践笔记-反硬件断点

2020-2-25 13:33
13685

目录

01-寻找OEP

  1. pushad后,esp定律失败(壳oep处有花指令,按键1添加一个nop失效,手动右键-二进制修改为90

  2. 在主模块代码起始地址,使用Ctrl+B搜索“FF 15 ?? ?? ?? ??”,找到调用IAT的地方(或者FF 25;手动找太慢,直接搜索)

  3. 调用IAT确实是FF 15,排除宝蓝的BC++、Delphi程序,是VS的

  4. 程序跑起来后,在主模块ctrl+f搜索指令“sub esp 0x58”,果然找到,下面还有一个call,一般就是GetVersion,断定就是VC 6.0程序<!--more-->

  5. 除此之外,还可以VC 6.0 还有winMain函数的特征(winMain、sub esp 0x58、getVersion

    image-20191121161139303

  6. 找到真实oep

    image-20191121161440388

02-异常回调反硬件断点

  1. 在OEP处发现IAT加密了
    image-20191121161834601

  2. IAT处设置硬件写入断点,发现并没有断下,真实oep处设置硬件执行,也没有断下,猜测有反硬件断点的技术;

    硬件断点没有触发->程序有反调试->会清除硬件断点或者让硬件断点失效

  3. 清除硬件断点的方法有哪些?相当于有哪些方法可以修改寄存器?

    1. SetThreadContext
    2. 异常回调函数中可以修改寄存器
  4. 搜索SetThreadContext函数下断,发现并未断下,说明是异常回调起的作用(并未实测,win10虚拟机中的OD,ctrl+g不能搜API名)

03-反断点异常分析

配置环境

  • 如何分析带有异常的程序?

    • 修改OD菜单-选项-调试选项:将其中的选项统统取消打钩,尽可能不忽略异常
    • 修改OD菜单-插件-StrongOD-Options:将其中跳过异常的选项取消打钩
  • 搭建异常触发环境

    image-20191121163511503

    image-20191121163541379

分析异常

  • 不忽略程序异常运行程序,一共有7处异常,依次如下:

    其中3、4尽管看起来发生的位置和异常类型是一致的,但实际上触发的位置是不同的,从堆栈能看出来二者不同(就像是,同一个API,a和b两处都可以调用,尽管被调用的API相同,但是调用的位置a和b是不同的)

    image-20191121163818581

    image-20191121163942584

    image-20191121164040127

    image-20191121164127484

    image-20191121164149432

    image-20191121164206546

    image-20191121164226741

  • 哪一个异常对硬件断点清0?(在异常点设置硬件断点,看能不能断下,能断下说明当前异常点前面的异常处理函数中没有对硬件断点清0)

    第二个异常处下硬件执行,重新执行,能够断下来,说明第一个异常无辜

    image-20191121191824207

    第三个异常下断,也能断下来,说明1、2无辜

    image-20191121192219618

    第四个异常下断,没有断下来,说明第三次异常处理中,对硬件断点清0

    image-20191121192554307

  • 重新运行,至第三次异常,通过堆栈窗口,发现第三次的SEH处理程序是43AF42,光标移动到此,直接enter,使其反汇编窗口中跟随,发现疑似设置清零寄存器的代码(硬件断点基于寄存器,寄存器清零即消除硬件断点

    image-20191121193506344

  • 将此地址设置为硬件执行断点(下面才是硬件断点清零的操作,所以此处可以下断),重新运行至此时,直接dump内存保存下来(dump时不用动其他设置,直接点脱壳即可,因为仅仅是为了看代码,能不能运行无所谓;之所以在此处dump,是为了后面IDA打开时,能直接停在43AF42时

    image-20191121193930282

  • IDA中打开上述dump后的文件,打开就在43AF42处(如上所言)

    image-20191121194526170

  • IDA中,structures窗口-edit-add struct type来添加新的结构体类型(mac 没有insert按键)

    最终操作后如下,确实是一个断点清零0的操作(具体结构体添加的操作,见逆向实战-笔记3-IDA使用-结构体部分)

    image-20191121195243343

    IDA中,F5解析为C代码(进一步验证

    image-20191121195415253

  • (硬件断点设置和取消,是通过设置寄存器来实现的的;是经过一个context结构体的,而非直接改dr0、Dr1这些单个的位

突破反硬件断点

  • 已经找到消除硬件断点的地方了,只需要跳过即可,可以将0043AF57以下的代码到0043AF72全部nop

  • 恢复之前的异常触发环境,使其能忽略异常(调试选项-异常-钩都打上、strongOD-option-skip some exception也打上)

  • 找到真实oep的地方,设置硬件执行断点,在消除硬件断点的前面也设置硬件执行断点(42为消除、86为oep

    image-20191121201846874

  • 重新运行,首先会断在42处(因为设置了硬件断点,他要消除),在这将关键代码nop掉,这样,OEP那就能成功断下来

    image-20191121202059712

    image-20191121202157938

  • 像这样,只要每次设置硬件断点时,程序若要消除这些硬件断点,一定会跳到42处,只要跳到这,就将其nop掉,这样,后面的硬件断点就能正常断下了

04-填充IAT的地方

  • IAT处设置硬件写入断点,重新运行,又到了反硬件断点的地方,手动nop掉,继续,成功断在IAT写入的地方(由于是写入断点,陷阱类,eip指向下句)

    先断在这,找了找,不是

    image-20191121202901653

    后断在这

    image-20191121203813185

  • 第二次断的地方,918C处,向上翻并没有发现,ctrl+up键进行微调,成功找到(ctrl+up时关掉mac的键盘映射)

    image-20191121204003173

05-获取API地址的地方

  • 写脚本的一般思路:API地址获取后,保存地址到临时变量,在填充IAT时,填充这个临时变量的值(即真正API的地址),而非经过壳代码处理后的加密值

  • 从8A填充IAT的地方,F7单步向下走,时刻关注寄存器,看是否出现“xxx.yyy”的形式(尤其是eax寄存器

  • 若遇到循环,想办法跳出,5个关键点:向上跳的循环、循环跳到哪、二者之间的条件跳转、条件跳转何时跳出循环何时仍在循环内、下断点在跳出循环的位置

  • 第一个循环(crtl+F7自动跑,F7暂停)

    1:向上跳,标示是一个循环;

    2: 向上跳的循环,跳到此;

    3/4: 12之间的Jxx条件跳转;

    由下到上,即由1-2之间找跳转,1、3、4均是;

    先测试1的,若跳出循环,则下在5,测试,发现程序跑到真实oep处,不对,舍去;

    再测试3,发现正常循环内,3压根执行不到;

    再测试4,若要出循环,就不跳,在6处下断,F9运行至此,再ctrl+F7,发现可以出循环,正确

    image-20191122084136249

  • 第二个循环

    自上而下测试,先测试1处的,要跳出,需要在1下面,即4,但4也是往上跳,因此再往下,断点下载5,F9运行至此,继续F7单步;

    3处,发现无论跳与不跳,都要执行到1,都会再往上跳,故舍去;

    image-20191122090542037

  • 跳出第二个循环后,就要慢点F7了,时刻关注寄存器是否出现xxx.yyy的形式(重点eax)

  • 跳出第二个循环后,继续F7,发现又进入了第一个循环,再次F9使其跳出循环1,然后又进入循环2,也再次F9跳出循环2,继续F7,最终终于到此

    image-20191122094732050

  • 99处下断,测试一下,是否每次运行完此条,eax都会出现xxx.yyy,发现确实如此

    image-20191122095727759

  • 也注意到,这条指令是出现在循环1内部的,想想也对,这种操作肯定是需要循环操作的,因此,以后再遇到循环,先看其内部,有没有这种操作,先测试再说,万一就是呢

  • 先不管99处是否是获取API地址的call,反正运行99后,寄存器中会出现xxx.yyy这种形式,这样就够了,这样就能够写脚本了(停在9F,反正eax有真正的函数地址,即xxx.yyy形式)

  • 注意:

    • 不知道内部究竟是怎么循环的,跳过了循环1,还可能进入循环2,再跳出循环2后,还可能再次进入循环1……
    • 没什么规律可循,反正就是坚持这样的原则:碰到循环,就想法跳出循环
    • 玄学问题,不要深究,能找到位置就行
  • 妈耶,我都要被自己啰嗦死了(可能这就是菜吧

06-写OD脚本自动操作

  • 三个通用的地址
    • OEP
    • 填充IAT
    • 获取API地址
    • (执行完相关指令的下一位置,而非当前位置)
  • 通用思路
    • 获取API地址处:获取寄存器的值,赋值给临时变量(xxx.yyy,即API真正地址
    • 填充IAT处:直接填充临时变量的值,即填充真实API地址,而非被壳修改过的
    • OEP处:一直运行,直至运行到OEP,标志填充IAT结束
  • 特殊地址:清除硬件断点的地方
    • 因不同的壳而异
    • 再如,有的壳,填充IAT的代码在壳申请的内存空间中,这样,每次地址都不相同,需要先获取申请后的基地址
  • 步骤
    • 右键-脚本功能-脚本运行窗口
    • 右键-读取脚本-打开相应脚本文件
    • 先用OD运行程序到壳OEP处,再使用脚本(不可在一开始的系统断点处就使用脚本,会出错)
// 1. 找到相关地址(前三个是通用的)   
MOV vOEP,00409486 // OEP地址
MOV vGetAPIAddr,438F9F // 获取API地址
MOV vWriteIATAddr,43918c // 写入IAT地址
MOV vHardwarePointAddr,0043AF51 // 清除硬件断点的地方
MOV vAPIaddr,0// 临时变量存储真实API地址
// 2. 设置断点(先清除其他,防干扰)  
BPHWC// 清除所有硬件断点 
BC// 清除所有软件断点

BPHWS vHardwarePointAddr,"x" // 在此处下断,运行到此即停止
BPHWS vOEP,"x" //
BPHWS vGetAPIAddr,"x" // 
BPHWS vWriteIATAddr,"x" //

//(记住大体形式,举一反三)
// 3.0 循环:F9运行程序
LOOP_1: 
    RUN
// 3.1 清除硬件断点:若断在此,则将其NOP掉10h个字节
    CMP eip,vHardwarePointAddr
    JNZ SIGN_1
    fill vHardwarePointAddr,22,90//NOP 10h个字节
// 3.2 获取API地址:若断在此,则保存API地址到临时变量
SIGN_1:
    CMP eip,vGetAPIAddr 
  JNZ SIGN_2
    MOV vAPIaddr,eax
// 3.3 填充IAT:若断在此,则填充真实API地址(临时变量)
SIGN_2:
    CMP eip,vWriteIATAddr 
  JNZ SIGN_3
    mov [edi], vAPIaddr//依据填充IAT的指令,来决定目的是谁
// 3.4 OEP:若断在此,则结束,否则继续循环
SIGN_3:
    cmp eip,vOEP 
  JE EXIT_1
    JMP LOOP_1
// 3.5 弹框,标示结束
EXIT_1:
    MSG "修复完毕"
  • 当所有的修复完毕之后,发现IAT不是一个典型的IAT数组 ,隔4个出现一个(长型-地址)

    image-20191122145503344

  • 转换hex窗口,再仔细观察,发现每一个地址后面都多了一个0,因此还需要继续修复

    image-20191122145717687

  • 工具:通用导入表修复工具

    image-20191122152618812

  • 工具使用

    • PChunter查看06.exe的进程ID
    • 代码起始和结尾:程序代码段要包含在其中
    • 新的IAT VA:新建的IAT放在哪,放在壳代码所在区段中就行,因为脱壳后,壳所在区段肯定用不着了(脱壳后,仅仅是改变OEP为真实的OEP,让程序跳过壳代码,壳那块区段还是存在的
    • 修复输入表:一般就别点了,因为在这是新建一个IAT表,ImportREC是通过IAT表来修复输入表的
  • 原理:

    • 在代码段里找调用IAT的地方,如

      image-20191122153036972

    • 在0x43CFCD中,一定保存的是 getVersion真实的地址,获取真实地址

    • 找一个新的地方,将这些地址按照正确的格式存起来

    • OD脚本跑完后,IAT中的地址其实也是正确的,只不过格式不多(每一个后面都加了0),而ImportREC工具是依据IAT来修复输入表的,格式错误他肯定是识别不出来,所以要新建一个格式正确的IAT数组

    • (仅仅看代码段中调用了外部dll的哪些API,没调用的哪些自然不用管,因为程序运行也用不到啊

  • 效果

    • 修复之前,代码段调用和IAT在内存中:

      image-20191122153036972

      image-20191122153734532

    • 修复后,代码段调用时地址变了,IAT也在其他内存新建了一份

      image-20191122153814266

08-dump内存

  • OD脚本跑完后,正好停在真实OEP处,脱壳保存为06.dump.exe

    image-20191122154305338

  • 利用7中,新建的IAT表来修复输入表(在这就别点重建输入表了,出错几率大,还是用专业的ImportREC,大家各司其职

09-修复输入表(依据新建的IAT

  • 先IAT,再输入表,就是通过IAT重建输入表,转储至8中dump后的文件

    image-20191122154752632

  • dump后打开失败,转储后打开成功,脱壳成功!

10-参考

  • 15PB 脱壳课程

阿里云助力开发者!开发者可享99元/年,续费同价!企业可享99元/199元双重权益

收藏
点赞5
打赏
分享
最新回复 (17)
雪    币: 2050
活跃值: (2805)
能力值: (RANK:260 )
在线值:
发帖
回帖
粉丝
xiaohang 3 2020-2-25 13:47
2
0
一般如果不涉及到驱动保护的花,反硬件断点和反反硬件断点,就是围绕SetThreadContext,GetThreadContext两个API函数进行攻防较量,楼主写的挺详细,如果能吧例子发一下就更好了。
雪    币: 4540
活跃值: (1011)
能力值: ( LV10,RANK:160 )
在线值:
发帖
回帖
粉丝
21Gun5 2 2020-2-25 14:11
3
0
xiaohang 一般如果不涉及到驱动保护的花,反硬件断点和反反硬件断点,就是围绕SetThreadContext,GetThreadContext两个API函数进行攻防较量,楼主写的挺详细,如果能吧例子发一下就更好了 ...
这是之前的随手笔记,例子在另一台电脑上,一时半会拿不到,实在不好意思
最后于 2020-2-25 14:11 被21Gun5编辑 ,原因:
雪    币: 106
活跃值: (549)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
xtayaitak 2020-2-25 15:07
4
0
好文章,学习了!
雪    币: 18867
活跃值: (60313)
能力值: (RANK:125 )
在线值:
发帖
回帖
粉丝
Editor 2020-2-25 15:09
5
0
感谢分享!
雪    币: 168
活跃值: (823)
能力值: ( LV10,RANK:173 )
在线值:
发帖
回帖
粉丝
kaoyange 1 2020-2-25 16:37
6
0
说实在的,对于这个文章,我不知道对于新手能学到什么(也许是写给高手看的),全部都是操作步骤,没有为什么。
雪    币: 4540
活跃值: (1011)
能力值: ( LV10,RANK:160 )
在线值:
发帖
回帖
粉丝
21Gun5 2 2020-2-25 17:28
7
0
kaoyange 说实在的,对于这个文章,我不知道对于新手能学到什么(也许是写给高手看的),全部都是操作步骤,没有为什么。
对新手来说确实不太友好,我标题就说明了是自己的实践笔记,所以没有考虑到这个问题
雪    币: 12500
活跃值: (3043)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
白菜大哥 2020-2-25 21:12
8
0
xiaohang 一般如果不涉及到驱动保护的花,反硬件断点和反反硬件断点,就是围绕SetThreadContext,GetThreadContext两个API函数进行攻防较量,楼主写的挺详细,如果能吧例子发一下就更好了 ...
seh里面检查,也比较常见
雪    币: 83
活跃值: (1052)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
killpy 2 2020-2-25 22:31
9
0
有64位的脱壳教程吗 期待
雪    币: 83
活跃值: (1052)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
killpy 2 2020-2-25 23:03
10
0
Universer import Fixer 不选择新建输入表 只输入新的IAT 你的意思是用它来修复之前的IAT 多余的00?
Ollydump插件 是利用新建的IAT 修复原来的输入表 而不是创建一个新的输入表 因为这个插件容易出错?
最后一张图是ImportREC工具吗?用它自动查找新建的IAT和之前修复的输入表 完后把相关数据导入表结构写入到之前dump下来的pe里?
雪    币: 4540
活跃值: (1011)
能力值: ( LV10,RANK:160 )
在线值:
发帖
回帖
粉丝
21Gun5 2 2020-2-25 23:43
11
0
killpy Universer import Fixer 不选择新建输入表 只输入新的IAT 你的意思是用它来修复之前的IAT 多余的00? Ollydump插件 是利用新建的IAT 修复原来的输入表 而不是创 ...
也是新手刚入门不久,好多东西没理解透,有点似是而非的感觉,感谢前辈帮我指出来
雪    币: 422
活跃值: (835)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
流星暴雨 2020-2-26 00:14
12
0
好东西,资瓷
雪    币: 2050
活跃值: (2805)
能力值: (RANK:260 )
在线值:
发帖
回帖
粉丝
xiaohang 3 2020-2-26 13:06
13
0
白菜大哥 seh里面检查,也比较常见
seh目的是用来处理自定义异常,核心还是要能下硬件断点,ring3下硬件断点是需要通过SetThreadContext这个函数的。
雪    币: 12500
活跃值: (3043)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
白菜大哥 2020-2-26 15:54
14
0
xiaohang seh目的是用来处理自定义异常,核心还是要能下硬件断点,ring3下硬件断点是需要通过SetThreadContext这个函数的。
这是正常思路。。有些壳喜欢在seh里面检查dxr寄存器
雪    币: 2050
活跃值: (2805)
能力值: (RANK:260 )
在线值:
发帖
回帖
粉丝
xiaohang 3 2020-2-26 19:15
15
0
白菜大哥 这是正常思路。。有些壳喜欢在seh里面检查dxr寄存器
ring3下不能直接访问drx,会产生gp异常
雪    币: 12500
活跃值: (3043)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
白菜大哥 2020-2-26 23:58
16
0
xiaohang ring3下不能直接访问drx,会产生gp异常
em。不是直接访问。是通过一个啥结构来着,堆栈里面存了,异常结构里面有。
雪    币: 2050
活跃值: (2805)
能力值: (RANK:260 )
在线值:
发帖
回帖
粉丝
xiaohang 3 2020-2-29 20:07
18
0
白菜大哥 em。不是直接访问。是通过一个啥结构来着,堆栈里面存了,异常结构里面有。
你是想说ContextRecord结构吧,这个结构是系统保存自线程Context的,所以还是没有绕开ThreadContext
游客
登录 | 注册 方可回帖
返回