在之前的CTF中常常出现了对STL容器的逆向,而在网上的资料中并没有搜到详细的STL容器逆向的经验和文章,这里配合编译器一起来看看常见的STL容器的内存模型,最后根据最近N1CTF比赛中cppmaster来进行实战。
STL存在非常多种类的容器,但是多个容器之间的底层实现其实是类似的,例如set和map都采用的是rb_tree这个结构,因此我们从这种底层实现的数据结构来对STL容器分类,这里将要分析的容器分为分析四类:rb_tree,deque,hashtable,vector,顺带分析一下string的结构。
当然可能还存在许许多多的STL容器,但是大体的分析思路是类似的。这里采用LLVM辅助我们分析容器的内存结构。
对于一个Class而言,在LLVM IR种对应的为StructType,可以使用API访问其下的子Type,因此可以随便简单写个pass来dump出ir种的类的内存模型,从而辅助我们逆向分析各个容器的内存表示和实现。具体代码如下。
| 
      1
      
      2
      
      3
      
      4
      
      5
      
      6
      
      7
      
      8
      
      9
      
      10
      
      11
      
      12
      
      13
      
      14
      
      15
      
      16
      
      17
      
      18
      
      19
      
      20
      
      21
      
      22
      
      23
      
      24
      
      25
      
      26
      
      27
      
      28
      
      29
      
      30
      
      31
      
      32
      
      33
      
      34
      
      35
      
      36
      
      37
      
      38
      
      39
      
      40
      
      41
      
      42
      
      43
      
      44
      
      45
      
      46
      
      47
      
      48
      
      49
      
      50
      
      51
      
      52
      
      53
      
      54
      
      55
      
      56
      
      57
      
      58
      
      59
      
      60
      
      61
      
      62
      
      63
      
      64
      
      65
      
      66
      
      67
      
      68
      
      69
      
      70
      
      71
      
      72
      
      73
      
      74
      
      75
      
      76
      
      77
      
      78
      
      79
      
      80
      
      81
      
      82
      
      83
      
      84
      
      85
      
      86
      
      87
      
      88
      
      89
      
      90
      
      91
      
      92
      
      93
      
      94
      
      95
      
      96
      
      97
      
      98
      
      99
      
      100
      
      101
      
      102
      
      103
      
      104
      
      105
      
      106
      
      107
      
      108
      
      109
      
      110
      
      111
      
      112
      
      113
      
      114
      
      115
      
      116
      
      117
      
      118
      
      119
      
      120
      
      121
      
      122
      | #include "llvm/Pass.h"#include "llvm/IR/Function.h"#include "llvm/IR/CFG.h"#include "llvm/IR/Type.h"#include "llvm/ADT/SmallSet.h"#include "llvm/ADT/SmallVector.h"#include "llvm/IR/BasicBlock.h"#include "llvm/Support/raw_ostream.h"#include "llvm/IR/LegacyPassManager.h"#include "llvm/Transforms/Utils/Cloning.h"#include "llvm/Transforms/IPO/PassManagerBuilder.h"#include<algorithm>#include<string>using namespace llvm;namespace{    struct DumpClass : public FunctionPass    {        static char ID;           DumpClass() : FunctionPass(ID) {}        std::string getTypeName(Type*type, DataLayout *data);        void dumpType(intdepth, Type*type, intindex, DataLayout *data);           boolrunOnFunction(Function &F) override        {            if(F.getName().compare("main"))                returnfalse;            std::set<StructType*> types;            const DataLayout *data =&F.getParent()->getDataLayout();            for(Function::iterator B =F.begin(); B !=F.end(); B++)            {                 for(BasicBlock::iterator I =B->getFirstInsertionPt(); I !=B->end(); I++)                {                    Instruction *instr =&*I;                    if(isa<AllocaInst>(*instr))                    {                        AllocaInst *a =(AllocaInst*)instr;                        Type*type=a->getAllocatedType();                        if(type->isStructTy())                        {                            StructType *s =(StructType*)type;                            if(s->isOpaque())                                continue;                            types.insert(s);                        }                    }                }            }            for(StructType *type:types)            {                errs()<<"\n\nstart dumping type: "+type->getStructName() +"\n";                dumpType(0, type, 0, (DataLayout*)data);            }              returnfalse;        }      };}std::string DumpClass::getTypeName(Type*type, DataLayout *data){    if(type->isIntegerTy())    {        IntegerType *i =(IntegerType*)type;        return"uint"+std::to_string(i->getBitWidth()) +"_t";    }    elseif(type->isPointerTy())    {        PointerType *ptrType =(PointerType*)type;        returngetTypeName(ptrType->getPointerElementType(), data) +"*";    }    elseif(type->isArrayTy())    {        ArrayType *arrType =(ArrayType*)type;        returngetTypeName(arrType->getArrayElementType(), data) +"["+std::to_string(arrType->getArrayNumElements()) +"]";    }    elseif(type->isFloatTy())    {        return"float";    }    elseif(type->isStructTy())    {        StructType *s =(StructType*) type;        returns->getStructName();    }    else    {        return"unknown_"+std::to_string(data->getTypeAllocSizeInBits(type));    }}void DumpClass::dumpType(intdepth, Type*type, intindex, DataLayout *data){    std::string blank ="";    for(inti =0; i < depth; i++)        blank +="\t";    if(type->isStructTy())    {        StructType *s =(StructType*)type;        errs() << blank +s->getStructName() +" {\n";        for(inti =0; i < s->getStructNumElements(); i++)        {            Type*subType =s->getStructElementType(i);            dumpType(depth +1, subType, i, data);        }        //        errs() << blank +"} field_"+std::to_string(index) +";\n";    }    else    {        errs() << blank +getTypeName(type, data) +" field_"+std::to_string(index) +";\n";    }}char DumpClass::ID=0;static RegisterPass<DumpClass> X("dump", "DumpClass");//Register forclangstatic RegisterStandardPasses Y(PassManagerBuilder::EP_EarlyAsPossible,  [](const PassManagerBuilder &Builder, legacy::PassManagerBase &PM) {    PM.add(new DumpClass());  }); | 
