Reverse¶
真正的签到¶
出题思路¶
本身作为签到题就没必要太刁难人,主要考察脱压缩壳(re选手基础技能),正好在4月的脱壳分享会也说了要出一道这种题。脱完壳后就打算搞个简单的加减乘除,但是还是出题时候考虑不周,出现了多解的情况。(按照正常思路一般都是一个解。)
问题主要出在当时做了一个除法的操作,因为C语言中5/2 与4/2都为2。
解题方法¶
法1¶
- 第一步,查壳,发现为upx。
-
直接可以用脱壳软件脱壳也可esp定律等手动脱壳。
-
脱壳后分析代码。
- 直接写脚本
#include<stdio.h> #include<string.h> int main(){ char fstr[17] = "pbm`KkL`dKQ2KeJLd"; char theflag[17]; char flag[17] = "scu_ctf_f4k3_f14g"; int i = 0; for(i = 0; i < 17; i++) theflag[i] = fstr[i]*2-flag[i]; for(i = 0; i < 17; i++) printf("%c",theflag[i]); }
法2¶
- 前面步骤一直,后面直接angr梭哈
import angr p = angr.Project('sign.exe', auto_load_libs=False) st = p.factory.call_state(addr=0x401520, add_options=angr.options.unicorn) sim = p.factory.simgr(st) sim.explore(find=0x40155e, avoid=0x40156c) print(sim.one_found.posix.dumps(0))
这个地方就可以看出,有多解情况了。
太空大战¶
出题思路¶
这题是由God sun出的,大概主要考察一个.net,加之让比赛变的更有趣一点,放了个小游戏上去。只要打完180个灰机(一个不落)控制台就会输出flag
(180个飞机,无需逆向,轻轻松松就可以打败。
解题方法¶
- 关键代码在assets/bin/Date/Managed/Assembly-CSharp.dll
- ⽤.NET Reflector打开分析
-
经过分析可以得知,每击落一架分级,调用一次这个关键方法。由代码可以看到总共需要摧毁了180个。(其实总共也就180个)
-
写解题脚本
import hashlib mask =[49552,26516,15988,29987,52902,33151,8086,39920,3604,21497,19862,12268,50822,26111,35391,20661,6370,14029,26707,42890,19391,13836,61102,38705,45159,12927,47794,39183,20776,44532,18925,4854,60596,11941,28994,11166,57586,48918,13199,42006,62781,31480,50464,53893,21233,61456,55842,46591,10574,45253,50991,44866,45945,17105,27273,18925,41001,64310,51846,46279,14977,61079,26330,1192,61190,38989,36161,17001,38576,49567,55929,31759,54550,12759,13756,60929,36365,27308,57132,42483,42263,57086,55839,13568,37191,18388,34592,4189,65492,24673,27016,6941,33229, 4180,35454,64874,36708,22948] l = len(mask) secret = "jFEQ6xFkUxKGzUbn" for i in range(1,181): secret = hashlib.md5((secret+str(mask[i%l])).encode()).hexdigest() if '6a37460f25c719a4' in secret: print (secret[0:16])
- 注意这里很多选手以为只调用一次,所以直接拿180%98 去处理,算出来的是错的。
PY 交易¶
出题思路¶
这个题目出题主要想考察一下python的逆向,校内打校外比赛的不多,见得题目相对较少。所以本着拓宽学习的目的,出了这道还原字节码的题目。相对来说这道题不是太难,通过相关博客搜索,然后一步步分析还原,还原后dis检验。
解题方法¶
- 首先直接还原python代码就好了,还原结果如下
inputs = input("please your flag:") inputs = inputs[7:-1] flag = "th31_scuctf_eXclus1v3" theflag = "" i = 0 j =0 print(flag[0]) if(len(flag) != len(inputs)): print("Error!") for i in range(0,len(flag)-14): theflag += (chr(ord(flag[i])+ord(inputs[i+8]))) for i in range(10,len(flag)-6): theflag += (chr(ord(flag[i])+ord(inputs[i-8]))) j = i+1 for i in range(j,len(flag)): theflag += (chr(ord(flag[i-3])+ord(inputs[i]))) flags =list(theflag) for i in range(0,len(flags)//2): flags[i] = chr(ord(flags[i])+20) #Flag scuctf{} #The flag text starts with "d1" and the eighth bit is "3" flagt = flags[len(flags)//2:len(flags)] theflag = "".join(flagt) for k in range(0,len(flags)//2): theflag += "".join(flags[k]) if(theflag == '×\x8bÙÍ\x8cÓÜî¤ú±¬¤¤úÖíÒ'): print("You win!") else: print("Error!!!")
- 接着就是逆向分析,写解题脚本
法1¶
enflag = '×\x8bÙÍ\x8cÓÜî¤ú±¬¤¤úÖíÒ' flag = 'th31_scuctf_eXclus1v3' ans = 'd1' + '*' * 19 step1 = enflag[9:] + enflag[0:9] theflag ='' for i in range(0,9): theflag += chr(ord(step1[i]) - 20) theflag += step1[9:] inputs = list(ans) for i in range(0,7): inputs[i + 8] = chr(ord(theflag[i]) - ord(flag[i])) for i in range(10,15): inputs[i - 8] = chr(ord(theflag[i - 3]) - ord(flag[i])) for i in range(15,21): inputs[i] = chr(ord(theflag[i - 3]) - ord(flag[i - 3])) inputs[7] = '3' print('scuctf{' + ''.join(inputs) + '}')
法2¶
from z3 import * flag = "th31_scuctf_eXclus1v3" dist = "×ÙÍÓÜî¤ú±¬¤¤úÖíÒ" inp = [BitVec(('x%s' % i), 8) for i in range(len(flag))] theflag = [] for i in range(0, len(flag) - 14): theflag.append(ord(flag[i]) + inp[i + 8]) for i in range(10, len(flag) - 6): theflag.append(ord(flag[i]) + inp[i - 8]) for i in range(len(flag) - 6, len(flag)): theflag.append(ord(flag[i - 3]) + inp[i]) flags = [_ for _ in theflag] for i in range(len(flags) // 2): flags[i] = flags[i] + 20 theflag = theflag[len(flags) // 2:] for i in range(len(flags) // 2): theflag.append(flags[i]) solver = Solver() for i in zip(theflag, dist): solver.append(i[0] == ord(i[1])) solver.check() model = solver.model() for i, v in enumerate(inp): try: print(chr(model[v].as_long()), end='') except: print(' ', end='')
- 题目前两位和第八位无法解除,题目中已经提示具体字符
ONIbase64¶
出题思路¶
本道题主要就是考察一个ollvm平坦化。也没想到这么惨烈。
解题方法¶
-
文件拉到最后就可以看到编译器地址,直接把它pull下来,编译.s文件得到可执行文件。
-
拖入IDA分析,是个标准的平坦化。
-
参考https://github.com/pcy190/deflat 去除平坦化
-
然后直接F5写解密脚本
from z3 import * from functools import reduce table = 'ZAnUX1W2oPNQ4sBMOd/+ChfGI5r8Hvt3uaLkbDgcyJYTipez6mxF0SEqRjVKwl97' coding = '5auRs6a4A2lEUObG5+uoPGuWHnimZLXtvkyEHxCFoal5' dist = map(lambda x: BitVecVal(table.find(x), 6), coding) flag = [BitVec('c%d' % i, 8) for i in range(32)] total = Concat(flag) s = [Extract(32 * (i + 1) - 1, 32 * i, total) for i in range(8)] temps = reduce(lambda x, y: x ^ y, s, 0) s = [i ^ temps for i in s] s.reverse() total = Concat(s) bits = [Extract(8 * (i + 1) - 1, 8 * i, total) for i in range(32)] bits = bits + [reduce(lambda x, y: x ^ y, bits)] tup = [bits[i:i + 3] for i in range(0, len(bits), 3)] outs = [] padding = BitVecVal(0, 2) for i, v in enumerate(tup): t = Concat(v) s1 = Extract(23, 18, t) s2 = Extract(17, 12, t) s3 = Extract(11, 6, t) s4 = Extract(5, 0, t) outs.append(s1) outs.append(s2) outs.append(s3) outs.append(s4) for v2 in tup[i + 1:]: v2[0] = v2[0] ^ Concat(padding, s1) v2[1] = v2[1] ^ Concat(padding, s2) v2[2] = v2[2] ^ Concat(padding, s3) solve = Solver() for i, v in enumerate(dist): solve.add(outs[i] == v) solve.check() model = solve.model() print(''.join(map(lambda x: chr(model.eval(x, 8).as_long()), reversed(flag))))
easy_re&easy_base¶
出题思路¶
既然要搞花样,当然少不当今最火的iot。采用腾讯TencentOS tiny 官方定制IoT开发板EVB_LX(暂时是限量的)编译环境: https://github.com/Tencent/TencentOS-tiny两个题目,都是考察找到被替换的base64密码表,由于考虑到直接上base有点难,所以出了一个easy_re过渡。
两个题目替换都涉及四段字符如下(把初始密码表拆分为四段):
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz" "0123456789" "+/"
easy_re是改变了这四段字符压栈顺序。没想到ida太过于智能化,显示结果即是正确压栈顺序。
easy_base考察偏难了,主要是对这四段字符进行了一些变换,如果逆向分析的话需要学习risc-v指令集。
当然,这两个题最简单的方法是把程序放入对应开发板里,他相应的串口也会输出字母表。
做题过程中也发现一些选手拿到题目直接猜测arm架构,拿着ida 当arm分析,还原的内容是错的,无从下手。如果拿到文件后File一下也会知道是risc-v架构。不至于走偏。
解题方法¶
-
首先,ida默认不支持risc-v,所以需要下载相关插件。https://github.com/lcq2/riscv-ida
-
然后,ida打开分析,直接就有正确的字母表压栈顺序,(原本是想让选手分析简单指令来确定或者爆破)
- 得到 字母表就很容易解出来了
import base64 str1 = "PalXPrhnOrLZT6PVQJ1oNr9dSqDVTbo==" string1 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ+/abcdefghijklmnopqrstuvwxyz" string2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" print(base64.b64decode(str1.translate(str.maketrans(string1, string2))))
-
easy_base 的话就需要分析指令得出具体操作或者直接开发板跑一下得到输出
-
看到大多数人解题无果,比赛最后放出了一个risc-v 64位的附件(代码一样),通过docker跑即可得到table。
- 然后直接解密得到flag
import base64 str1 = "UoH+U/DJV/YlQdUOU94JPYxJgdHMUWK=" string1 = "a0b1c2d3e4f5g6h7i8j9ZYXWVUTSRQPON+klmnopqrABCDEFGHIJKLM/stuvwxyz" string2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" print(base64.b64decode(str1.translate(str.maketrans(string1, string2))))
- 如果对题目感兴趣的,可以之后再研究。我附上题目主文件源代码
#include "mcu_init.h" #include "tos_k.h" #define TASK_SIZE 1024 k_task_t k_task_task1; k_task_t k_task_task2; uint8_t k_task1_stk[TASK_SIZE]; uint8_t k_task2_stk[TASK_SIZE]; int share = 0xCBA7F9; k_sem_t sem; unsigned char *scuctf_flag_base64="UoH+U/DJV/YlQdUOU94JPYxJgdHMUWK="; unsigned char base64_right[65]=""; void scuctf_base64(void) { unsigned char base64_1[26]="ABCDEFGHIJKLMNOPQRSTUVWXYZ"; unsigned char base64_2[26]="abcdefghijklmnopqrstuvwxyz"; unsigned char base64_3[10]="0123456789"; unsigned char base64_4[2]="+/"; int i=0,j=0,k=0,q=25,r=10,n=0; for(i=0;i<20;) { base64_right[i]=base64_2[j]; j++; base64_right[i+1]=base64_3[k]; k++; i+=2; } for(i=20;i<33;i++) { base64_right[i]=base64_1[q]; q--; } for(i=33;i<42;i++) { if(i==33) { base64_right[33]=base64_4[0]; } else { base64_right[i]=base64_2[r]; r++; } } for(i=42;i<64;i++) { if(n<13) { base64_right[i]=base64_1[n]; n++; } else { if(i==55) { base64_right[i]=base64_4[1]; } else { base64_right[i]=base64_2[r]; r++; } } } } void task1(void *pdata) { int task_cnt1 = 0; while (1) { printf("welcome scuctf from %s cnt: %d\n", __func__, task_cnt1++); tos_sem_pend(&sem, ~0U); gpio_bit_write(GPIOA, GPIO_PIN_7, share % 2); } } void task2(void *pdata) { int task_cnt2 = 0; scuctf_base64(); while (1) { share++; for(int i=0; i<5; i++) { printf("Where is scuctf_base64? %s cnt: %08x\n%s", __func__, task_cnt2--,base64_right); tos_task_delay(50); } tos_sem_post(&sem); } } void main(void) { board_init(); usart0_init(115200); tos_knl_init(); tos_task_create(&k_task_task1, "task1", task1, NULL, 3, k_task1_stk, TASK_SIZE, 0); tos_task_create(&k_task_task2, "task2", task2, NULL, 3, k_task2_stk, TASK_SIZE, 0); k_err_t err = tos_sem_create(&sem, 1); if (err != K_ERR_NONE) { goto die; } tos_knl_start(); die: while (1) { asm("wfi;"); } } int _put_char(int ch) { usart_data_transmit(USART0, (uint8_t) ch ); while (usart_flag_get(USART0, USART_FLAG_TBE)== RESET){ } return ch; }
小结¶
由于是普通校赛,题目也没出过分难,个人感觉难以把握还算可以。这次题目主要也本着打破传统scuctf 常规题目,一丢丢小小的创新。 .NET,risc-v,ollvm,apk,python等。即使这些可能在全国ctf中是常见题目,但是感觉校内还是几乎没出的。比赛过程中也发生了很多趣味东西,比如第一题一题多解,flag设置时候多加了空格导致选手提交报错等好多问题。
总之希望scuctf越来越有趣,参与人数越来越多吧!