首页
论坛
课程
招聘
雪    币: 4706
活跃值: 活跃值 (118)
能力值: ( LV10,RANK:160 )
在线值:
发帖
回帖
粉丝

[原创]脱壳实践笔记-动态空间填充IAT

2020-2-25 13:31 3675

[原创]脱壳实践笔记-动态空间填充IAT

2020-2-25 13:31
3675

目录

01-找OEP:esp定律

  • F8过PUSHFD、esp右键-HW break、F9

  • 单步几下到达:有个sub esp,0x58,下面还有一个call(一般就是GetVersion),故可判定VC 6.0

    image-20191122185855905

  • 下面的WinMain也算是Vc6.0的特征<!--more-->

    image-20191122185927427

  • 若pushad与pushfd连着,则fd之后再下断

  • 暂停至popfd后,是一个call,enter过去是lea esp

  • call xxx + lea esp [esp+4] = jmp xxx(混淆指令-化简为繁

    • CALL 001E1CD7 = push 001E1CD7+5(sub esp,0x4)、jmp 001E1CD7
    • LEA ESP,DWORD PTR SS:[ESP+0x4]:add esp,4
  • 有些call不是真正的函数,要F7,而非F8

02-填充IAT的地址是动态变化的

  1. FF 15那个call IAT的地方,数据窗跟随,IAT处异常,IAT被壳修改了

    image-20191122190023454

  2. IAT处下硬件写入断点,重新运行,检查断下的地方是否正确(有时没有那么准确,一下就到断在理想位置;通过上面的地址和值判断

  3. 第一次断在rep movs那,硬件写入为陷阱类,指向下一条,故rep movs的上一条才是;rep movs 物理上一条为JB语句,也不可能是其他地方jmp过来的,jmp指令不可能触发硬件写入,故舍去

    image-20191122191018196

  4. 再一次断在call处,目的地址同1中都是475080,故这就是(源地址不同,但都是xx5039,可能是一个偏移

    image-20191122191140575

  5. 在mov 上设置硬件执行断点,去掉原来的硬件写入,重新运行,发现有时会断下,有时断不下;而硬件写入时,每次都会准确断下,推测此指令所在地址是动态变化的

  6. 重复1-5,反复测试,发现mov这条填充IAT的指令地址是不确定的,但是后面那个0895是确定的,应该是一个偏移,所以推测:壳代码填充IAT的代码是在申请的内存中执行 (偏移+申请内存的首地址 = 最终地址

  7. 验证上述猜想

    • 申请内存用 VirtualAlloc,ctrl+g搜索并下断点,堆栈中观察调用处,是主模块调用再停止,否则放行;
    • enter进入调用处,call下1下断,F9使其跑完此函数,返回值eax=300000,这就是申请空间的基地址
    • 基地址+填充IAT指令的偏移0895=0x300895,ctrl+g到内存窗口,发现都是0
    • F9继续执行,发现0x300895此处内存已改变,右键-反汇编,正是mov 那条填充IAT的指令,验证上述猜想

03-准确找到填充IAT的地方

  1. 调用VirtualAlloc的地方0047A37D CALL EAX ,此处下断,运行至此时F8,得到eax即基地址 0x1F0000

    image-20191120191855196

  2. 基地址+填充IAT指令的偏移,0x1F0000 + 0x0895 = 0x1F0895,跳到此处,下硬件执行(此时刚申请完内存,具体代码还没有拷过去,所以是一堆00,不打紧,照样下断

    image-20191120191920293

  3. F9运行,发现断在0x1F0895(此时就有代码了

    image-20191120191815132

  4. 这样就可保证,每次都会断在填充IAT处(不像一开始有时候断成功,有时候失败

04-壳代码中自定义了API

  1. GetProcAddr用来获取API地址,若要填充IAT(别管是不是修改后再填充,都会用到此
  2. ctrl+g,搜索,下断,堆栈中,非主模块的调用放行,测试发现,主模块并没有调用此API(一直放行,都放到填充IAT指令那了都没出现),推测:壳代码中自己实现了一个GetProcAddress
  3. 同样的思路,GetModuleHandleA和W下断,也没找到(真实OEP之后的调用肯定不是,在到达真实OEP前,已经填充完IAT了,也就已经调用完GetModuleHandleA了

05-找获取API地址的地方

  1. 每次运行,都要获取壳代码申请的内存空间的首地址,此时为1f0000(有随机性,每次申请的都不一样,故如此(系统有伪随机性,何况是在虚拟中,尽管如此,也要获取,确保万无一失

    image-20191122192636153

  2. 填充IAT的位置偏移0895+基地址1f0000=1f0895,下硬件执行,运行到此

    image-20191122193041387

    image-20191122193108680

  3. F7单步向下执行,发现进入循环,暂不看代码窗口,只关注堆栈窗口,发现是挨个字符的判断(一个函数名中全部字符的循环

  4. 运行到向上跳的jmp时,就是循环语句,在本jmp指令与它跳往的地方,二者之间,找JXX条件跳转语句,JXX一般就是跳出循环的

    image-20191120202646938

  5. 在跳出的xx1ccc下断,F9测试一下,发现就不是一个函数名中各个字符的循环了,而是一个个函数名的循环,表示确实是跳出了内层的循环

  6. 在4的基础上继续F7单步,继续找负责本层循环的jxx条件跳转语句(这一个逻辑正好与4相反,4:跳则出循环,5:不跳才是出循环,但实质相同

    image-20191120203141485

  7. 4的循环时一个函数名字符串中各个字符的循环,5的循环是多个函数名的循环,猜测,接下来就是多个dll的循环了(函数名称字符串有多个字符、一个dll有多个函数、一个exe有多个dll)

  8. 在5的基础上,继续F7单步,此时就要慢点了,快要到关键点了,要时刻关注寄存器窗口,肯定会出现xxx.yyy此种形式;发现1911执行后,eax中出现xxx.yyy

    image-20191120204525809

  9. 这就是所谓获取API地址的地方(不管这个值是怎么出来的,我只知道,运行到此时,eax寄存器中有我想要的东西:原始的API地址)

06-OD脚本自动化修复IAT

(将真实API的地址添加到IAT,而非壳加密后的值

 

(要运行至壳OEP时再执行脚本,前面那个系统断点用OD的F9跳过

// 基础模版

// 1. 找到三个地址
MOV dwGetAPIAddr,004385BF
MOV dwWriteIATAddr, 004385F0
MOV dwOEP,00409486

// 2. 设置断点(设置之前先清除所有
BC    // 清除所有软件断点
BPHWC // 清除所有硬件断点
BPMC  // 清除所有内存断点

BPHWS dwGetAPIAddr, "x" //当执行到此地址时产生中断.
BPHWS dwWriteIATAddr, "x" //当执行到此地址时产生中断.
BPHWS dwOEP, "x" //当执行到此地址时产生中断.

// 3. 循环
LOOP_START:
  RUN   // 运行,即F9
  CMP dwGetAPIAddr,eip
  JNZ case1
  MOV dwTmp,eax// 将真实API地址保存至临时变量
  JMP LOOP_START
case1:
  CMP dwWriteIATAddr,eip  
  JNZ case2  
  MOV [edi],dwTmp// 将真实API地址填充至IAT表
  JMP LOOP_START
case2:
  CMP dwOEP,eip  
  JNZ LOOP_START  // 继续循环
  MSG "到达OEP!脚本结束"// 若达到OEP则结束
// 依据不同的壳,在基础版本上加以修改
// 1. 找到三个地址
MOV dwGetAPIAddr, 001A36        // 获取真实API地址(偏移
MOV dwWriteIATAddr, 000897    // 填充IAT(偏移
MOV dwOEP, 0047148B                    // OEP(真实地址
MOV dwBase,0047A37F                    // 壳申请的内存空间首地址(真实地址

// 2. 设置断点
BC    // 清除所有软件断点
BPHWC // 清除所有硬件断点
BPMC  // 清除所有内存断点

BPHWS dwOEP, "x"     //OEP设置断点,此地址不会变
BPHWS dwBase, "x" //申请内存空间返回基地址,是后面两个位置的基础

// 3. 循环
LOOP_START:
  RUN       // 继续运行,直至下个断点

  CMP dwBase,eip// 内存空间申请完成,返回基地址
  JNZ case0
  ADD dwGetAPIAddr,eax  // 基址+偏移 = 最终地址
  ADD dwWriteIATAddr,eax    
  BPHWS dwGetAPIAddr, "x" // 有了最终地址,再下断点
  BPHWS dwWriteIATAddr, "x" 
  JMP LOOP_START // 断点都下完了,继续执行
case0:
  CMP dwGetAPIAddr,eip
  JNZ case1
  MOV dwTmp,eax // 获取真实API地址,赋值给临时变量
  JMP LOOP_START // 继续执行
case1:
  CMP dwWriteIATAddr,eip  
  JNZ case2  
  MOV [edx],dwTmp// 将临时变量中的真实API地址填充到IAT
  JMP LOOP_START//继续
case2:
  CMP dwOEP,eip  // 若到达OEP,则前面都运行完,结束
  JNZ LOOP_START  // 否则继续执行
  MSG "到达OEP!脚本结束"// 弹窗,标示已结束

如图,修复完成(OEP是结束条件,所以正好运行至OEP

 

image-20191122213958158

07-dump内存

  • 此时IAT表是正常的,dump下来后,通过此正常的IAT来修复输入表

    image-20191122214212076

08-修复输入表

image-20191122215400920

 

脱壳成功

 

image-20191122215429251

09-参考

  • 15PB 脱壳课程

[公告]看雪论坛2020激励机制上线了!多多参与讨论可以获得积分快速升级?

最新回复 (1)
雪    币: 40
活跃值: 活跃值 (37)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
killpy 活跃值 2 2020-2-25 23:47
2
0
MOV dwBase,0047A37F                    // 壳申请的内存空间首地址(真实地址
这个每次都不一样 你怎么可以写成固定的呢?
wei为啥要运行至壳OEP时再执行脚本?运行到oep的时候 iat 都被壳处理完了 你此时运行脚本 根本硬断断不下来吧?
游客
登录 | 注册 方可回帖
返回