咖喱忍者

导航

x32dbg之数据结构在反汇编中熟悉

课堂笔记

1. 1. 全局对象的构造.exe

  • 找main函数

    • 我们先基础的查看一下源码
#include <stdio.h>
// 静态编译比较方便看源码
class CObjA
{
public:
    CObjA()
    { 
        printf("CObjA::CObjA();\n"); 
    }
};

class CObjB
{
public:
    CObjB()
    {
        printf("CObjB::CObjB();\n");
    }
};

// 一个全局变量,全局变量的初始化在 main 之前
CObjA g_object1;
CObjB g_object2;

// 8B 4D FC 8B 11 89 55 F8 8B 4D F8 FF 15 ?? ?? ?? ?? FF 55 F8 EB CF

int main()
{

    return 0;
}
    • 我们来思考一下,全局变量在mian函数之前初始化
    • 那么就是说类的构造函数在main函数之前调用
    • 那我们怎么找呢?我们先引入一个基础概念,栈回溯
    • 下断点在main函数处,然后查看VS堆栈表
    • image.png
    • image.png
    • image.png
    • 发现了一个特殊函数:_initterm(__xc_a, __xc_z);
    • 再次下断至构造函数,翻看栈回溯会发现正好来自此函数
    • image.png
    • 那么我可以使用反汇编窗口提取该特征码,用于下次定位
8B 4D FC 8B 11 89 55 F8 8B 4D F8 FF 15 ?? ?? ?? ?? FF 55 F8 EB CF
    • image.png
    • 打开OD,Ctrl+B直接转跳
    • image.png
    • 特征码能够识别代码了,那我们再升级一下进行手动查找
    • 查看VS的void __cdecl _initterm();返回处,断点至返回
    • 发现该特征处有两处push/push/call、push/push/call
    • image.png
    • 手动查找
    • image.png
    • image.png
    • 并不是所有的初始化函数都需要分析,有些函数是运行时库提供的,比如说第一个全局变量的初始化函数。一般来讲,全局变量的初始化会用到 ecx 调用构造函数
    • 全局变量的 this 指针通常使用 mov 指令进行赋值,局部变量一般使用 lea 指令
  • END

2. 2. 全局对象的析构.exe

  • 找main函数

    • 查看源代码
#include <stdio.h>
class CObjA
{
public:
    ~CObjA()
    {
        printf("CObjA::~CObjA();\n");
    }
};

class CObjB
{
public:
    ~CObjB()
    {
        printf("CObjB::~CObjB();\n");
    }
};

// 一个全局变量,全局变量的释放在 main 之后
CObjA g_object1;
CObjB g_object2;

int number = 10;

// 89 45 c4 8b 4d ec 8b 55 d4 89 11 8b 45 c4 89 45 d0 8b 4d d0 ff 15 ?? ?? ?? ?? ff 55 d0
int main()
{
    return 0;
}
    • 断点至析构函数处,查看上层函数
    • image.png
    • 查看反汇编
    • image.png
    • 提取特征
8B 45 C4 89 45 D0 8B 4D D0 FF 15 ?? ?? ?? ?? FF 55 D0
    • 转到OD
    • image.png
  • END

3. 3. 字符串容器.exe

a.找main函数

    • 我们先看代码
int main()
{
    CString str1("hello world");
    printf_s("%S", str1.GetBuffer());
}
    • 断点至printf,查看反汇编
    • image.png
    • 打开OD,定位main函数
    • image.png
    • 分析代码1:断点至第一个call处(CA9CAF),将ebp-18放入16进制查看
    • image.png
    • 单步F8走,发现初始化了一下4字节的空间保存this指针
    • image.png
    • 执行第二个call(CA9CBC)
    • image.png
    • 发现ebp-8的位置变为了一个有内容的内存指针,
    • 跟踪这个指针,发现此处存储了宽字符字符串
    • image.png

b.注释CString,查看String

    • 查看源码
