STL逆向

c++ STL

STL 逆向方法论

  • 识别 STL 容器类型
  • 识别 STL 容器操作
  • 提取 STL 容器中的数据

逆向 STL 主要的问题:

  1. 缺少符号,无法判断 STL 容器类型 (主要困难)
  2. STL 内部数据结构复杂,难以提取数据
  3. 优化使大量 stl 函数被 inline

方法:解析 STL 容器内存数据,从内存数据角度判断容器类型,提取容器内容判断函数操作。

常见 STL 容器

序列容器

  • vector
  • Deque
  • List
  • String

关联容器

  • Pair
  • Map
  • Set
  • MultiMap / MultiSet

无序关联容器:unordered_map

C++ STL关联式容器是什么?

此类容器在存储元素值的同时,还会为各元素额外再配备一个值(又称为“键”,其本质也是一个 C++ 基础数据类型或自定义类型的元素),它的功能是在使用关联式容器的过程中,如果已知目标元素的键的值,则直接通过该键就可以找到目标元素,而无需再通过遍历整个容器的方式。

弃用序列式容器,转而选用关联式容器存储元素,往往就是看中了关联式容器可以快速查找、读取或者删除所存储的元素,同时该类型容器插入元素的效率也比序列式容器高。

常见 STL 容器适配器

容器适配器是一个封装了序列容器的类模板,它在一般序列容器的基础上提供了一些不同的功能。之所以称作适配器类,是因为它可以通过适配容器现有的接口来提供不同的功能。

其实,容器适配器中的“适配器”,和生活中常见的电源适配器中“适配器”的含义非常接近。我们知道,无论是电脑、手机还是其它电器,充电时都无法直接使用 220V 的交流电,为了方便用户使用,各个电器厂商都会提供一个适用于自己产品的电源线,它可以将 220V 的交流电转换成适合电器使用的低压直流电。

从用户的角度看,电源线扮演的角色就是将原本不适用的交流电变得适用,因此其又被称为电源适配器。

举一个例子,假设一个代码模块 A,它的构成如下所示:

class A{
public:
    void f1(){}
    void f2(){}
    void f3(){}
    void f4(){}
};

现在我们需要设计一个模板 B,但发现,其实只需要组合一下模块 A 中的 f1()、f2()、f3(),就可以实现模板 B 需要的功能。其中 f1() 单独使用即可,而 f2() 和 f3() 需要组合起来使用,如下所示:

class B{
private:
    A * a;
public:
    void g1(){
        a->f1();
    }
    void g2(){
        a->f2();
        a->f3();
    }
};

可以看到,就如同是电源适配器将不适用的交流电变得适用一样,模板 B 将不适合直接拿来用的模板 A 变得适用了,因此我们可以将模板 B 称为 B 适配器.

容器适配器也是同样的道理,简单的理解容器适配器,其就是将不适用的序列式容器(包括 vector、deque 和 list)变得适用。容器适配器的底层实现和模板 A、B 的关系是完全相同的,即通过封装某个序列式容器,并重新组合该容器中包含的成员函数,使其满足某些特定场景的需要。

stack(STL stack)容器适配器:采用默认的 deque 基础容器

queue容器适配器:其底层使用的基础容器选择默认的 deque 容器

priority_queue :vector

逆向时处理容器适配器只需要用底层容器来处理即可

STL 各高级类型dump数据

std::string

  • 固定长度 32 字节,4 个 dq
  • 第一个指针字段指向数据地址
  • 第二个 size_t 字段存储字符串长度

内存分布:

image-20230120202731025

IDA dump脚本:

def read_dbg_cppstr_64(objectAddr):
    # a easy function for read std:string
    # 首地址就是begin指针
    strPtr = idc.read_dbg_qword(objectAddr)
    result = ''
    i = 0
    while True:
        onebyte = idc.read_dbg_byte(strPtr + i)
        if onebyte == 0:
            break
        else:
            result = chr(onebyte)
        i += 1
        return result

std::stringsteam

可以看作是读写文件

内存分布:image-20230120203121252

image-20230120203135198

std::vector

  • 固定长度 24 字节,3 个 dq
  • 第一个指针字段指向数组起始地址
  • 第二个指针字段指向数组最后元素地址
  • 第三个指针字段指向最大内存地址

内存分布:image-20230120203330744

IDA dump脚本

def vetor_dump(addr):
    ELEMENT_SIZE = 8
    data_addr = []
    vetor_base = idc.read_dbg_qword(addr + 0x0)
    vetor_end = idc.read_dbg_qword(addr + 0x8)
    for i in range(vetor_base,vetor_end,ELEMENT_SIZE):
        data_addr.append(i)
    return data_addr

