std::map是基于红黑树实现的。红黑树对于很多朋友来说是一个比较难的数据结构,特别是逆向分析,更是让人头大。本文不打算讨论红黑树的相关技术细节,网上有很多资料,有兴趣的朋友可以自行搜索学习。本文主要讨论std::map逆向分析相关技术。
内存布局
根据前面文章介绍的经验,我们找到map成员变量定义的代码,如下:
//xtree 头文件
template<class _Traits>
class _Tree_comp_alloc
{ // base class for tree to hold ordering predicate, allocator
public:
typedef _Tree_comp_alloc<_Traits> _Myt;
typedef typename _Traits::allocator_type allocator_type;
typedef typename _Traits::key_compare key_compare;
typedef _Tree_base_types<typename _Traits::value_type,
allocator_type> _Alloc_types;
/*省略部分代码 */
private:
_Compressed_pair<key_compare,
_Compressed_pair<_Alty, _Tree_val<_Val_types> > > _Mypair;
};
template<class _Val_types>
class _Tree_val
: public _Container_base
{ // base class for tree to hold data
public:
typedef _Tree_val<_Val_types> _Myt;
typedef typename _Val_types::_Nodeptr _Nodeptr;
typedef _Nodeptr& _Nodepref;
/*省略部分代码*/
_Nodeptr _Myhead; // pointer to head node
size_type _Mysize; // number of elements
};
可以看出,map由两个变量构成:
_Myhead:指向红黑树head节点的指针
_Mysize: 红黑树节点数
在32位系统下,map在内存中占用8个字节。
接着我们看一下节点的结构:
template<class _Value_type,
class _Voidptr>
struct _Tree_node
{ // tree node
_Voidptr _Left; //+0 left subtree, or smallest element if head
_Voidptr _Parent;//+4 parent, or root of tree if head
_Voidptr _Right;//+8 right subtree, or largest element if head
char _Color;//+12 _Red or _Black, _Black if head _Red = 0 ,_Black = 1
char _Isnil;//+13 true only if head (also nil) node
_Value_type _Myval; //+16 为了内存对齐,在这个变量前面补齐了2个字节 the stored value, unused if head
private:
_Tree_node& operator=(const _Tree_node&);
};
_Tree_node中_Color变量是节点颜色,_Red = 0 ,_Black = 1。_Myval是std::pair<key,value>类型的变量,std::pair<key,value>由_first和_second两个变量,分别是key和value。在32位系统的内存中,_Tree_node占用的空间取决于map的key和value的类型。_Tree_node在内存中实际占用的的内存比各个成员变量的和还要多两个字节。这是因为系统为了内存对齐,在_Myval前补了两个字节,所以_Myval的偏移实际上是16个字节。
实现map的红黑树,是从_Myhead节点开始,它实际上是红黑树的哨兵节点。_Myhead具有以下特点:
- _Myhead的_Isnil = true
- _Myhead的_Parent指向红黑树的根节点
- _Myhead的_Left指向红黑树的最左边的节点,_Right指向最右边的节点(这是为了遍历时,方便找到begin())
下图体现了红黑树的结构特点,图中NIL节点就是_Myhead节点,除了root和_Myhead,其他节点的_Parent没有画出来,实际上它们的_Parent指针都指向它们各自的前驱节点。
逆向分析
测试代码如下:
// 编译环境:VS2015(v140) + X86 + Release
int main()
{
map<int, int> lst = { { 0,1 },{ 1,2 },{ 2,3 },{3,4},{4,4},{ 5,5 } };
for (auto it = lst.begin(); it != lst.end(); it++)
{
printf("%d", it->first);
}
return 0;
}
编译一下,用IDA打开,用强大F5反编译一下,如下:
先看一下,遍历红黑树的部分,这其实对树的中序遍历,由于_Myhead节点中存放了树的最左边节点,遍历直接从_Myhead->_Left开始。
我们进入map构造函数看一下:
上图中,函数sub_401830是插入节点的代码,代码相对比较复杂,本文就不分析了,其实也没有比要。我们的目的通常是在逆向中识别到map的代码,只要认识就可以了。如果想要了解红黑树插入节点的代码原理,何不直接看源码呢?
函数sub_4016D0的功能是申请空间创建节点,这是map的典型特征代码。在逆向分析中,见到这段代码基本可以确定是map了。
总结
对于map的逆向相对来说还是比较困难的,尤其是key和value是比较复杂的数据结构时,需要更多的耐心,特别是要记住map的内存布局,和节点的内存布局。不过话说回来,正是因为map代码相对复杂,其特征也比较明显,有很多特征可以帮助我们识别是否是map。为了方便学习,我已经将测试代码的idb文件上传。