int main()
{
    //CString str1("hello world");
    //printf("%S", str1.GetBuffer());
    
    string str1 = "1";                      // len = 1
    string str2 = "1234";
    string str3 = "1234567";
    string str4 = "1234567890";
    string str5 = "123456789ABCD";
    string str6 = "123456789ABCDEFG";       // len = 16

    my_string* pstr = (my_string*)&str5;
    printf("self = %p\n", pstr->self);
    printf("length = %d\n", pstr->length);
    printf("size = %d\n", pstr->size);
    if (pstr->length > 0xF)
        printf("str = %s\n", pstr->ptr);
    else
        printf("str = %s\n", pstr->str);

    return 0;
}

c.断点至String

    • 发现压入了字符后使用thiscall了一个basic函数
    • image.png
    • 调试器至此,发现依旧是类对象的指针
    • 但是在指针之后,紧跟着的就是它的字符串
    • 总共能存有4*4=16>16-1=15个字符串
    • 在这16个字符串后,跟的是这个字符长
    • 在strlen后面,存的就是15这个最大的长
    • image.png

d.断点至字符串长16处

    • 查看反汇编,发现与上文少了一个mov
    • image.png
    • 打开OD,查看该处指针(ebp-E4)
    • 发现在this指针之后,紧跟的是一个内存地址
    • image.png
    • 查看该处内存,发现这段堆内存才是真实字符串
    • image.png
    • 分析可得string类型的数据结构
struct my_string
{
    struct my_string* self;     // 指向自己的指针
    union
    {
        char* ptr;              // 当数据大于15字节的时候
        char str[0x10];         // 当数据小于16字节的时候
    };
    int length;                 // 当前占用的长度
    int size;                   // 指向的堆空间的大小(最多能存储多少)
};
  • END

4. 5. list 容器.exe

a.找main函数

    • 在C++的list中,是由循环链表构成的
int main()
{
    list<int> l;

    l.push_back(4);
    l.push_back(3);
    l.push_back(2);
    l.push_back(1);

    my_list<int>* m = (my_list<int>*)&l;
    // 获取第一个元素
    my_node<int>* node = m->head->next;
}
    • 查看反汇编,发现与上文有些许不同
    • 第一个CALL都是将地址清0
    • image.png
    • image.png
    • 跟踪第二个CALL,发现this指针被初始化了
    • image.png
    • 查看指针后的0x01124A18,发现此处内容一致
    • 则可判断为此处为根节点,且头指向尾(循环)
    • image.png

b.跟踪push_back(4);的CALL

    • 发现this后2个指针处,变成了1,则猜测这是数量
    • image.png
    • 跟踪根节点0x01124A18,发现数据更新了
    • image.png
    • 继续跟踪该指针,发现此处的位置存储了push_back(4)
    • image.png

c.再次跟踪push_back(3);

    • 发现根节点的指向也变化了
    • image.png
    • 且该地址保存了push_back(3)
    • image.png

d.跟踪push_back(1);

    • image.png
    • image.png

e.得出list的数据结构

// 节点结构体
template <class T>
struct my_node
{
    struct my_node* next;       // 指向下一个
    struct my_node* prev;       // 指向前一个
    T element;                  // 数据域
};

// 容器结构体
template <class T>
struct my_list
{
    struct my_list* self;       // 指向自己的指针
    struct my_node<T>* head;    // 头节点,不存储数据
    int length;                 // 元素个数
};

f.有了结构体,那我们来手写一下循环

    • 源代码
my_list<int>* m = (my_list<int>*)&l;
// 获取第一个元素
my_node<int>* node = m->head->next;
    
while (node != m->head)
{
    printf("%d", node->element);
    node = node->next;
}
    • 分析反汇编
    • image.png
    • 得出了begin的地址,那么我们就可以开始循环了
    • 循环条件就是begin!=end(头节点值的指针)
    • image.png
    • 分析循环体
    • image.png

g.迭代器的运用

    • 查看源码
auto begin = l.begin();
auto second = ++begin;
auto end = l.end();
    • image.png

h.分析begin迭代器

    • 此处指针与上文对象一致,第一个CALL为初始化空间
    • 第二个CALL就是初始化了begin迭代器对象,跟踪此处
    • image.png
    • 发现第一个还是this指针,第二个是空
    • 第三个是迭代器所指对象的地址0x010FFBD3
    • 不难发现,此处的地址就存了list的头节点
    • image.png

i.分析迭代器重载的实现原理

    • image.png