deque是queue,stack,deque等线性容器的底层实现,通过dump工具可以dump出具体的内存结构如下。不难发现在deque中,主要分为三个部分,第一个用于描述map的,其中包含map大小和map的指针,第二个则是指向deque中起始元素的Deque_iterator,第三个则是指向了deque中结束元素的Deque_iterator。
| 
      1
      
      2
      
      3
      
      4
      
      5
      
      6
      
      7
      
      8
      
      9
      
      10
      
      11
      
      12
      
      13
      
      14
      
      15
      
      16
      
      17
      
      18
      
      19
      
      20
      
      21
      
      22
      | class.std::deque<string> {    class.std::_Deque_base {        struct.std::_Deque_base<std::__cxx11::basic_string<char>, std::allocator<std::__cxx11::basic_string<char> > >::_Deque_impl {            struct.std::_Deque_base<std::__cxx11::basic_string<char>, std::allocator<std::__cxx11::basic_string<char> > >::_Deque_impl_data {                class.std::__cxx11::basic_string**map;                uint64_t map_size;                struct.std::_Deque_iterator {                    class.std::__cxx11::basic_string*cur;                    class.std::__cxx11::basic_string*first;                    class.std::__cxx11::basic_string*last;                    class.std::__cxx11::basic_string**map;                } start;                struct.std::_Deque_iterator {                    class.std::__cxx11::basic_string*cur;                    class.std::__cxx11::basic_string*first;                    class.std::__cxx11::basic_string*last;                    class.std::__cxx11::basic_string**map;                } finish;            } field_0;        } field_0;    } field_0;} field_0; | 
