Self-Modified Code

简介

自修改代码(Self-Modified Code)是一类特殊的代码技术,即在运行时修改自身代码,从而使得程序实际行为与反汇编结果不符,同时修改前的代码段数据也可能非合法指令,从而无法被反汇编器识别,这加大了软件逆向工程的难度。

自修改代码通常有两种破解方式,第一种是根据静态分析结果直接修改程序二进制文件,第二种则是在动态调试时将解密后的程序从内存中 dump 下来。

例题:GWCTF 2019 - re3

惯例拖入 IDA,主函数逻辑比较简单,主要是利用 mprotect() 修改代码段权限后再改写 sub_402219() 开头的 234 字节数据,最后再调用 sub_40207B()sub_402219()

void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
  int i; // [rsp+8h] [rbp-48h]
  char s[40]; // [rsp+20h] [rbp-30h] BYREF
  unsigned __int64 v5; // [rsp+48h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  __isoc99_scanf("%39s", s);
  if ( (unsigned int)strlen(s) != 32 )
  {
    puts("Wrong!");
    exit(0);
  }
  mprotect(&dword_400000, 0xF000uLL, 7);
  for ( i = 0; i <= 223; ++i )
    *((_BYTE *)sub_402219 + i) ^= 0x99u;
  sub_40207B(&unk_603170);
  sub_402219(s);
}

此时我们可以看到 sub_402219() 处数据全都无法被识别:

按照 main() 逻辑将这块区域进行修改:

这个时候我们便能获得一个正常的函数了:

sub_402219() 开头按 alt + p 重新定义函数范围(也可以先按 u 取消原有定义后再按 p 重建定义),此时我们便能获得正确的反编译结果:

sub_402219() 首先会调用 sub_400A71(),最终会调用到 sub_4007C6() ,主要作用就是把 0x603170 处的 16 字节拷贝到栈上,这里就不展开了。

接下来会调用两次 sub_40196E() ,第一个参数为拷贝到栈上的 0x603170 处的 16 字节,第二个参数分别为我们的输入 &s[0]&s[16]。该函数最后会调用到 sub_401828(),非常明显的 AES 加密特征: 初始变换→9轮循环运算→最终运算,调用的几个函数也符合 AES 内部逻辑,这里就不展开了:

sub_402219() 中最后会将加密后的输入与 0x6030A0 处数据进行对比:

那么加密密钥 0x603170 上是什么数据呢?我们回到主函数,在调用 sub_402219() 之前首先会调用 sub_40207B(),其主要逻辑是调用了好几次 sub_401CF9(), 传入的参数都是位于 data 段上的数据:

而看到 sub_401CF9() 开头的四个常数就知道这应该是 md5 算法,仔细一看程序逻辑也是如此:

因此 sub_402219() 的逻辑便清晰了:进行多轮 md5 哈希并将结果存储到 0x603170 处,我们直接动态调试一下获得此处数据:

AES 是对称加密算法,现在我们有了密钥(0x603170),又有了密文(0x6030A0),直接用 python 内置的密码学库即可获得 flag:

Last updated