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堆栈表
- 发现了一个特殊函数:_initterm(__xc_a, __xc_z);
- 再次下断至构造函数,翻看栈回溯会发现正好来自此函数
- 那么我可以使用反汇编窗口提取该特征码,用于下次定位
8B 4D FC 8B 11 89 55 F8 8B 4D F8 FF 15 ?? ?? ?? ?? FF 55 F8 EB CF
-
- 打开OD,Ctrl+B直接转跳
- 特征码能够识别代码了,那我们再升级一下进行手动查找
- 查看VS的void __cdecl _initterm();返回处,断点至返回
- 发现该特征处有两处push/push/call、push/push/call
- 手动查找
- 并不是所有的初始化函数都需要分析,有些函数是运行时库提供的,比如说第一个全局变量的初始化函数。一般来讲,全局变量的初始化会用到 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;
}
-
- 断点至析构函数处,查看上层函数
- 查看反汇编
- 提取特征
8B 45 C4 89 45 D0 8B 4D D0 FF 15 ?? ?? ?? ?? FF 55 D0
-
- 转到OD
-
END
3. 3. 字符串容器.exe
a.找main函数
-
- 我们先看代码
int main()
{
CString str1("hello world");
printf_s("%S", str1.GetBuffer());
}
-
- 断点至printf,查看反汇编
- 打开OD,定位main函数
- 分析代码1:断点至第一个call处(CA9CAF),将ebp-18放入16进制查看
- 单步F8走,发现初始化了一下4字节的空间保存this指针
- 执行第二个call(CA9CBC)
- 发现ebp-8的位置变为了一个有内容的内存指针,
- 跟踪这个指针,发现此处存储了宽字符字符串
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函数
- 调试器至此,发现依旧是类对象的指针
- 但是在指针之后,紧跟着的就是它的字符串
- 总共能存有4*4=16>16-1=15个字符串
- 在这16个字符串后,跟的是这个字符长
- 在strlen后面,存的就是15这个最大的长
d.断点至字符串长16处
-
- 查看反汇编,发现与上文少了一个mov
- 打开OD,查看该处指针(ebp-E4)
- 发现在this指针之后,紧跟的是一个内存地址
- 查看该处内存,发现这段堆内存才是真实字符串
- 分析可得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
- 跟踪第二个CALL,发现this指针被初始化了
- 查看指针后的0x01124A18,发现此处内容一致
- 则可判断为此处为根节点,且头指向尾(循环)
b.跟踪push_back(4);的CALL
-
- 发现this后2个指针处,变成了1,则猜测这是数量
- 跟踪根节点0x01124A18,发现数据更新了
- 继续跟踪该指针,发现此处的位置存储了push_back(4)
c.再次跟踪push_back(3);
-
- 发现根节点的指向也变化了
- 且该地址保存了push_back(3)
d.跟踪push_back(1);
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;
}
-
- 分析反汇编
- 得出了begin的地址,那么我们就可以开始循环了
- 循环条件就是begin!=end(头节点值的指针)
- 分析循环体
g.迭代器的运用
-
- 查看源码
auto begin = l.begin();
auto second = ++begin;
auto end = l.end();
h.分析begin迭代器
-
- 此处指针与上文对象一致,第一个CALL为初始化空间
- 第二个CALL就是初始化了begin迭代器对象,跟踪此处
- 发现第一个还是this指针,第二个是空
- 第三个是迭代器所指对象的地址0x010FFBD3
- 不难发现,此处的地址就存了list的头节点
i.分析迭代器重载的实现原理
j.分析end迭代器
-
- 因为此段汇编较多,我们先确定main函数结尾
- 上文我们分析了根节点,由于我这次重新运行了
- 所以查看end迭代器的第三个地址,的确是根节点的
-
END
5. 4. vector 容器
-
找main函数
-
- 查看源码
vector<int> vec;
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);
-
- 查看反汇编,发现与上文list相似,同样2个call
- 打开OD,跟踪第二个call
- 跟踪push_back(1);发现三个地址被初始化了
- 跟踪push_back(2);发现三个地址被初始化了
- 并且三个地址的地址与上一次的地址改变了
- 跟踪其中第一个0x01354658、第二个地址0x01354660.
- 对比发现,第二个地址是第一个地址+8(0x60-0x58)
- 也就是说int值占了4字节,2个就是8字节
- 意味着第二个地址是vector末尾元素+sizeof(int)
- 跟踪push_back(3);发现三个地址被初始化了
- 相同情况,发现后三个地址也被改变了
- 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句话一致,我们直接查看反汇编
b.分析begin迭代器
-
- 与上文list迭代器一致,共12字节
- 跟踪第三个指针0x011F4288,发现是容器第一个值
c.分析迭代器重载++
-
- 查看内存地址,这时候迭代器的第二个、第三个地址都有了
- 我们先跟踪第三个0x011F428C,发现该地址是 值2的地址
- 说明迭代器第三个地址确实是迭代器值的指针
- 详细点说明就是(*迭代器)迭代器的解引用
- 就是该元素在vector容器中(元素),如本例为int,2.
- 跟踪第二个地址0x01F0FB84,发现该处疑似是一个迭代器
- 我们再看一下汇编代码,跟踪重载时传入的ebp-38
- 我们就能非常直观的发现,两处地址都是0x01F0FB84,
- 由此可见,迭代器的第二个地址,保存的是关联前的迭代器地址
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处
b.分析vec.erase(vec.begin());函数
-
- 由于此处的begin为临时变量,所以不需要push和memset
- 并且erase移除时,需要将迭代器前移
- 临时变量删除时,需要实时得调用析构函数
c.解析vector第四个地址
-
- 我们分析完了erase()函数,断点至此
- 我们就可以来看一下erase前后的区别
- 在删除元素时,vector第三、四个地址时相同的
- 删除begin后,第三个地址被改变了
- 改写一下源码,删除end
auto begin = --vec.end();
vec.erase(begin);
-
- 删除前,vector地址第三个=第四个=0x00CF42EC尾部+1的地址
- 单步走,删除尾部数据,发现第三个地址前移了4字节
- 由此可发现,第四个地址存储着此容器最大的地址数
- 用于检测动态数组需不需要扩容,如果容量不够则扩
- 得到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的逻辑分析
-
那么什么是逻辑呢?
-
- 我们回想一下,MFC是基于消息的框架
- 那么我们作用在消息上就可以了
- 我们先来看一个拦截消息例子
- Windows 常用消息大全 - 李华丽 - 博客园
a.消息的定位
-
- 这是一个左键弹起的消息,对应 LBUTTONUP=0,利用OD拦截它
- 我们断点得知,此处地址为系统模块
- 那么我们的逻辑应该处于主模块中
- 查看E板块,主模块基址0x230000
- 加上模块大小后0x100C000,在调试中创建条件断点
- 点击跟踪步入,发现我们已经进入MFC的处理函数了
b.特征码定位
-
我们既然知道MFC框架映射消息宏的机制
-
那么该机制肯定有特点,断点至OnBnClickedButton1()
-
猜测特征码
-
- 查看类向导,发现这个函数属于BN_CLICKED
- 得知此处消息映射宏的特征码为
- call dword ptr [ebp-48h]
- 用OD重新打开MFC,执行到OEP,搜索
- 得到所有特征,运行程序,界面加载后右键全部设断
- 此时程序会暂停,说明此处暂停不属于我们要找的,F2去除
- 手动点击按钮,发现特征码
- 回车+回车进入函数,得到该函数体
-
END
10. MFCPj001.exe
a.分析程序:试错
-
发现该程序共有3个可按按钮,以及1个禁止按钮
-
尝试上文ebp-48特征码
-
- 发现并没有该特征,说明此按钮并非映射BN_Click消息
b.尝试ebp-08特征码
-
- 在MFC中有一个函数OnInitDialog() 函数,用于消息处理
- 查看VS特征码得到,call dword ptr [ebp-8]:
- 在OD中执行程序,初始化窗口后搜索并下断
- 点击按钮1,程序暂停
- 按特征猜测函数,在上文的CALL处按两下回车,进入函数体
- 相同操作猜测按钮2、3
- 观察3个函数,发现唯一区别是一个传参不同,猜测该函数为关键函数
-
分析关键函数
-
- 发现有三个关键判断,判断值为EAX+C0/C4/C8≠C/8/5,
- 查看上文,发现三个按钮正好是C0,C4,C8,对应12/8/5
c.分析关键跳
-
- 发现三个不等于则跳都指向函数尾部退出
- 那么在退出前干了什么呢?
- 进入010E7E31的CALL
-
-
- 发现该函数用于获取窗口句柄
-
-
- 进入010F0716的CALL
-
-
- 发现此函数用于检测窗口后将其窗口禁止设置为FALSE
-
-
- 宏观来看这个CALL
-
-
- 发现实际上是3个PUSH+CALL,右往左传参
- 第三个PUSH为第二个为后者CALL的参数1
- 那说明这个参数1来自前者CALL的返回值
-
-
END
课后作业
1. MFCPj000.exe
-
分析程序:试错
-
- 发现该程序共有1个禁止的按钮
- 那猜测这个按钮是按了某个按键就会显示
- 那我们脸滚键盘!!!
- 这么巧的吗,按了回车和空格,
- 居然两个按键都能亮!?
-
END
2. MFCPj002.exe
a.分析程序:试错
-
发现该程序供有9个可按按钮和一个禁止按钮
-
- 那么按照【52pojie】分析可能两种情况,
- 第一种就是上文001中的9个按钮函数
- 第二种就是【此贴】中分析可得9个按钮共用同一个函数
-
找特征码
-
- 按上文精*,偶不,是经验,断点第六个
- 标识函数体
- 分析按钮1、2、3得到关键函数
- 分析按钮9,得到9个按钮标识符
- C0/C4/C8/
- CC/D0/D4/
- D8/DC/E0
b.分析关键函数
-
- 发现对应按钮标识的关键跳,以及1/0
- PS:JE是==1,JNZ是≠1
c.试错出答案
- END
3. MFCPj003.exe
a.分析程序:试错
-
界面与上文一样,则说明框架一致
-
- 按上文得到按钮1、2、9函数体
- 以及一致的C0~E0的标识符,标识符后是2处跳
b.分析按钮函数体
-
通览一遍函数,发现在函数尾部有两个关键CALL
-
- 查看特征,发现在函数入口有一次调用,
- 那我们猜测这个就是检测是否成功解密的函数
- 那么我可以猜测这两个函数的作用
c.分析关键函数
-
- 得到按钮对应的值
- 点击之后,发现并没有结果,为什么会没有结果呢?
d.再次分析函数体
-
- 观察按钮函数体尾部,发现这个关键函数不是每次都调用的
- 向前逆向,发现判断条件来自一个SUB,断点至SUB处
- 发现此处ECX为按钮点击次数,我们重复点击
-
END
4. 开动脑筋.exe
- END