学破解第206天,《IDA与OD同步调试及常见反调试手段》学习

查看 71|回复 7
作者:小菜鸟一枚   
前言:
  坛友们,年轻就是资本,和我一起逆天改命吧,我的学习过程全部记录及学习资源:https://www.52pojie.cn/thread-1791705-1-1.html
立帖为证!--------记录学习的点点滴滴
0x1 联调准备
  1.在分析ctf题目时,经常需要动态和静态分析结合判断函数的作用,参数的传递,堆栈变化等等,这个时候如果有一款插件能够让OD和IDA同时调试,会大大减轻我们来回调试的工作量,也更方便我们进行对比。
  2.ret-sync 是一组插件,可帮助将调试会话 (WinDbg/GDB/LLDB/OllyDbg2/x64dbg) 与 IDA/Ghidra/Binary Ninja 反汇编程序同步。
  3.下载地址:https://github.com/bootleg/ret-sync,我们只需要IDA和od支持,可以打开文件夹复制链接到:https://minhaskamal.github.io/DownGit/#/home,然后就可以很方便的下载了我们想要的文件,不需要整个代码库下载下来。
  4.安装方式:
把ext_ida文件夹复制到IDA目录
把ext_olly1里编译的dll复制ollydbg的plugin里
打开IDA拖入调试程序, Alt+F7选择SyncPlugin.py (好像要退出一次, 必须有idb)
打开OD, 拖入程序, 点击OD窗口标题, Alt+s开启同步(Alt+U关闭同步)
  5.折腾了一晚上几个小时还不行,明天晚上直接装论坛7.7的IDA,自带全插件。
0x2 实战分析
  1.以BUUCTF上的Crackme为例,进来后F5看看代码:
int wmain()
{
  FILE *v0; // eax
  FILE *v1; // eax
  char v3; // [esp+3h] [ebp-405h]
  char v4[256]; // [esp+4h] [ebp-404h] BYREF
  char Format[256]; // [esp+104h] [ebp-304h] BYREF
  char v6[256]; // [esp+204h] [ebp-204h] BYREF
  char v7[256]; // [esp+304h] [ebp-104h] BYREF
  printf("Come one! Crack Me~~~\n");
  memset(v7, 0, sizeof(v7));
  memset(v6, 0, sizeof(v6));
  while ( 1 )
  {
    do
    {
      do
      {
        printf("user(6-16 letters or numbers):");
        scanf("%s", v7);
        v0 = (FILE *)sub_4024BE();
        fflush(v0);
      }
      while ( !(unsigned __int8)sub_401000(v7) );
      printf("password(6-16 letters or numbers):");
      scanf("%s", v6);
      v1 = (FILE *)sub_4024BE();
      fflush(v1);
    }
    while ( !(unsigned __int8)sub_401000(v6) );
    sub_401090(v7);
    memset(Format, 0, sizeof(Format));
    memset(v4, 0, sizeof(v4));
    v3 = ((int (__cdecl *)(char *, char *))loc_4011A0)(Format, v4);
    if ( (unsigned __int8)sub_401830(v7, v6) )
    {
      if ( v3 )
        break;
    }
    printf(v4);
  }
  printf(Format);
  return 0;
}
  2.通过题目描述和运行程序,可以知道要求输入用户名welcomebeijing,密码也是6-16位字符,如果不正确会提示Please try again,因此可知v7是welcomebeijing,v6是输入的密码。

Come one! Crack Me~~~
user(6-16 letters or numbers):welcomebeijing
password(6-16 letters or numbers):welcomebeijing
Please try again
user(6-16 letters or numbers):
  3.接下来开始同步调试,od插件目录放编译后的dll,发现是动态基址,所以需要用study PE固定基址,打开ida sync,od启动插件,发现ida变黄了,说明同步成功了,接下来直接运行到输入用户名和密码后的while循环处,密码这里输入6个a,然后看看v6和v7做了什么处理。
while ( !(unsigned __int8)sub_401000(v6) );
    sub_401090(v7);