这里需要注意的是:deque对元素的保存采取了分块保存的机制。deque采用一块连续的内存保存了一系列的指针。其中map即指向这一块连续的内存,换句话说map是一个指针,指向一个指针数组。而这个指针数组中的指针指向的则是一篇连续的,用于实际保存数据的内存区域,我们称为data array(每个元素的大小取决于deque中的模板类型),具体的内存示意图可以如下图所示。
而iterator的结构需要关注的是cur,他将直接指向数据数组中具体的元素,还有就是iterator下的map指针,这个元素则代表当前iterator指向的元素所在区块对应map中地址,last和first则指向data array的起始和结束。因此可以得知start和finish的map并不一定是相同的(即start迭代器指向元素不一定和finish迭代器指向元素处在同一个data array中),所以iterator在进行迭代的时候是需要根据map跳跃到不同的data array中。
vector作为常见又方便的stl容器,其实现并不复杂,比上述的deque要简单得多。vector仅仅由三个指针构成:start,finish,end_of_storage。start用于指向数组的第一个元素,而finish指向结束的位置,即最后一个元素后面。而end_of_storage指向当前为vector分配的堆空间的结束地址。
| 
      1
      
      2
      
      3
      
      4
      
      5
      
      6
      
      7
      
      8
      
      9
      
      10
      
      11
      | class.std::vector<string> {    struct.std::_Vector_base {        struct.std::_Vector_base<std::__cxx11::basic_string<char>, std::allocator<std::__cxx11::basic_string<char> > >::_Vector_impl {            struct.std::_Vector_base<std::__cxx11::basic_string<char>, std::allocator<std::__cxx11::basic_string<char> > >::_Vector_impl_data {                class.std::__cxx11::basic_string*start;                class.std::__cxx11::basic_string*finish;                class.std::__cxx11::basic_string*end_of_storage;            } field_0;        } field_0;    } field_0;} field_0; | 
为什么需要有一个end_of_storage呢,vector是一个动态的容器,他会根据元素数目分配固定内存,但是当有新的元素加入时,如果分配的固定内存不足以存放新的元素的话,则会进行扩容。因此,这个指针则用于对vector需要扩容时使用(需要注意的是元素的大小取决于模板类型type的大小,因此存在(finish-start)%sizeof(type)==0)。具体的内存示意图可以如下图所示。
rb_tree即红黑树,具体的定义可以去网上翻阅资料,这里并不讨论其具体的实现和算法,仅仅讨论其数据结构在内存中的表示。rb_tree分为两个部分,一个适用于比较的key_compare,另一个则是header。重点分析header,header中存在一个node,和node_count。node即这个红黑树的起始节点,而node_count则代表这颗红黑树拥有多少的节点。node中描述了当前节点的颜色,父亲节点,左儿子和右儿子节点。
| 
      1
      
      2
      
      3
      
      4
      
      5
      
      6
      
      7
      
      8
      
      9
      
      10
      
      11
      
      12
      
      13
      
      14
      
      15
      
      16
      
      17
      
      18
      
      19
      
      20
      | class.std::map<string,string> {    class.std::_Rb_tree.6{        struct.std::_Rb_tree<std::__cxx11::basic_string<char>, std::pair<const std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> >, std::_Select1st<std::pair<const std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> > >, std::less<std::__cxx11::basic_string<char> >, std::allocator<std::pair<const std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> > > >::_Rb_tree_impl {            struct.std::_Rb_tree_key_compare {                struct.std::less {                    uint8_t value;                } key_compare;            } compare;            struct.std::_Rb_tree_header {                struct.std::_Rb_tree_node_base {                    uint32_t color;                    struct.std::_Rb_tree_node_base*parent;                    struct.std::_Rb_tree_node_base*left;                    struct.std::_Rb_tree_node_base*right;                } node;                uint64_t node_count;            } field_1;        } field_0;    } field_0;} field_0; | 
