《从0到1CTFer成长之路》逆向工程个人笔记--静态分析

上一篇文章:《从0到1CTFer成长之路》逆向工程个人笔记--逆向工程基础

IDA 使用入门

加载文件

打开 IDA,点击 GO,即可把程序拖拽到 IDA 中

IDA 分为 32bit 和 64bit 两种架构,选择哪种结构,可以在把程序拖拽到 IDA 后,根据 Load a new file 界面决定是否更换另一种架构

界面介绍

  • 1:导航栏(程序不同类型的数据:普通函数,未定义函数的代码,数据,未定义)
  • 2:反汇编主窗口
  • 3:函数窗口(函数名称+地址)
  • 4:输出窗口(下方输入框可以输入命令)
  • 5:状态指示器(AU:idle 代表 IDA 完成了对程序的自动化分析)

数据类型

在反汇编窗口,右键菜单 或按空格键可以在控制流图和文本界面之间切换

IDA 会将代码段多数数据正确标注为代码类型,并反汇编,特殊段部分位置标注为 8 字节整型 qword,用户可自行纠正

被标注为代码的位置:地址黑色显示

被标注为数据的位置:地址灰色显示

未定义数据类型的位置:显示黄色

定义数据类型的快捷键

  • U 键:取消一个地方已有的数据类型定义
  • D 键:让某一个位置变成数据,一直按,会改变字节大小(1/2/4/8)
  • C 键:让某一个位置变成指令,定义为指令后,IDA 会自动以此为起始位置进行递归下降反汇编
  • A 键:以该位置为起点定义一个以"\0"结尾的字符串类型
  • *键:将此处定义为一个数组,可设置数组属性
  • O 键:将此处定义为一个地址偏移

函数操作

反汇编不是完全连续的,而是由分散的各函数拼凑而成的。

每个函数都有局部变量、调用约定等信息,控制流图也只能以函数为单位生成和显示,所以需要正确定义函数

  • 删除函数:在函数窗口中选中函数后,按 Delete
  • 定义函数:在反汇编窗口中选中对应行后,按 P
  • 修改函数参数:在函数窗口中选中并按 Ctrl+E,或者在反汇编窗口内部按 Alt+P

定义函数后就可以进行调用约定分析、栈变量分析、函数调用参数分析

导航操作

IDA 有导航历史功能,可以后退或前进到某次浏览的位置

  • 后退到上一位置:Esc
  • 前进到下一位置:Ctrl+Enter
  • 跳转到某一特定为止:G,输入地址或已经定义的名称
  • 跳转到某一区段:Ctrl+S

类型操作

处理 C/C++语言的各种数据类型(函数声明、变量声明、结构体声明),并且允许用户自由指定

选中变量或函数后按 Y,在对话框中输入正确的 C 语言类型,IDA 就可以解析并自动应用这个类型

IDAPython

Alt+F7 或 File→Script file 菜单,可以执行 py 脚本文件

Shift+F2 或 File →Script command 菜单,打开脚本面板

其他功能

  • Strings 窗口:Shift+F12 打开,可识别程序中的字符串
  • 十六进制窗口:F2 对数据库数据进行修改,再按 F2 应用修改

HexRays 反编译入门

HexRays 作为 IDA 的插件运行,可以利用 IDA 确定的函数局部变量和数据类型,优化后生成类似 C 语言的伪代码,然后用户就可以浏览生成的伪代码,添加注释,重命名标识符,修改变量类型,切换数据的显示格式

生成伪代码

在反汇编窗口中定位到目标函数,按 F5 即可生成伪代码

每个局部变量后面的注释实际代表这个变量所在的位置

在标识符上按 N,可以更换名称

切换数据显示格式

在常量点击右键,可以切换格式:

  • Hexadecimal 十六进制,
  • Octal 八进制
  • Char:转化为诸如'A'的格式
  • Enum:转化为枚举中的一个值
  • Invert sign:按照补码解析为负数
  • Bitwise negate:按位取反,~0xF0

修改变量类型

当源代码经过复杂的编译器优化流程后(如 GCC 编译器 O3 优化),再进行伪代码生成,会发现代码的阅读性差