00401D57   .  50            push eax                                 ;  eax就是我输入的6个a
00401D58   .  E8 A3F2FFFF   call crack2.00401000
00401D5D   .  83C4 04       add esp,0x4                              ;  如果al不为0,那么这里就失败,需要重新输入密码
00401D60   .  0FB6C8        movzx ecx,al
00401D63   .  85C9          test ecx,ecx                             ;  crack2.00402331
00401D65   .  75 05         jnz short crack2.00401D6C
00401D67   .^ E9 4EFFFFFF   jmp crack2.00401CBA
  4.通过上面的代码和IDA中同步显示的流程图可以知道如果call 401000后,没有清空eax寄存器,那么程序流程就跳到重新输入用户名和密码的地方去了,ida进去看看,发现就是判断我们输入的密码是不是字母或数字,如果没有问题就执行下一句代码处理v7。
v2 = strlen(a1);
  for ( i = 0; i
  5.进来看一看V7的call crack2.00401090处理,光频肉眼看不出来啥,OD F7进去单步慢慢看,可知第一个for循环416050这个地址存了0-255,然后do while循环没看懂具体干了啥,接下来看while循环,result=v3,所以里面的if必须成立,然后发现不做任何处理,最后返回值也是0,不会影响逻辑。
_BYTE *__cdecl sub_401090(_BYTE *a1)
{
  _BYTE *result; // eax
  int v2; // [esp+Ch] [ebp-18h]
  int v3; // [esp+10h] [ebp-14h]
  _BYTE *v4; // [esp+14h] [ebp-10h]
  int i; // [esp+18h] [ebp-Ch]
  char v7; // [esp+20h] [ebp-4h]
  char v8; // [esp+22h] [ebp-2h]
  unsigned __int8 v9; // [esp+23h] [ebp-1h]
  for ( i = 0; i = v4 - (a1 + 1) )
      v3 = 0;
    ++v2;
  }
  return result;
}
  6.跳出call,继续往后看,两个数据拷贝操作,call 004011A0执行完,出现了提示语成功和失败,然而并没有看到相关运算,所以推测只是把字符取出来,一会运算后进行提示,那么再往下看就是一个非常关键的if判断了,因为执行完这个if后才能跳出循环,而且call 00401830同时将用户名和密码传参进去,肯定是关键处理,进去看看
bool __cdecl sub_401830(int a1, const char *a2)
{
  int v3; // [esp+18h] [ebp-22Ch]
  int v4; // [esp+1Ch] [ebp-228h]
  int v5; // [esp+28h] [ebp-21Ch]
  unsigned int v6; // [esp+30h] [ebp-214h]
  char v7; // [esp+36h] [ebp-20Eh]
  char v8; // [esp+37h] [ebp-20Dh]
  char v9; // [esp+38h] [ebp-20Ch]
  unsigned __int8 v10; // [esp+39h] [ebp-20Bh]
  unsigned __int8 v11; // [esp+3Ah] [ebp-20Ah]
  char v12; // [esp+3Bh] [ebp-209h]
  int v13; // [esp+3Ch] [ebp-208h] BYREF
  char v14; // [esp+40h] [ebp-204h] BYREF
  char v15[255]; // [esp+41h] [ebp-203h] BYREF
  char v16[256]; // [esp+140h] [ebp-104h] BYREF
  v4 = 0;
  v5 = 0;
  v11 = 0;
  v10 = 0;
  memset(v16, 0, sizeof(v16));
  v14 = 0;
  memset(v15, 0, sizeof(v15));
  v9 = 0;
  v6 = 0;
  v3 = 0;
  while ( v6 ProcessHeap + 3) != 2 )
        a2[v6] = 34;
      v8 = (a2[v6] | 0x20) - 87;
    }
    else
    {
      v8 = ((a2[v6] | 0x20) - 97) % 6 + 10;
    }
    __rdtsc();
    __rdtsc();
    v9 = v8 + 16 * v9;
    if ( !((int)(v6 + 1) % 2) )
    {
      v15[v3++ - 1] = v9;
      v9 = 0;
    }
    ++v6;
  }
  while ( v5 NtGlobalFlag & 0x70) != 0 )
      v12 = v10 + v11;
    v16[v5] = byte_416050[(unsigned __int8)(v7 + v12)] ^ v15[v4 - 1];
    if ( (unsigned __int8)*(_DWORD *)&NtCurrentPeb()->BeingDebugged )
    {
      v10 = -83;
      v11 = 43;
    }
    sub_401710(v16, a1, v5++);
    v4 = v5;
    if ( v5 >= (unsigned int)(&v15[strlen(&v14)] - v15) )
      v4 = 0;
  }
  v13 = 0;
  sub_401470(v16, &v13);
  return v13 == 0xAB94;
}
  7.似乎发现了不得了的东西:反调试,先暂停分析,补一补反调试知识。