std::list

  • 双向循环链表存储
  • 头结构 + 节点结构
  • 遍历长度可以用 size 字段确定

image-20230120203453756

内存布局:image-20230120203507098

IDA dump 脚本:

def dump_stl_list(p_list_addr):
    data_addr = []
    list_size = idc.read_dbg_qword(p_list_addr+0x10)
    cur_node = p_list_addr
    for i in range(list_size):
        cur_node = idc.read_dbg_qword(cur_node + 0x0)
        data_addr.append(cur_node + 0x10)

std::deque

  • 头结构 + 迭代器结构
  • 适用 std::deque / std::stack

image-20230120203905814

  1. stl_deque.start.node 确定first map 项位置
  2. stl_deque.start.last - stl_deque.start.first 确定缓冲区大小
  3. stl_deque.finish.node 确定last map 项位置
  4. 对于每一个 map 项:
    1. start 项,解析 cur, last 区间
    2. finish 项,解析 start, cur 区间
    3. 其余项,解析 start, last 区间

内存布局:image-20230120203718899

IDA dump 脚本:

deque_iter = namedtuple('deque_iter',['cur','first','last','node'])
def parse_iter(addr):
    # 解析队列迭代器
    cur = idc.read_dbg_qword(addr + 0x0)
    first = idc.read_dbg_qword(addr + 0x8)
    last = idc.read_dbg_qword(addr + 0x10)
    node = idc.read_dbg_qword(addr + 0x18)
    return deque_iter(cur,first,last,node)

def dump_deque(addr):
    ELEMENT_SIZE = 4    # std::deque<xx> xx 的类型大小来指定
    data_addr = []
    start_iter = parse_iter(addr + 0x10)
    finish_iter = parse_iter(addr + 0x30)
    buf_size = start_iter.last - start_iter.first
    map_size = start_iter.node
    map_finish = finish_iter.node

    # 解析第一个缓存数据
    for i in range(start_iter.cur,start_iter.last,ELEMENT_SIZE):
        data_addr.append(i)

    # 解析最后一个缓存数据
    for i in range(finish_iter.first,finish_iter.cur,ELEMENT_SIZE):
        data_addr.append(i)

    # 解析中间缓存数据
    for i in range(map_start + 8,map_finish - 8,8):
        buf_start = idc.read_dbg_qword(b)
        for i in range(buf_start,buf_start + buf_size, ELEMENT_SIZE):
            data_addr.append(i)
    return data_addr

std::map

  • 底层采用 Rb-Tree 实现(红黑二叉树)
  • 头结构 + 节点结构
  • 用二叉树遍历可提取数据
  • 适用 std::map / std::set / std::multimap / std::multiset

内存分布:image-20230120204147238

IDA dump 脚本:

def parse_gnu_map_header(address):
    root = idc.read_dbg_qword(address + 0x10)
    return root

def parse_gnu_map_node(address):
    left = idc.read_dbg_qword(address + 0x10)
    right = idc.read_dbg_dword(address + 0x10)
    data = address + 0x20
    return left, right, data

def parse_gnu_map_travel(address):
    # address <—— std::map struct address
    result = []
    worklist = [parse_gnu_map_header(address)]
    while len(worklist) > 0:
        addr = worklist.pop()
        (left, right, data) = parse_gnu_map_node(addr)
        if left > 0: worklist.append(left)
        if right > 0: worklist.append(right)
        result.append(data)
    return result

std::unsorted_map

  • 底层采用 HashTable 实现
  • 头结构 + Bucket 数组 + 节点结构
  • 所有节点结构用单链表串联(****dump 只需要遍历单链表)
  • 头结构的第三个字段为单链表头
  • 适用 **std::**unsorted_map **/ std::**unsorted_set / …

内存布局:image-20230120204319472

IDA dump 脚本:

def dump_stl_hashmap(addr):
    # dump stl hashmap gnu c++ x64
    data_addr = []
    bucket_addr = idc.read_dbg_qword(addr + 0x10)
    node_addr = bucket_addr
    while node_addr != 0:
        data_addr.append(node_addr + 0x8)
        node_addr = idc.read_dbg_qword(node_addr)
    return data_addr

std::shared_ptr

第一个指针就是数据指针

内存布局:image-20230120204555397


实战部分


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 jaytp@qq.com

×

喜欢就点赞,疼爱就打赏