j.分析end迭代器

    • 因为此段汇编较多,我们先确定main函数结尾
    • image.png
    • 上文我们分析了根节点,由于我这次重新运行了
    • 所以查看end迭代器的第三个地址,的确是根节点的
    • image.png
  • END

5. 4. vector 容器

  • 找main函数

    • 查看源码
vector<int> vec;

vec.push_back(1);
vec.push_back(2);
vec.push_back(3);
    • 查看反汇编,发现与上文list相似,同样2个call
    • image.png
    • 打开OD,跟踪第二个call
    • image.png
    • 跟踪push_back(1);发现三个地址被初始化了
    • image.png
    • 跟踪push_back(2);发现三个地址被初始化了
    • 并且三个地址的地址与上一次的地址改变了
    • image.png
    • 跟踪其中第一个0x01354658、第二个地址0x01354660.
    • 对比发现,第二个地址是第一个地址+8(0x60-0x58)
    • 也就是说int值占了4字节,2个就是8字节
    • 意味着第二个地址是vector末尾元素+sizeof(int)
    • image.png
    • image.png
    • 跟踪push_back(3);发现三个地址被初始化了
    • image.png
    • 相同情况,发现后三个地址也被改变了
    • 0x013542A4-0x01354298=0xC->12
    • 分析可得,第三个地址就是元素末尾后一个
    • 得到数据结构
// vector 容器对应的结构体
template <class T>
struct my_vector
{
    struct my_vector* self;     // 指向自己
    T* begin;                   // 指向堆空间起始位置
    T* data_end;                // 指向数据的结尾部分 end迭代器指向的位置
    LPVOID 目前未知;            // 由于目前没有用到,暂且未知,继续分析
};
  • END

6. 7. 迭代器的分析.exe

a.找main函数

    • 查看源码
int main()
{
    vector<int> vec;

    vec.push_back(1);
    vec.push_back(2);
    vec.push_back(3);

    auto begin = vec.begin();
    // 初始化迭代器 + 将迭代器作为参数传递给 begin 建立关联

    auto second = ++begin;
    // 初始化迭代器 + 将 ++begin 的返回值拷贝构造到迭代器

    auto end = vec.end();
    // 同 begin

    return 0;
}
    • 由于前4句话一致,我们直接查看反汇编
    • image.png

b.分析begin迭代器

    • 与上文list迭代器一致,共12字节
    • image.png
    • 跟踪第三个指针0x011F4288,发现是容器第一个值
    • image.png

c.分析迭代器重载++

    • image.png
    • 查看内存地址,这时候迭代器的第二个、第三个地址都有了
    • image.png
    • 我们先跟踪第三个0x011F428C,发现该地址是 值2的地址
    • 说明迭代器第三个地址确实是迭代器值的指针
    • 详细点说明就是(*迭代器)迭代器的解引用
    • 就是该元素在vector容器中(元素),如本例为int,2.
    • image.png
    • 跟踪第二个地址0x01F0FB84,发现该处疑似是一个迭代器
    • image.png
    • 我们再看一下汇编代码,跟踪重载时传入的ebp-38
    • 我们就能非常直观的发现,两处地址都是0x01F0FB84,
    • 由此可见,迭代器的第二个地址,保存的是关联前的迭代器地址
    • image.png

d.得出vector迭代器的数据结构

struct my_iterator;

struct s
{
    void* target;       // 指向的类型是不固定
    struct my_iterator* self;
};

struct my_iterator
{
    struct s* point;            // 自己和关联的容器
    struct my_iterator* prev;   // 迭代器构成的表
    void* node;                 // 具体的的节点
};
  • END

7. 4. vector 容器 - 迭代器部分.exe

a.找main函数

    • 查看源码
int main()
{
    vector<int> vec;

    vec.push_back(1);
    vec.push_back(2);
    vec.push_back(3);

    auto begin = vec.begin();
    vec.erase(vec.begin());

    // 自己遍历迭代器   data_end - begin 数据元素个数
    auto myvec = (my_vector<int>*)&vec;
    int* mybegin = myvec->begin;
    while (mybegin != myvec->data_end)
    {
        printf("%d ", *mybegin);
        mybegin++;
    }
    /*
        // 前两句的汇编代码是生成了一个 1 的引用
        // 通过 push_back 函数的原型可以知道参数就是引用
        // 使用引用传参可以提高传递的效率并节省空间

        // 将 1 保存在了一个临时变量中
        mov         dword ptr [ebp-0F0h],1  
        // 获取这个临时变量的地址
        lea         eax,[ebp-0F0h]
        // 将地址作为参数传递给函数
        push        eax
        // 设置 this 指针的值
        lea         ecx,[vec]
        // 调用 push_back 函数
        call        std::bad_array_new_length::`vector deleting destructor' (0681717h)  
    */

    return 0;
}
    • 反汇编跟踪至begin处
    • image.png