int __cdecl main(int argc, const char **argv, const char **envp)
{
  __int64 i; // rsi
  unsigned int v4; // eax
  __m128i si128; // [rsp+0h] [rbp-98h]
  char v7[16]; // [rsp+10h] [rbp-88h] BYREF
  char v8[96]; // [rsp+20h] [rbp-78h] BYREF
  int v9; // [rsp+80h] [rbp-18h]
  unsigned __int64 v10; // [rsp+88h] [rbp-10h]

  v10 = __readfsqword(0x28u);
  strcpy(v7, "nf_ddzbff!}");
  memset(v8, 0, sizeof(v8));
  si128 = _mm_load_si128((const __m128i *)&xmmword_9F0);
  v9 = 0;
  __printf_chk(1LL, "Input your answer: ", envp);
  __isoc99_scanf("%s", v8);
  if ( strlen(v8) != 27 )
  {
    puts("Wrong input length!");
    return 1;
  }
  for ( i = 0LL; i != 28; ++i )
  {
    LOBYTE(v4) = v8[i];
    if ( (unsigned __int8)(v4 - 97) <= 0x19u )
    {
      v4 = (102 * ((char)v4 - 97) + 3) % 0x1Au + 97;
LABEL_4:
      if ( si128.m128i_i8[i] != (_BYTE)v4 )
        goto LABEL_9;
      continue;
    }
    if ( (unsigned __int8)(v4 - 65) > 0x19u )
      goto LABEL_4;
    if ( si128.m128i_i8[i] != (102 * ((char)v4 - 65) + 3) % 0x1Au + 65 )
    {
LABEL_9:
      puts("Wrong answer!");
      return 1;
    }
  }
  puts("Congratulations!");
  return 0;
}

这是因为 HexRays 把字符串数组识别成了 3 个变量,字符串赋值处理操作变成了 128 位浮点数赋值+64 位 qword 赋值+32 位 dword 赋值(__m128i 类型的 v6,__int64 的 v7,__int 的 v8)

  • byte-1 字节整型,8 位,char、__int8
  • word-2 字节整型,16 位,short、__int16
  • dword-4 字节整型,32 位,int、__int32
  • qword-8 字节整型,64 位,__int6,long long

也就是说,__m128i si128,v7,v8 实际上是整个字符串数组,因此我们需要修正变量的类型

在 v6 上按 Y,输入 char[28],28=16+8+4

int __cdecl main(int argc, const char **argv, const char **envp)
{
  __int64 i; // rsi
  unsigned int v4; // eax
  char v6[28]; // [rsp+0h] [rbp-98h] BYREF
  char v7[96]; // [rsp+20h] [rbp-78h] BYREF
  int v8; // [rsp+80h] [rbp-18h]
  unsigned __int64 v9; // [rsp+88h] [rbp-10h]

  v9 = __readfsqword(0x28u);
  strcpy(&v6[16], "nf_ddzbff!}");
  memset(v7, 0, sizeof(v7));
  *(__m128i *)v6 = _mm_load_si128((const __m128i *)&xmmword_9F0);
  v8 = 0;
  __printf_chk(1LL, "Input your answer: ", envp);
  __isoc99_scanf("%s", v7);
  if ( strlen(v7) != 27 )
  {
    puts("Wrong input length!");
    return 1;
  }
  for ( i = 0LL; i != 28; ++i )
  {
    LOBYTE(v4) = v7[i];
    if ( (unsigned __int8)(v4 - 97) <= 0x19u )
    {
      v4 = (102 * ((char)v4 - 97) + 3) % 0x1Au + 97;
LABEL_4:
      if ( v6[i] != (_BYTE)v4 )
        goto LABEL_9;
      continue;
    }
    if ( (unsigned __int8)(v4 - 65) > 0x19u )
      goto LABEL_4;
    if ( v6[i] != (102 * ((char)v4 - 65) + 3) % 0x1Au + 65 )
    {
LABEL_9:
      puts("Wrong answer!");
      return 1;
    }
  }
  puts("Congratulations!");
  return 0;
}

第二段代码通过统一的数据存储方式清晰的变量访问方式简洁的逻辑结构,显著提高了代码的可读性。它更符合人类的思维习惯,减少了理解代码时的认知负担

HexRays 还可以修改参数类型、函数原型、全局变量类型、结构体、枚举

  • Shift+F1:Local Types 窗口,可以操作 C 类型,再按 Insert:添加类型
  • Ctrl+F9:加载 C 语言头文件

IDA 与 HexRays 进阶

寻找 main 函数

很多可执行文件时先经过 CRT 的初始化,再转到 main()函数

寻找 main 函数

  • main()函数经常在可执行文件的靠前位置(因为很多链接器是先处理对象文件后处理静态库)
  • VC 的入口点(IDA 中的 start()函数)会直接调用 main()函数,再 start()函数中被调用的函数有 3 个参数,并且返回值被传入 exit()函数的,可以重点查看
  • GCC 将 main()函数的地址传入 __libc_start_main 来调用 main()函数,查看调用的参数即可找到 main()函数的地址

手动应用 FLIRT 签名

函数列表和导航条底色为青色的区域,与 FLIRT 函数前面识别库有关