0x3 反调试
  1.IsDebuggerPresent函数
IsDebuggerPresent查询进程环境块(PEB)中的IsDebugged标志。如果进程没有运行在调试器环境中,
函数返回0;如果调试附加了进程,函数返回一个非零值。
  2.CheckRemoteDebuggerPresent函数
CheckRemoteDebuggerPresent同IsDebuggerPresent几乎一致。它不仅可以探测系统其他进程是否被
调试,通过传递自身进程句柄还可以探测自身是否被调试。
  3.NtQueryInformationProcess函数
这个函数是Ntdll.dll中一个原生态API,它用来提取一个给定进程的信息。它的第一个参数是进程句柄,
第二个参数告诉我们它需要提取进程信息的类型。为第二个参数指定特定值并调用该函数,相关信息就
会设置到第三个参数。第二个参数是一个枚举类型,其中与反调试有关的成员有
ProcessDebugPort(0x7)、ProcessDebugObjectHandle(0x1E)和ProcessDebugFlags(0x1F)。
例如将该参数置为ProcessDebugPort,如果进程正在被调试,则返回调试端口,否则返回0。
  4.GetLastError函数
使用SetLastError函数,将当前的错误码设置为一个任意值。
如果进程没有被调试器附加,错误码会重新设置,因此GetLastError获取的错误码应该不是我们设置的任意值。
但如果进程被调试器附加,多数调试器默认的设置是捕获异常后不将异常传递给应用程序,这时GetLastError获取的错误码应该没改变。
对于DeleteFiber函数,如果给它传递一个无效的参数的话会抛出ERROR_INVALID_PARAMETER异常。
如果进程正在被调试的话,异常会被调试器捕获。所以,同样可以通过验证LastError值来检测调试器的存在。
0x57就是指ERROR_INVALID_PARAMETER,可以通过GetLastError() != 0x57判断
  5.ZwSetInformationThread函数
ZwSetInformationThread拥有两个参数,第一个参数用来接收当前线程的句柄,第二个参数表示线程信息类型,
若其值设置为ThreadHideFromDebugger(0x11),使用语句ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, NULL, 0);
调用该函数后,调试进程就会被分离出来。该函数不会对正常运行的程序产生任何影响,但若运行的是调试器程
序,因为该函数隐藏了当前线程,调试器无法再收到该线程的调试事件,最终停止调试。
还有一个函数DebugActiveProcessStop用来分离调试器和被调试进程,从而停止调试。两个API容易混淆,需要牢记它们的区别。
  6.检测BeingDebugged属性
Windows操作系统维护着每个正在运行的进程的PEB结构,它包含与这个进程相关的所有用户态参数。
这些参数包括进程环境数据,环境数据包括环境变量、加载的模块列表、内存地址,以及调试器状态。
  7.检测ProcessHeap属性