这里需要注意的是,当红黑树中不存在节点时,header中node的parent是0,而left和right则指向node自身。当插入了结点之后,这个parent则会指向树的根节点,而left和right则指向红黑树的最左边的节点和最右边的节点。
同时需要分清楚rb_tree变量本身和树本身。rb_tree变量中的node是不携带具体数据元素的,而树本身的节点在其node_base结构体结束后的内存区域则是数据元素。具体的内存示意图如下图所示。
map和set的实现皆是基于rb_tree,而唯一不同之处在于set直接在node中存储数据,而map在node中存储的是键值对,是一个pair,而pair<a,b>在内存中的表示则是直接a后存放b。
unordered_map和unordered_set的底层是由hashtable实现的。Prime_rehash_policy结构体适用于hashtable的rehash操作的不做分析,这里我们主要需要关注的是buckets,这个指针指向了一个_Hash_node_base*数组,如果指针为null则无效,这些指针数组中的指针指向的是一个链表,可以通过不断的访问next遍历链表中的所有node。bucket_count则表示buckets的大小,而element_count指的当前hashtable中所有的节点数目。默认的hashtable采用取模的方式找到对应的bucket,bucket_count一般为13。
| 
      1
      
      2
      
      3
      
      4
      
      5
      
      6
      
      7
      
      8
      
      9
      
      10
      
      11
      
      12
      
      13
      
      14
      
      15
      | class.std::unordered_map<int, string> {        class.std::_Hashtable {                struct.std::__detail::_Hash_node_base**buckets;                uint64_t bucket_count;                struct.std::__detail::_Hash_node_base {                        struct.std::__detail::_Hash_node_base*next;                } field_2;                uint64_t element_count;                struct.std::__detail::_Prime_rehash_policy {                        floatmax_load_factor;                        uint64_t next_resize;                } field_4;                struct.std::__detail::_Hash_node_base*field_5;        } field_0;} field_0; | 
因此需要遍历一个hashtable只需要遍历其bucket找到有效的list,并遍历各个list就能找到所有的数据了,类似rb_tree,元素的数据存储与链表结构_Hash_node_base的后面。具体的内存模型如下图所示。
string的结构比较简单。主要由一个指针ptr和字符串长度string_length构成。除此之外还会存在一个联合体,当字符串的长度小于8时会直接存储与这个缓冲区中,此时ptr指针将指向这个local_buf,大于这个local_buf将会在堆上另外分配内存,此时这个union转而用于存储分配堆的大小。
| 
      1
      
      2
      
      3
      
      4
      
      5
      
      6
      
      7
      
      8
      
      9
      
      10
      | class.std::__cxx11::basic_string {    struct.std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_Alloc_hider {        uint8_t*ptr;    } dataplus;    uint64_t string_length;    union.anon {        uint64_t allocated_capacity;        uint8_t local_buf[8];    };} field_0; | 
