小菜鸟一枚 发表于 2023-6-2 18:27

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

前言:

  坛友们,年轻就是资本,和我一起逆天改命吧,我的学习过程全部记录及学习资源:(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; //
char v4; // BYREF
char Format; // BYREF
char v6; // BYREF
char v7; // 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是输入的密码。

![https://s1.ax1x.com/2023/06/02/pCS30II.jpg](https://s1.ax1x.com/2023/06/02/pCS30II.jpg)

```
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 < v2; ++i )
{
    if ( !isalnum(a1) )
      return 0;
}
```

  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; //
int v3; //
_BYTE *v4; //
int i; //
char v7; //
char v8; //
unsigned __int8 v9; //

for ( i = 0; i < 256; ++i )
    byte_416050 = i;
v2 = 0;
v9 = 0;
v3 = 0;
result = a1;
v4 = a1;
do
    LOBYTE(result) = *v4;
while ( *v4++ );
while ( v2 < 256 )
{
    v8 = byte_416050;
    v9 += v8 + a1;
    v7 = byte_416050;
    ++v3;
    byte_416050 = v8;
    byte_416050 = v7;
    result = (_BYTE *)v3;
    if ( v3 >= 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; //
int v4; //
int v5; //
unsigned int v6; //
char v7; //
char v8; //
char v9; //
unsigned __int8 v10; //
unsigned __int8 v11; //
char v12; //
int v13; // BYREF
char v14; // BYREF
char v15; // BYREF
char v16; // 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 < strlen(a2) )
{
    if ( isdigit(a2) )
    {
      v8 = a2 - 48;
    }
    else if ( isxdigit(a2) )
    {
      if ( *((_DWORD *)NtCurrentPeb()->ProcessHeap + 3) != 2 )
      a2 = 34;
      v8 = (a2 | 0x20) - 87;
    }
    else
    {
      v8 = ((a2 | 0x20) - 97) % 6 + 10;
    }
    __rdtsc();
    __rdtsc();
    v9 = v8 + 16 * v9;
    if ( !((int)(v6 + 1) % 2) )
    {
      v15 = v9;
      v9 = 0;
    }
    ++v6;
}
while ( v5 < 8 )
{
    v10 += byte_416050[++v11];
    v12 = byte_416050;
    v7 = byte_416050;
    byte_416050 = v12;
    byte_416050 = v7;
    if ( (NtCurrentPeb()->NtGlobalFlag & 0x70) != 0 )
      v12 = v10 + v11;
    v16 = byte_416050[(unsigned __int8)(v7 + v12)] ^ v15;
    if ( (unsigned __int8)*(_DWORD *)&NtCurrentPeb()->BeingDebugged )
    {
      v10 = -83;
      v11 = 43;
    }
    sub_401710(v16, a1, v5++);
    v4 = v5;
    if ( v5 >= (unsigned int)(&v15 - 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 = byte_416050[(unsigned __int8)(v7 + v12)] ^ v15;
```

  3.sub_401470函数进去可以看到如下信息(去掉反调试和干扰代码整理后),可以看到a2(第二个参数就是传递来的v16了)就是不断进行比较,接下来大胆假设a2就是相等的,那么v16就是dbappsec了。

```
unsigned int *__usercall sub_401470@<eax>(int a1@<ebx>, _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 != 'b' )
{
    *a3 &= 0x61u;
    _EAX = (int *)*a3;
}
else
{
    _EAX = (int *)a3;
    *a3 |= 0x14u;
}

if ( a2 != 'a' )
    *a3 &= 0xAu;
else
    *a3 |= 0x84u;
if ( a2 != 'p' )
    *a3 >>= 7;
else
    *a3 |= 0x114u;
if ( a2 != 'p' )
    *a3 *= 2;
else
    *a3 |= 0x380u;

if ( a2 != 's' )
{
    v5 = (char)a3;
    *a3 ^= 0x1ADu;
}
else
{
    *a3 |= 0xA04u;
    v5 = (char)a3;
}

if ( a2 != 'e' )
    *a3 |= 0x4Au;
else
    *a3 |= 0x2310u;
if ( a2 != 'c' )
{
    *a3 &= 0x3A3u;
    return (unsigned int *)*a3;
}
else
{
    result = a3;
    *a3 |= 0x8A10u;
}
return result;
}
```

  4.byte_416050是一个动态数组,我们调试看一看,v16到底怎么来的,OD往下翻,00401A4E这一行对应的while ( v5 < 8 ),然后继续往下慢慢看,对比ida里面的代码看,可知这是一个反调试,if后面就是v16赋值的那一句了。

```
00401A4E   >83BD E4FDFFFF>cmp dword ptr ss:,0x8
00401A55   . |0F8D B0010000 jge crack2.00401C0B
00401A5B   . |0FB695 F6FDFF>movzx edx,byte ptr ss:
00401A62   . |83C2 01       add edx,0x1
00401A65   . |8895 F6FDFFFF mov byte ptr ss:,dl
00401A6B   . |0FB685 F5FDFF>movzx eax,byte ptr ss:
00401A72   . |0FB68D F6FDFF>movzx ecx,byte ptr ss:
00401A79   . |0FB691 506041>movzx edx,byte ptr ds:


00401AE8   . /74 16         je short crack2.00401B00               ;
00401AEA   . |0FB695 F6FDFF>movzx edx,byte ptr ss:
00401AF1   . |0FB685 F5FDFF>movzx eax,byte ptr ss:
00401AF8   . |03D0          add edx,eax
00401AFA   . |8895 F7FDFFFF mov byte ptr ss:,dl
00401B00   > \0FB68D F7FDFF>movzx ecx,byte ptr ss:      
```

  5.从401B00这里开始就很关键了,一定到定位到v16赋值的地方,下面逐行解读汇编代码

```
00401B00   > \0FB68D F7FDFF>movzx ecx,byte ptr ss:      ;ecx=v12
00401B07   .0FB695 F2FDFF>movzx edx,byte ptr ss:      ;edx=v7
00401B0E   .03CA          add ecx,edx                              ;ecx=v7+v12
00401B10   .888D F7FDFFFF mov byte ptr ss:,cl
00401B16   .0FB685 F7FDFF>movzx eax,byte ptr ss:      ;eax=ecx
00401B1D   .8A88 50604100 mov cl,byte ptr ds:
00401B23   .888D F7FDFFFF mov byte ptr ss:,cl
00401B29   .8B95 D8FDFFFF mov edx,dword ptr ss:         ;edx=v4
00401B2F   .0FB68415 FCFD>movzx eax,byte ptr ss:    ;eax=v15
00401B37   .0FB68D F7FDFF>movzx ecx,byte ptr ss:      ;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) )
    {
      v8 = a2 - 48;

    v9 = v8 + 16 * v9;
    if ( !((int)(v6 + 1) % 2) )//v6如果为奇数,条件成立
    {
      v15 = 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;
               
                for (int i = 0; i < password.length; i++) {
                        flag=(byte) (password^arr);
                }
               
               
               
                System.out.println(bytesToHex(flag));
      }
      
      public static String bytesToHex(byte bytes[]) {
      StringBuffer sb = new StringBuffer();
      for (int i = 0; i < bytes.length; i++) {
            int number = bytes & 0xff;
            String hex = Integer.toHexString(number);
            if (number >= 0 && number < 16) {
                sb.append("0" + hex);
            } else {
                sb.append(hex);
            }
      }
      return sb.toString();
    }
}
```

  8.运行后输出:4eb5f3992391a1ae,放到md5在线加密网站,加密得到flag:flag{d2be2981b84f2a905669995873d6a36c}

## 0x5 参考资料:
  1.(http://strivemario.work/archives/b40b8192.html)

  2.(https://blog.csdn.net/qq_39542714/article/details/106834898)

  3.(https://wwtc.lanzoum.com/iV3sJ0y0rfzc)

  4.(https://www.52pojie.cn/thread-1584115-1-1.html)

柳叶刀 发表于 2023-6-4 01:06


谢谢分享,我今天也用到10转16了.
_itoa_s(待转换十进制数,存放转换结果的数组或指针,16);

semmy 发表于 2023-6-2 18:59

能够坚持下来,真心非常厉害,赞一个{:1_921:},讲得也非常不错。

moruye 发表于 2023-6-2 20:11

GJH588 发表于 2023-6-2 21:57

不错,能坚持学习很好

a2639339247 发表于 2023-6-2 22:17

bnjzzheng 发表于 2023-6-2 22:26

GuiXiaoQi 发表于 2023-6-2 22:30

大佬就是大佬,牛逼

sRGB 发表于 2023-6-2 23:07

不会想象的明天 发表于 2023-6-3 00:10

holen2024 发表于 2023-6-3 00:34

页: [1] 2 3
查看完整版本: 学破解第206天,《IDA与OD同步调试及常见反调试手段》学习