Reserved数组中一个未公开的位置叫作ProcessHeap,它被设置为加载器为进程分配的第一个堆的位置。
ProcessHeap位于PEB结构的0x18处。第一个堆头部有一个属性字段,它告诉内核这个堆是否在调试器中创建。
这些属性叫作ForceFlags和Flags。在Windows XP系统中,ForceFlags属性位于堆头部偏移量0x10处;
在Windows 7系统中,对于32位的应用程序来说ForceFlags属性位于堆头部偏移量0x44处
  8.还有一些例如易语言时钟检测,也能进行反调试。
0x4 继续分析
  1.通过学习反调试知识,可知此处有三个反调试,可以直接手动nop掉对应代码或者不管它,论坛的OD能过掉很多反调试。
if ( *((_DWORD *)NtCurrentPeb()->ProcessHeap + 3) != 2 )
if ( (NtCurrentPeb()->NtGlobalFlag & 0x70) != 0 )
if ( (unsigned __int8)*(_DWORD *)&NtCurrentPeb()->BeingDebugged )
  2.这个函数肯定也是要返回true的,那么最后return语句的前一句就很关键,可以看到经过诸多判断后,v13是0,经过运算后要等与0125624,那么v16的来源就很关键,但是这中间都经历了一个sub_401710函数,也需要关注。
v13 = 0;
  ((void (__cdecl *)(char *, int *))sub_401470)(v16, &v13);
  return v13 == 0125624;
可以看到v16在这里得到初值:
  v16[v5] = byte_416050[(unsigned __int8)(v7 + v12)] ^ v15[v4 - 1];
  3.sub_401470函数进去可以看到如下信息(去掉反调试和干扰代码整理后),可以看到a2(第二个参数就是传递来的v16了)就是不断进行比较,接下来大胆假设a2就是相等的,那么v16就是dbappsec了。