这样设计的目的是方便字符串大小变化的适应,便于实现字符串拼接等工作,且小字符串不用在堆上分配,对速度有少量提升。具体的内存示意图如下图所示,左边的是长度小于8的string。
该题给与了一个ELF文件,并给出了hint.txt:Recursive stl containers ( > 3 ), Identify and traverse these containers
不难知道这个题目与STL容器密切相关,配合traverse不难猜测肯定是使用stl容器套容器的操作,先打开ida看看实现,找到main函数。
经过动调,可以定位到代码输入函数所在位置,在main中给他命名为input_check。而他上面的函数明显初始化了一个deque的数据结构,第一行对应map,第二行对应map_size,接下来是两个iterator,四个qword一组。
点开每个iterator的cur,也就是cur,可以看到其中存储了两个元素。
如果理解了上述的数据结构内存模型,很容易看出来其实是rb_tree,第一个qword是compare_key,第二个qword是color,第三个qword代表parent,第四个和第五个是left和right,第六个qword则代表node_count,因此其实就是deque<rb_tree<>>这种嵌套方式。
随意点开rb_tree中的一个node,分析一下rb_tree对应的模板类型,可以看到他存储的其实是std::pair<int,hashtable>,所以其实是deque<map<int,hashtable<>>>,为什么我会知道是int呢,因为后文分析输入流时输入的都是数字,并通过数字从容器中访问内部数据。
继续查看一下hash_table的buckets。
随意点开一个bucket,里面存储的是个链表,链表节点数据中存储的也是一个std::pair<int,deque>,因此这其实是一个unordered_map<int,deque>
到这里我们对该变量有了大体的认识,大概率是不断地stl容器嵌套,接下来继续去分析输入和这些容器之间的关系。input_check函数通过input_num输入一个数字,并且返回该容器对应元素。这个明显是deque的取元素操作。
然后通过step1跳转到下一层,下一层和input_check逻辑其实是差不多的,也是输入一个数字,然后获取map中对应的元素。
然后通过step2跳转到下一层,持续同样的操作,只不过这一层是unordered_map罢了。
这样的顺序刚好和我们之前分析的数据类型嵌套的顺序相符,所以我们需要根据这些函数来反推出这些数据结构的嵌套顺序。不难发现unordered_map的代码明显存在一个取mod的操作,而map存在std::rebalanced的操作。因此可以写出如下脚本来分析出容器的嵌套类型。、
| 
      1
      
      2
      
      3
      
      4
      
      5
      
      6
      
      7
      
      8
      
      9
      
      10
      
      11
      
      12
      
      13
      
      14
      
      15
      
      16
      
      17
      
      18
      
      19
      
      20
      
      21
      
      22
      
      23
      
      24
      
      25
      
      26
      
      27
      | importida_bytesimportida_funcsimportida_xrefimportidaapistep_funcs ={}funclist =Functions()rebalance_func =Noneforf infunclist:    name =ida_funcs.get_func_name(f)    if'step'inname:        step_funcs[name] =f    elif'rebalance'inname:        rebalance_func =fassertrebalance_func !=Nonestep_type ={}forname, addr instep_funcs.items():    code =str(idaapi.decompile(addr))    if'rebalance'incode:        step_type[name] ='map'    elif'%'incode:        step_type[name] ='hash_map'    else:        step_type[name] ='deque'fori inrange(28):    print('\''+step_type['step'+str(i +1)] +'\''+', ', end ='') | 