b.分析vec.erase(vec.begin());函数

    • 由于此处的begin为临时变量,所以不需要push和memset
    • 并且erase移除时,需要将迭代器前移
    • image.png
    • 临时变量删除时,需要实时得调用析构函数
    • image.png

c.解析vector第四个地址

    • 我们分析完了erase()函数,断点至此
    • 我们就可以来看一下erase前后的区别
    • 在删除元素时,vector第三、四个地址时相同的
    • image.png
    • 删除begin后,第三个地址被改变了
    • image.png
    • 改写一下源码,删除end
auto begin = --vec.end();
vec.erase(begin);
    • 删除前,vector地址第三个=第四个=0x00CF42EC尾部+1的地址
    • image.png
    • image.png
    • 单步走,删除尾部数据,发现第三个地址前移了4字节
    • 由此可发现,第四个地址存储着此容器最大的地址数
    • 用于检测动态数组需不需要扩容,如果容量不够则扩
    • image.png
    • 得到vector数据结构
// vector 容器对应的结构体
template <class T>
struct my_vector
{
    struct my_vector* self;     // 指向自己
    T* begin;                   // 指向堆空间起始位置
    T* data_end;                // 指向数据的结尾部分 end迭代器指向的位置
    T* heap_end;                // 指向堆空间的结尾
};

d.有了数据结构,我们手动循环一下

  • 从vec获取到this指针
  • this指针的第二个就是begin
  • 第三个就是end
  • 循环即可
// 自己遍历迭代器   data_end - begin 数据元素个数
auto myvec = (my_vector<int>*)&vec;
int* mybegin = myvec->begin;
while (mybegin != myvec->data_end)
{
    printf("%d ", *mybegin);
    mybegin++;
}

struct my_iterator
{
    struct s* point;            // 自己和关联的容器
    struct my_iterator* prev;   // 迭代器构成的表
    void* node;                 // 具体的的节点
};

struct s
{
    void* target;       // 指向的类型是不固定
    struct my_iterator* self;
};
  • END

8. 6. map容器.exe

  • 挖坑,待填

9. 8. MFC REV.exe

  • 由于MFC属于框架,

  • 我们在初始阶段并不要分析框架代码

  • 着重注意MFC的逻辑分析

  • 那么什么是逻辑呢?

a.消息的定位

    • 这是一个左键弹起的消息,对应 LBUTTONUP=0,利用OD拦截它
    • image.png
    • image.png
    • 我们断点得知,此处地址为系统模块
    • 那么我们的逻辑应该处于主模块中
    • 查看E板块,主模块基址0x230000
    • 加上模块大小后0x100C000,在调试中创建条件断点
    • image.png
    • image.png
    • 点击跟踪步入,发现我们已经进入MFC的处理函数了
    • image.png

b.特征码定位

  • 我们既然知道MFC框架映射消息宏的机制

  • 那么该机制肯定有特点,断点至OnBnClickedButton1()

  • image.png

  • 猜测特征码

    • image.png
    • 查看类向导,发现这个函数属于BN_CLICKED
    • image.png
    • 得知此处消息映射宏的特征码为
    • call dword ptr [ebp-48h]
    • 用OD重新打开MFC,执行到OEP,搜索
    • image.png
    • image.png
    • 得到所有特征,运行程序,界面加载后右键全部设断
    • image.png
    • 此时程序会暂停,说明此处暂停不属于我们要找的,F2去除
    • image.png
    • 手动点击按钮,发现特征码
    • image.png
    • 回车+回车进入函数,得到该函数体
    • image.png
  • END

10. MFCPj001.exe

