ISCC 练武校级赛和擂台部分 WP,主要是 Mobile 和 Misc
练武
Misc
书法大师
下载附件,图片属性内找到字符串L9k8JhGfDsA
,binwalk 发现一个压缩包(其实一开始的附件里有一堆压缩包,但只有 1-50 或 45 才有用),拿字符串解密得到 message45.txt,内容如下
1 右女 丙色 太歌 从少 生乙 女下 没个 没医 石成 男真 那比 市尖 丙比 成慢 作〇 切厂 石站 马〇 片摔 尖趣 耳回 石乙 女群 工睡
猜测是和笔画数有关,两个一组,写个脚本用cnchar-data
查询
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 import requestsfrom urllib import parseimport jsonurl = "https://unpkg.com/cnchar-data@1.1.0/draw/" output = '' data = "右女 丙色 太歌 从少 生乙 女下 没个 没医 石成 男真 那比 市尖 丙比 成慢 作〇 切厂 石站 马〇 片摔 尖趣 耳回 石乙 女群 工睡" def get_strokes (x ): try : res = requests.get(url + parse.quote(x) + ".json" , timeout=10 ) res.raise_for_status() j = json.loads(res.text)['strokes' ] return str (hex (len (j))[2 :]) except : print (f"Error fetching strokes for character '{x} '" ) return '00' for char in data: if char == ' ' : output += ' ' else : output += get_strokes(char) print (output) print (output)
解出来再用CyberChef
,From Hex
后From Base64
反方向的钟
下载附件,打开 txt 发现第二个字符不太对,疑似 Unicode 零宽字符加密
用自己的 unicodetool.html 工具解密,或 Github 有个项目叫 Hidden Word
查看图片,时间指向 4 点 5 分,数字 8 推测为日期,图片名为 202502
那时间就是 202502080405,wav 不知道是啥
然后 😡 然后发现跟这些没关系,拿 txt 里的 base64 解一下再 xor 那个文本就出来了
Mobile
简单配一下环境
下载adb 并配置 Path,然后用 Android Studio 自带模拟器开个虚拟机
下载frida-server ,需要的架构可以用 adb 命令查看
1 adb shell getprop ro.product.cpu.abi
我的架构是x86_64
,那就下载frida-server-XX.XX.XX-android-x86_64.xz
并解压
运行以下命令
1 2 3 4 5 6 adb push .\frida-server-android-x86_64 /data/local/tmp adb shell su cd /data/local/tmpchmod +x frida-server-android-x86_64./frida-server-android-x86_64
然后在新终端设置端口转发并查看进程
1 2 3 4 adb forward tcp:27042 tcp:27042 adb forward tcp:27043 tcp:27043 frida-ps -U
正常输出进程列表说明已配置完成,接下来可以用 python 的 frida 库,也可以用frida -U com.android.xxx -l payload.js
这样的方式进行注入
三进制战争
jadx 反编译,查看逻辑
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 public final boolean CHECK1 (String str1) { if (str1.length() <= 13 ) { return false ; } String substring = str1.substring(0 , 5 ); Intrinsics.checkNotNullExpressionValue(substring, "substring(...)" ); if (Intrinsics.areEqual(substring, "ISCC{" ) && str1.charAt(str1.length() - 1 ) == '}' ) { String stringFromJN1 = stringFromJN1(); String substring2 = str1.substring(11 , str1.length() - 1 ); Intrinsics.checkNotNullExpressionValue(substring2, "substring(...)" ); String stringFromJNl = stringFromJNl(stringFromJN1, substring2); String substring3 = str1.substring(5 , 11 ); Intrinsics.checkNotNullExpressionValue(substring3, "substring(...)" ); if (Intrinsics.areEqual(substring3, stringFromJNI("2vW&77" )) && CHECK2(stringFromJNl)) { return true ; } } return false ; } private final boolean CHECK2 (String str1) { return Intrinsics.areEqual(str1, "011102001221020101000020001220010221" ); } static { System.loadLibrary("mobile02" ); }
满足CHECK1
、CHECK2
即可,具体函数实现可在 native 层的libmobile02.so
找到
编写hook1.py
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 import fridadef on_message (message, data ): if message['type' ] == 'send' : print ("*****[frida hook]***** : {0}" .format (message['payload' ])) else : print ("*****[frida hook]***** : " + str (message)) jscode = """ Java.perform(function x() { Java.choose('com.example.mobile02.MainActivity', { onMatch: function (instance) { console.log(instance.stringFromJN1()); }, onComplete: function () { console.log('Done'); } }); }); """ a.get_usb_device(-1 ).attach('mobile02' ) process = frid script = process.create_script(jscode) script.on('message' , on_message) print ('[*] HOOK' )script.load()
输出如下
1 2 3 [*] HOOK c292ed6911b76747c8e620bda5c1f18a Done
stringFromJN1
为c292ed6911b76747c8e620bda5c1f18a
stringFromJNl = stringFromJNl(stringFromJN1, substring2)
,substring2
为 flag 后半部分
substring3
和stringFromJNI("2vW&77")
相等
编写hook2.py
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 import fridadef on_message (message, data ): if message['type' ] == 'send' : print ("*****[frida hook]***** : {0}" .format (message['payload' ])) else : print ("*****[frida hook]***** : " + str (message)) jscode = """ Java.perform(function x() { Java.choose('com.example.mobile02.MainActivity', { onMatch: function (instance) { console.log(instance.stringFromJNI("2vW&77")); }, onComplete: function () { console.log('Done'); } }); }); """ process = frida.get_usb_device(-1 ).attach('mobile02' ) script = process.create_script(jscode) script.on('message' , on_message) print ('[*] HOOK' )script.load()
结果为
那substring3
就是'sc2Rd%2S
,继续往下看知道stringFromJNl
等于011102001221020101000020001220010221
,简单 hook 一下调用stringFromJNl()
,观察发现 substring2 只有 6 个字符,写脚本逐字符爆破,再拼上substring3
就是完整 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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 function hook ( ) { Java .perform (function ( ) { const MainActivity = Java .use ("com.example.mobile02.MainActivity" ); const enc = "011102001221020101000020001220010221" ; MainActivity ["CHECK1" ].implementation = function (str1 ) { console .log (`MainActivity.CHECK1 is called: str1=${str1} ` ); let res = this ["CHECK1" ]("ISCC{6xY*08S4t(5U}" ); console .log (`MainActivity.CHECK1 res=${res} ` ); return res; }; MainActivity ["stringFromJNl" ].implementation = function (key, input ) { console .log ( `MainActivity.stringFromJNl is called: key=${key} , input=${input} ` ); let res = this ["stringFromJNl" ](key, input); console .log (`MainActivity.stringFromJNl res=${res} ` ); let correctInput = input.split ("" ); for (let pos = 0 ; pos < input.length ; pos++) { for (let charCode = 32 ; charCode <= 126 ; charCode++) { let char = String .fromCharCode (charCode); input = correctInput.join ("" ); let modifiedInput = input.substring (0 , pos) + char + input.substring (pos + 1 ); try { let testres = this ["stringFromJNl" ](key, modifiedInput); if (testres === enc) { console .log (modifiedInput); } const testIndex = 6 * pos, encIndex = 6 * pos + 6 ; if ( testIndex < testres.length && encIndex < enc.length ) { if ( testres.substring (testIndex, encIndex) === enc.substring (testIndex, encIndex) ) { console .log ( `Match at pos=${pos} , char='${char} '` ); correctInput[pos] = char; } } } catch (e) { console .log ( `Error with char '${char} ' at pos ${pos} : ${e} ` ); } } } return res; }; let Intrinsics = Java .use ("kotlin.jvm.internal.Intrinsics" ); Intrinsics ["areEqual" ].overload ( "java.lang.Object" , "java.lang.Object" ).implementation = function (obj, obj2 ) { console .log ( `Intrinsics.areEqual is called: obj=${obj} , obj2=${obj2} ` ); let res = this ["areEqual" ](obj, obj2); console .log (`Intrinsics.areEqual res=${res} ` ); return res; }; }); } setImmediate (hook);
爆破出来是P2q&3R
,然后会发现MainActivity.CHECK1
结果还是false
,原因是他本地 CHECK 有问题,检查的长度不对,直接交就行,flag 为ISCC{'sc2Rd%2SP2q&3R}
Encode
下载附件发现无法安装,改后缀为 zip 再拿 WinRAR 一键修复~~(又不是不行)~~
改回 apk 再安装,jadx 打开,关键部分如下
1 2 3 4 5 6 7 8 9 10 11 12 private native boolean nativeCheckFormat (String str) ;private native boolean nativeCheckLast (String str) ;static { System.loadLibrary("encode" ); } public boolean Jformat (String str) { int lastIndexOf = str.lastIndexOf("_" ); if (lastIndexOf == -1 || str.length() < 10 || !str.startsWith("ISCC{" ) || !str.endsWith("}" )) { return false ; } return nativeCheckLast(str.substring(lastIndexOf + 1 , str.length() - 1 )) && nativeCheckFormat(str); }
导出libencode.so
用 IDA 打开分析逻辑,里面有个简单 base64。在函数名列表查找关键字encode
Java_com_example_encode_MainActivity_nativeCheckFormat()
内可找到一个 128 位数据xmmword_14540
,右键改属性为 Data,解出来是bVpbW0pdTRwcXXAA
,还有一行数据LOBYTE(v3) = (*(_DWORD *)v9 ^ 0x75686F7D | *(_DWORD *)(v9 + 3) ^ 0x45437575) == 0;
可提取出十六进制数据\x7D\x6F\x68\x75\x75\x75\x43\x45
encode_front_part(front_part) == stru_14540
,查看encode_front_part()
,核心代码如下
1 2 for ( i = 0LL ; i != v3; ++i ) std ::string ::push_back(&v9, (char )v2[i] ^ 0x2F u);
可以看到每个字符异或了2f
,可以拿 CyberChef 解出前半 flag,多了一个/
是为了凑 3 字节而填充的数据
encode_last_part
对应后半,编码逻辑就是每个字符加 3 再对整个字符串反转,解密只需要反转再减 3,完整 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 import base64import structFRONT_CIPHER = "bVpbW0pdTRwcXXAA" CHECK1 = 0x75686F7D CHECK2 = 0x45437575 def decode_front (): raw = base64.b64decode(FRONT_CIPHER + "==" ) plain = bytes (b ^ 0x2F for b in raw) tmp = plain[:-2 ] return tmp.decode() def decode_last (): N = 7 arr1 = struct.pack("<I" , CHECK1) arr2 = struct.pack("<I" , CHECK2) arr = bytearray (N) arr[0 :4 ] = arr1 arr[3 :7 ] = arr2[:4 ] suffix_chars = [chr ((arr[N - 1 - i] - 3 ) & 0xFF ) for i in range (N)] return "" .join(suffix_chars) def encode_last (s: str ) -> bytes : arr = [(ord (c) + 3 ) & 0xFF for c in s] arr.reverse() return bytes (arr) if __name__ == "__main__" : front = decode_front() last = decode_last() flag = f"ISCC{{{front} _{last} }}" print ("front part:" , front) print ("last part :" , last) print ("flag :" , flag)
输出
1 2 3 front part: Butterb33r last part : B@rrelz flag : ISCC{Butterb33r_B@rrelz}
睡蕉小猴
打的时候没真机,模拟器奇奇怪怪的很多问题
jadx 反编译查看源代码,模拟器安装 apk 打开是一段重复动画
在类 p000.Shui18
下有个函数getFlag()
,对ENCRYPTED_FLAG
直接 base64 解码得前半 flag
继续翻看资源文件,发现一个可疑的res/raw/key18.txt
MainActivity
下检查 flag 的函数为CHECK()
,满足需求即为正确 flag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private boolean check (String str) { if (str.length() < 13 || !str.substring(0 , 5 ).equals("ISCC{" ) || !str.substring(str.length() - 1 ).equals("}" )) { return false ; } return Jformat(str.substring(11 , str.length() - 1 ), getSnowkey()); } private boolean Jformat (String str, String str2) { try { return ((Boolean) new DexClassLoader (getDir("dex" , 0 ).getAbsolutePath() + "/fox" , getCodeCacheDir().getAbsolutePath(), null , getClassLoader()).loadClass("com.example.mobile03.fox" ).getMethod("isFlag" , Context.class, String.class).invoke(null , this , str)).booleanValue(); } catch (Exception e) { e.printStackTrace(); return false ; } }
首先 hook 一下getSnowkey()
拿到 key
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 import fridadef on_message (message, data ): if message['type' ] == 'send' : print ("*****[frida hook]***** : {0}" .format (message['payload' ])) else : print ("*****[frida hook]***** : " + str (message)) jscode = """ Java.perform(function x() { Java.choose('com.example.mobile03.MainActivity', { onMatch: function (instance) { console.log(instance.getSnowkey()); }, onComplete: function () { console.log('Done'); } }); }); """ process = frida.get_usb_device(-1 ).attach('mobile03' ) script = process.create_script(jscode) script.on('message' , on_message) print ('[*] HOOK' )script.load()
key 为342150
。再审计Jformat()
,调用了类com.example.mobile03.Fox18
里的方法isFlag()
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 public class Fox18 { private static final String TARGET = "42f793efdf4dbfaf82556b9a562473a84a9806a81267b8b47c5e06308fc765ba825818e2ccf2793477b75805be6c45e8ee4df9849516ed0af1a4eebebd497fb8" ; public static boolean isFlag (Context context, String str) { try { PublicKey publicKey = (PublicKey) new DexClassLoader (context.getDir("dex" , 0 ).getAbsolutePath() + "/rabbit" , context.getCodeCacheDir().getAbsolutePath(), null , Fox18.class.getClassLoader()).loadClass("com.example.mobile03.rabbit" ).getMethod("getPublicKey" , Context.class).invoke(null , context); Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding" ); cipher.init(1 , publicKey); return TARGET.equalsIgnoreCase(bytesToHex(cipher.doFinal(str.getBytes()))); } catch (Exception e) { e.printStackTrace(); return false ; } } public static String getPrivateKey (Context context) { try { return (String) new DexClassLoader (context.getDir("dex" , 0 ).getAbsolutePath() + "/rabbit" , context.getCodeCacheDir().getAbsolutePath(), null , Fox18.class.getClassLoader()).loadClass("com.example.mobile03.rabbit" ).getMethod("getPrivateKey" , Context.class).invoke(null , context); } catch (Exception e) { e.printStackTrace(); return null ; } } private static String bytesToHex (byte [] bArr) { StringBuilder sb = new StringBuilder (); for (byte b : bArr) { String hexString = Integer.toHexString(b & UByte.MAX_VALUE); if (hexString.length() == 1 ) { sb.append('0' ); } sb.append(hexString); } return sb.toString().toLowerCase(); } }
一路追踪 RSA 私钥fox->rabbit->pig-swan
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 public class swan { private static native String genPvKey (String str) ; private static native String getPvKey () ; static { System.loadLibrary("swan" ); } public static String getPrivateKey () { if (m55a("1235" ) == 1 ) { return getPvKey(); } return genPvKey(m56b()); } public static int m55a (String str) { if (str.length() % 2 == 0 ) { return 1 ; } return 0 ; } public static String m56b () { return "demo" ; } }
导出libswan.so
,IDA 打开并分析,需要调用getPvKey()
得到私钥数据
翻看资源文件,在res/raw/key18.txt
可以找到提示enkeyishere
,下方有空白字符隐写,尝试了一下不是whitespace
语言,那大概率就是 snow 隐写。前面已经通过getSnowkey()
拿到密码,直接解密
1 .\SNOW.EXE -p 342150 key18.txt > out
后续未复现,但大致思路是调用getPvKey后解出私钥,获取flag
Pwn
连上环境已是拼尽全力 😭
第一题看了似乎是泄露 canary+libc
另外第一题没给 libc
擂台
Misc
蛇壳下的秘密
下载附件,是个使用 pyinstaller 打包的贪吃蛇小游戏,先pyinstxtractor
解包得到50个附件生成.pyc
拿本地的uncompyle6
无法反编译这个 pyc,拿 pycdc 也不行,于是先玩贪吃蛇游戏,途中出现了两次弹窗,一次提示继续玩,一次提示行百里者半九十,一直玩到 300 多分不再出现提示
发现同目录下生成了game_log.zip
,解压需要密码
直接使用 010Editor 打开 pyc 分析可读部分,发现在game_log
字样前有个Welcome
,测试了一下正是压缩包密码,解压得到game_log.txt
里面有两个可疑字符串
1 2 ISCC{eWVhcgo=} ISCC{U2FsdGVkX1+L/wKmHIDfApCg80p+D+QrET/NmTD7QNeRSGbAkJFM}
推测是寻找密码解后面的密文,eWVhcgo=
解 base64 得到year
,联想目前出现的字符串和 Hint,排列组合各种密码和加密方式,最后发现是 RC4.密码为serpentyearISCC
flag 为
Mobile
whereisflag
可找到密文iB3A7kSISR
,替换表直接上 frida
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function hook ( ) { Java .perform (function ( ) { let a = Java .use ("com.example.whereisflag.a" ); a["compute" ].implementation = function (str ) { console .log (`a.compute is called: str=${str} ` ); let result = this ["compute" ]( "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" ); console .log (`a.compute result=${result} ` ); return result; }; }); } setImmediate (hook);
得到替换表后解密
1 2 3 4 5 from string import printableget="baZEYXVLIUTQPOGNMFRCAeKJDB10zyxwvutsrqponmlkjihSgfdcHW98765432" [::-1 ] enc="iB3A7kSISR" for i in enc[::-1 ]: print (printable[get.index(i)],end="" )
得到 flag