然后根据上述的出的容器嵌套顺序和对应容器的内存模型dump出对应的数据结构。
| 
      1
      
      2
      
      3
      
      4
      
      5
      
      6
      
      7
      
      8
      
      9
      
      10
      
      11
      
      12
      
      13
      
      14
      
      15
      
      16
      
      17
      
      18
      
      19
      
      20
      
      21
      
      22
      
      23
      
      24
      
      25
      
      26
      
      27
      
      28
      
      29
      
      30
      
      31
      
      32
      
      33
      
      34
      
      35
      
      36
      
      37
      
      38
      
      39
      
      40
      
      41
      
      42
      
      43
      
      44
      
      45
      
      46
      
      47
      
      48
      
      49
      
      50
      
      51
      
      52
      
      53
      
      54
      
      55
      
      56
      
      57
      
      58
      
      59
      
      60
      
      61
      
      62
      
      63
      
      64
      
      65
      
      66
      
      67
      
      68
      
      69
      
      70
      
      71
      
      72
      
      73
      
      74
      
      75
      
      76
      
      77
      
      78
      
      79
      
      80
      
      81
      
      82
      
      83
      
      84
      
      85
      
      86
      
      87
      
      88
      
      89
      
      90
      
      91
      
      92
      
      93
      
      94
      
      95
      
      96
      
      97
      
      98
      
      99
      
      100
      
      101
      
      102
      
      103
      
      104
      
      105
      
      106
      
      107
      
      108
      
      109
      
      110
      
      111
      
      112
      
      113
      
      114
      
      115
      | importida_bytesimportidaapisize_table ={    'map': 48,    'deque': 80,    'string': 32,    'hash_map': 16,}defdump_rbtree_node(addr):    return{        'color': ida_bytes.get_qword(addr),        'parent': ida_bytes.get_qword(addr +8),        'left': ida_bytes.get_qword(addr +16),        'right': ida_bytes.get_qword(addr +24),    }defdump_rbtree_map(addr):    key =ida_bytes.get_qword(addr)    node_addr =addr +8    node_num =ida_bytes.get_qword(addr +40)    result ={}    defvisit(node):        ifnode ==idaapi.BADADDR:            return        info =dump_rbtree_node(node)        assertinfo['color'] ==1orinfo['color'] ==0        value_key =ida_bytes.get_qword(node +32)        data_addr =node +40        result[value_key] =data_addr        ifinfo['left'] !=0:            visit(info['left'])        ifinfo['right'] !=0:            visit(info['right'])    d =dump_rbtree_node(node_addr)    visit(d['parent'])    assertlen(result.keys()) ==node_num    returnresultdefdump_deque_iterator(addr):    return{        'cur': ida_bytes.get_qword(addr),        'first': ida_bytes.get_qword(addr +8),        'last': ida_bytes.get_qword(addr +16),        'map': ida_bytes.get_qword(addr +24),    }defdump_deque(addr, delta):    deque_map =ida_bytes.get_qword(addr)    map_size =ida_bytes.get_qword(addr +8)    assertmap_size ==8    start =dump_deque_iterator(addr +16)    finish =dump_deque_iterator(addr +16+32)    assertstart['last'] ==finish['last'] andstart['first'] ==finish['first'] andstart['map'] ==finish['map']    assert(finish['cur'] -start['cur']) %delta ==0    ptr =start['cur']    index =0    result ={}    whileptr !=finish['cur']:        result[index] =ptr        index +=1        ptr +=delta    returnresultdefdump_string(addr):    data =ida_bytes.get_qword(addr)    length =ida_bytes.get_qword(addr +8)    returnida_bytes.get_bytes(data, length).decode()defdump_hashtable_map(addr):    addrs =set()    hash_table =ida_bytes.get_qword(addr)    table_num =ida_bytes.get_qword(addr +8)    fori inrange(table_num):        link_list =ida_bytes.get_qword(hash_table +8*i)        iflink_list ==0:            continue        ptr =ida_bytes.get_qword(link_list)        whileptr !=0:            assertptr !=idaapi.BADADDR            addrs.add(ptr)            ptr =ida_bytes.get_qword(ptr)    result ={}    fora inaddrs:        result[ida_bytes.get_qword(a +8)] =a +16    returnresulttype_list =['deque', 'map', 'hash_map', 'deque', 'map', 'map', 'hash_map', 'hash_map', 'map', 'map', 'hash_map', 'map', 'map', 'deque', 'deque', 'map', 'map', 'map', 'map', 'map', 'map', 'map', 'map', 'map', 'map', 'map', 'deque', 'deque', 'string']defdump_dfs(depth, addr):    stl_type =type_list[depth]    tmp ={}    ifstl_type =='map':        tmp =dump_rbtree_map(addr)    elifstl_type =='hash_map':        tmp =dump_hashtable_map(addr)    elifstl_type =='deque':        next_type =type_list[depth +1]        tmp =dump_deque(addr, size_table[next_type])    elifstl_type =='string':        returndump_string(addr)    else:        assertFalse    node ={}    fork, v intmp.items():        node[k] =dump_dfs(depth +1, v)    returnnoderesult =dump_dfs(0, 0x7FFEA5D3A460)importjsonwith open('json_data.txt','w+') as f:    json.dump(result, f)#dump_rbtree_map(0x55F47B277658) | 
最后得到如下数据文件,是一个dict的嵌套,最后保存的是字符串。
最后main函数通过对比字符串,来判断我们输入的一系列数字是否能够访问到对应的字符串,从而判断数字是否正确。
最后根据dump出的dict从而写出dfs查找出对应的数字序列,从而获得flag。
| 
      1
      
      2
      
      3
      
      4
      
      5
      
      6
      
      7
      
      8
      
      9
      
      10
      
      11
      
      12
      
      13
      
      14
      
      15
      
      16
      
      17
      | target ='8850a16d-e427-446e-b4df-5f45376e20e4'path =[]defdfs(depth, node):    globalpath    iftype(node) ==dict:        fork, v innode.items():            path.append(k)            dfs(depth +1, v)            path.pop()    else:        ifnode ==target:            print(path)importjsonf =open('json_data.txt','r')data =json.load(f)f.close()dfs(0, data) | 
更多【STL容器逆向与实战(N1CTF2022 cppmaster wp)】相关视频教程:www.yxfzedu.com