a.分析程序:试错

  • image.png

  • 发现该程序共有3个可按按钮,以及1个禁止按钮

  • 尝试上文ebp-48特征码

    • image.png
    • 发现并没有该特征,说明此按钮并非映射BN_Click消息

b.尝试ebp-08特征码

    • 在MFC中有一个函数OnInitDialog() 函数,用于消息处理
    • image.png
    • 查看VS特征码得到,call dword ptr [ebp-8]:
    • image.png
    • 在OD中执行程序,初始化窗口后搜索并下断
    • image.png
    • 点击按钮1,程序暂停
    • image.png
    • 按特征猜测函数,在上文的CALL处按两下回车,进入函数体
    • image.png
    • 相同操作猜测按钮2、3
    • image.png
    • image.png
    • 观察3个函数,发现唯一区别是一个传参不同,猜测该函数为关键函数
    • image.png
  • 分析关键函数

    • 发现有三个关键判断,判断值为EAX+C0/C4/C8≠C/8/5,
    • 查看上文,发现三个按钮正好是C0,C4,C8,对应12/8/5
    • image.png

c.分析关键跳

    • 发现三个不等于则跳都指向函数尾部退出
    • image.png
    • 那么在退出前干了什么呢?
    • 进入010E7E31的CALL
      • 发现该函数用于获取窗口句柄
      • image.png
    • 进入010F0716的CALL
      • 发现此函数用于检测窗口后将其窗口禁止设置为FALSE
      • image.png
    • 宏观来看这个CALL
      • 发现实际上是3个PUSH+CALL,右往左传参
      • 第三个PUSH为第二个为后者CALL的参数1
      • 那说明这个参数1来自前者CALL的返回值
      • image.png
  • END

课后作业

1. MFCPj000.exe

  • 分析程序:试错

    • image.png
    • 发现该程序共有1个禁止的按钮
    • 那猜测这个按钮是按了某个按键就会显示
    • 那我们脸滚键盘!!!
    • 这么巧的吗,按了回车和空格,
    • 居然两个按键都能亮!?
  • END

2. MFCPj002.exe

a.分析程序:试错

  • image.png

  • 发现该程序供有9个可按按钮和一个禁止按钮

    • 那么按照【52pojie】分析可能两种情况,
    • 第一种就是上文001中的9个按钮函数
    • 第二种就是【此贴】中分析可得9个按钮共用同一个函数

发一篇关于MFC查找按钮事件(映射消息)的文章,初级 - 『脱壳破解区』 - 吾爱破解

https://www.52pojie.cn/thread-151725-1-1.html

  • 找特征码

    • 按上文精*,偶不,是经验,断点第六个
    • image.png
    • 标识函数体
    • image.png
    • 分析按钮1、2、3得到关键函数
    • image.png
    • 分析按钮9,得到9个按钮标识符
    • C0/C4/C8/
    • CC/D0/D4/
    • D8/DC/E0
    • image.png

b.分析关键函数

    • 发现对应按钮标识的关键跳,以及1/0
    • PS:JE是==1,JNZ是≠1
    • image.png

c.试错出答案

image.png

  • END

3. MFCPj003.exe

a.分析程序:试错

  • image.png

  • 界面与上文一样,则说明框架一致

    • 按上文得到按钮1、2、9函数体
    • 以及一致的C0~E0的标识符,标识符后是2处跳
    • image.png

b.分析按钮函数体

  • 通览一遍函数,发现在函数尾部有两个关键CALL

  • image.png

    • 查看特征,发现在函数入口有一次调用,
    • 那我们猜测这个就是检测是否成功解密的函数
    • image.png
    • 那么我可以猜测这两个函数的作用
    • image.png

c.分析关键函数

    • 得到按钮对应的值
    • image.png
    • 点击之后,发现并没有结果,为什么会没有结果呢?
    • image.png

d.再次分析函数体

    • 观察按钮函数体尾部,发现这个关键函数不是每次都调用的
    • image.png
    • 向前逆向,发现判断条件来自一个SUB,断点至SUB处
    • image.png
    • 发现此处ECX为按钮点击次数,我们重复点击image.png
  • END

4. 开动脑筋.exe

  • END

课外作业

老师笔记

posted on 2020-11-30 23:37  咖喱忍者  阅读(1077)  评论(0编辑  收藏  举报