Shift+F5 可以打开 Signature 列表,显示已经应用的函数签名库。查看导航条可以发现,VC(Visual C++) 运行时很多代码没有识别,这是因为 IDA 没有自动为这个程序应用其他 VC 运行时的签名。

处理 HexRays 失败情况

对于没有符号、优化等级较高的程序,HexRays 会经常出现各种失败情况,大多数出错原因是与这个函数相关的某些参数设置错误 ,如这个函数中调用其他函数的调用约定 出现错误,导致参数解析失败调用前后栈不平衡

比如:

  • 一个使用 __stdcall 的函数被误认为使用 __cdecl 调用约定,两个调用约定清理参数空间的方法不一样,导致跟踪栈指针时出现问题
  • 一个 __thiscall 被错误识别成 __fastcall。函数多出一个不存在的参数
  • 某个 __fastcall 函数被错误识别成 __cdecl 函数,而参数个数都是 1,这时反编译器没办法找到栈上的参数,因为它实际上使用的是近期传参

下面给出两种常见的错误形式,注意:一开始的代码形式就是正常的,下面会故意改错,然后意在通过错误方式重新纠正代码

call analysis failed

文件:3-UPX_packed_dumo_SCY.exe(用 32 位的 IDA 打开)

刚进入就警告

IDA 检测到文件的 导入表(Imports Segment)可能被破坏或修改。这通常意味着文件可能被加壳(Packed)或经过某种形式的修改,目的是增加逆向分析的难度。这个与我们的主要目的无光,可以忽略此警告

进去之后,通过 start 函数寻找 main 函数,进入scrt_common_main_seh 函数

双击右边的函数进入

双击第 42 行的函数

即可进入 main 函数内部

如果将 sub_271010 的类型从原来的"__thiscall sub_271010",修改为"__cdeccl sub_271010"。也就是修改调用约定(单击第三行,注意不要进入函数,按 Y 键就可以修改)

反编译器就会自动重新反编译进行刷新,然后弹出警告。此时就是我们人为地制作了一个错误,下面我们就来学习根据这个警告,反向地修正这个错误。

反编译失败并提示 call analysis failed 通常意味着反编译器在分析函数调用时遇到了问题,无法正确解析或重建调用关系。

根据警告按 G 跳转到 0x271006

通过刚刚的操作,我们可以明确知道,报错是因为我们修改了函数的调用约定,然后反编译器在寻找函数调用的参数的时候出现了错误,这时我们只要把在 0x271006 地址处的函数原型声明修复就可以了

在上图红框处按 Y,修改为int __thiscall sub_271010(int *this);

然后就可以发现,又可以进入 main 函数了,说明反编译又可以正常进行了。如果不修正的话,会发现我们想进入 main 函数,一定会出现上面的报错

sp-analysis failed

这种错误的原因是,当优化等级较高时,编译器将省略帧指针 rbp 的使用,转而使用 rsp 引用所有的局部变量,为了找到局部变量,IDA 通过跟踪每条指令对 rsp 的修改来查找并解析局部变量。但是 IDA 在跟踪 rsp 时出现了问题,导致反编译失败

根源是某个函数调用的调用阅读出错,或该函数的参数个数出错,导致 IDA 算错了栈指针的变化量

针对这种情况,"Options→General",在新对话框里,勾选"Stack pointer"(堆栈指针)

然后反汇编窗口每行的地址旁边就会多出一列,即 IDA 分析的函数执行到每个地址时栈的偏移量

对于没有使用动态长度数组的正常程序,在初始化完毕,调用前后栈的偏移量不变

遇到此类报错时,就一点点看完栈指针,将其与正常栈指针的变化规律相比较,就可以快速找到有问题的地方

相关推荐
在下陈平安16 分钟前
java-LinkedList源码详解
java·开发语言
C666688820 分钟前
同步(Synchronous)和异步(Asynchronous)
开发语言·c#
MYX_30925 分钟前
第七节 文件与流
开发语言·c++·学习·算法
m0_7482489427 分钟前
在线影视播放网站PHP电影网站源码自动采集MKCMS升级版米酷模板含WAP手机版附三套模板
android·开发语言·php
阿里云云原生42 分钟前
云消息队列 ApsaraMQ Serverless 演进:高弹性低成本、更稳定更安全、智能化免运维
运维·安全·serverless
K-D小昊2 小时前
AWS云设施攻击
安全·flask·云计算·aws
~怎么回事啊~2 小时前
chrome-mojo C++ Bindings API
开发语言·c++
muxue1782 小时前
数据结构:栈
java·开发语言·数据结构
小涵3 小时前
Python和JavaScript在字符串比较上的差异
开发语言·javascript·python
运维小文4 小时前
python文本处理-基础篇
开发语言·python·正则表达式