unsigned int *__usercall sub_401470@(int a1@, _BYTE *a2, unsigned int *a3)
{
  int *_EAX; // eax
  char v5; // al
  char _AL; // al
  unsigned int *result; // eax
  if ( *a2 != 'd' )
    *a3 ^= 3u;
  else
    *a3 |= 4u;
  if ( a2[1] != 'b' )
  {
    *a3 &= 0x61u;
    _EAX = (int *)*a3;
  }
  else
  {
    _EAX = (int *)a3;
    *a3 |= 0x14u;
  }
  if ( a2[2] != 'a' )
    *a3 &= 0xAu;
  else
    *a3 |= 0x84u;
  if ( a2[3] != 'p' )
    *a3 >>= 7;
  else
    *a3 |= 0x114u;
  if ( a2[4] != 'p' )
    *a3 *= 2;
  else
    *a3 |= 0x380u;
  if ( a2[5] != 's' )
  {
    v5 = (char)a3;
    *a3 ^= 0x1ADu;
  }
  else
  {
    *a3 |= 0xA04u;
    v5 = (char)a3;
  }
  if ( a2[6] != 'e' )
    *a3 |= 0x4Au;
  else
    *a3 |= 0x2310u;
  if ( a2[7] != 'c' )
  {
    *a3 &= 0x3A3u;
    return (unsigned int *)*a3;
  }
  else
  {
    result = a3;
    *a3 |= 0x8A10u;
  }
  return result;
}
  4.byte_416050是一个动态数组,我们调试看一看,v16到底怎么来的,OD往下翻,00401A4E这一行对应的while ( v5
00401A4E   >  83BD E4FDFFFF>cmp dword ptr ss:[ebp-0x21C],0x8
00401A55   . |0F8D B0010000 jge crack2.00401C0B
00401A5B   . |0FB695 F6FDFF>movzx edx,byte ptr ss:[ebp-0x20A]
00401A62   . |83C2 01       add edx,0x1
00401A65   . |8895 F6FDFFFF mov byte ptr ss:[ebp-0x20A],dl
00401A6B   . |0FB685 F5FDFF>movzx eax,byte ptr ss:[ebp-0x20B]
00401A72   . |0FB68D F6FDFF>movzx ecx,byte ptr ss:[ebp-0x20A]
00401A79   . |0FB691 506041>movzx edx,byte ptr ds:[ecx+0x416050]
00401AE8   . /74 16         je short crack2.00401B00                 ;  
00401AEA   . |0FB695 F6FDFF>movzx edx,byte ptr ss:[ebp-0x20A]
00401AF1   . |0FB685 F5FDFF>movzx eax,byte ptr ss:[ebp-0x20B]
00401AF8   . |03D0          add edx,eax
00401AFA   . |8895 F7FDFFFF mov byte ptr ss:[ebp-0x209],dl
00401B00   > \0FB68D F7FDFF>movzx ecx,byte ptr ss:[ebp-0x209]        
  5.从401B00这里开始就很关键了,一定到定位到v16赋值的地方,下面逐行解读汇编代码
00401B00   > \0FB68D F7FDFF>movzx ecx,byte ptr ss:[ebp-0x209]        ;  ecx=v12
00401B07   .  0FB695 F2FDFF>movzx edx,byte ptr ss:[ebp-0x20E]        ;  edx=v7
00401B0E   .  03CA          add ecx,edx                              ;  ecx=v7+v12
00401B10   .  888D F7FDFFFF mov byte ptr ss:[ebp-0x209],cl
00401B16   .  0FB685 F7FDFF>movzx eax,byte ptr ss:[ebp-0x209]        ;  eax=ecx
00401B1D   .  8A88 50604100 mov cl,byte ptr ds:[eax+0x416050]
00401B23   .  888D F7FDFFFF mov byte ptr ss:[ebp-0x209],cl
00401B29   .  8B95 D8FDFFFF mov edx,dword ptr ss:[ebp-0x228]         ;  edx=v4
00401B2F   .  0FB68415 FCFD>movzx eax,byte ptr ss:[ebp+edx-0x204]    ;  eax=v15[v4 - 1]
00401B37   .  0FB68D F7FDFF>movzx ecx,byte ptr ss:[ebp-0x209]        ;  ecx=byte_416050[(unsigned __int8)(v7 + v12)]
00401B3E   .  33C1          xor eax,ecx                              ;  v16=eax^ecx
  6.我们已知v16是dbappsec,通过F2下断点能读出ecx的值0x2a,0xd7,0x92,0xe9,0x53,0xe2,0xc4,0xcd,v15就是密码。
if ( isdigit(a2[v6]) )
    {
      v8 = a2[v6] - 48;
    v9 = v8 + 16 * v9;
    if ( !((int)(v6 + 1) % 2) )  //v6如果为奇数,条件成立
    {
      v15[v3++ - 1] = v9; v15就是前一位*16+后一位
      v9 = 0;
    }
    ++v6;
  7.所以最后可写出如下脚本,用C实在不知道咋转16进制,懵逼,还好java基础也懂一点,勉强写出来了。
package ctf;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Iterator;
public class test01 {
        public static void main(String[] args) throws NoSuchAlgorithmException, UnsupportedEncodingException {
            int arr[] = {0x2a,0xd7,0x92,0xe9,0x53,0xe2,0xc4,0xcd};
            char password[] = {'d','b','a','p','p','s','e','c'};
            byte flag[] = new byte[8];
                for (int i = 0; i = 0 && number
  8.运行后输出:4eb5f3992391a1ae,放到md5在线加密网站,加密得到flag:flag{d2be2981b84f2a905669995873d6a36c}
0x5 参考资料:
  1.OD和IDA调试同步插件
  2.BUUCTF--crackMe
  3.OD ret-sync同步插件
  4.IDA Pro 7.7.220118 (SP1) 全插件绿色版

函数, 进程

semmy   

能够坚持下来,真心非常厉害,赞一个,讲得也非常不错。
moruye   

厉害厉害
GJH588   

不错,能坚持学习很好
a2639339247   

感谢分享!
bnjzzheng   

感感谢分享,感谢分享
GuiXiaoQi   

大佬就是大佬,牛逼
sRGB   

被关了半年小黑屋,我现在回来学习了
您需要登录后才可以回帖 登录 | 立即注册

返回顶部