本文最后更新于 2025年2月10日 上午
VNCTF2025 Writeup Reverse 写在最前面 “但是太阳,它每时每刻都是夕阳也都是旭日。当它熄灭着走下山去收尽苍凉残照之际,正是它在另一面燃烧着爬上山巅布散烈烈朝辉之时。” 好久没有更新博客了,在不断的被否定与碰壁中艰难行走,VNCTF2025交出了自己接触CTF以来的最完美的答卷,虽然没有AK,但赛后也努力解出了。感谢一路上给予我指点的师傅和陪伴我的朋友们,我相信我会越来越好。
Just reverse it!
fish_hook jadx看逻辑
提示联网再钓鱼,又有个download hook_fish.dex,字符串搜一下
搜到一个地址http://47.121.211.23/hook_fish.dex
下载hook_fish.dex
继续审main的逻辑
先对每个字符+68,再转成hex, 再进行递归的相邻换位(index:0<——>1,2<——>3这样换),
异或换位相当于:
1 2 3 4 5 6 7 8 9 10 11 void code (char *a, int index, int len) { if (index >= len - 1 ) { return ; } char temp = a[index]; a[index] = a[index + 1 ]; a[index + 1 ] = temp; code(a, index + 2 , len); }
再进行一个加密
1 2 3 4 5 6 7 8 9 10 11 12 for (int i2 = 0 ; i2 < str3.length; i2++) { if (str3[i2] >= 'a' && str3[i2] <= 'f' ) { str3[i2] = (char ) ((str3[i2] - '1' ) + (i2 % 4 )); } else { str3[i2] = (char ) (str3[i2] + '7' + (i2 % 10 )); } }
最后进了fish_hook.hex
jadx分析fish_hook.hex
给了一个码表然后对应转化成
1 jjjliijijjjjjijiiiiijijiijjiijijjjiiiiijjjjliiijijjjjljjiilijijiiiiiljiijjiiliiiiiiiiiiiljiijijiliiiijjijijjijijijijiilijiijiiiiiijiljijiilijijiiiijjljjjljiliiijjjijiiiljijjijiiiiiiijjliiiljjijiiiliiiiiiljjiijiijiijijijjiijjiijjjijjjljiliiijijiiiijjliijiijiiliiliiiiiiljiijjiiliiijjjliiijjljjiijiiiijiijjiijijjjiiliiliiijiijijijiijijiiijjjiijjijiiiljiijiijilji
思路是先查表还原对应回去,然后爆破一下加密,然后换位回去之后转回字符再-68即可
exp如下:
fish1.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class FishDecoder : def __init__ (self ): self.fish_dcode = { "iiijj" : 'a' , "jjjii" : 'b' , "jijij" : 'c' , "jjijj" : 'd' , "jjjjj" : 'e' , "ijjjj" : 'f' , "jjjji" : 'g' , "iijii" : 'h' , "ijiji" : 'i' , "iiiji" : 'j' , "jjjij" : 'k' , "jijji" : 'l' , "ijiij" : 'm' , "iijji" : 'n' , "ijjij" : 'o' , "jiiji" : 'p' , "ijijj" : 'q' , "jijii" : 'r' , "iiiii" : 's' , "jjiij" : 't' , "ijjji" : 'u' , "jiiij" : 'v' , "iiiij" : 'w' , "iijij" : 'x' , "jjiji" : 'y' , "jijjj" : 'z' , "iijjl" : '1' , "iiilj" : '2' , "iliii" : '3' , "jiili" : '4' , "jilji" : '5' , "iliji" : '6' , "jjjlj" : '7' , "ijljj" : '8' , "iljji" : '9' , "jjjli" : '0' } def decode (self, cipher_text ): chunks = [cipher_text[i:i+5 ] for i in range (0 , len (cipher_text), 5 )] return '' .join(self.fish_dcode.get(chunk, '?' ) for chunk in chunks) decoder = FishDecoder() cipher_text = "jjjliijijjjjjijiiiiijijiijjiijijjjiiiiijjjjliiijijjjjljjiilijijiiiiiljiijjiiliiiiiiiiiiiljiijijiliiiijjijijjijijijijiilijiijiiiiiijiljijiilijijiiiijjljjjljiliiijjjijiiiljijjijiiiiiiijjliiiljjijiiiliiiiiiljjiijiijiijijijjiijjiijjjijjjljiliiijijiiiijjliijiijiiliiliiiiiiljiijjiiliiijjjliiijjljjiijiiiijiijjiijijjjiiliiliiijiijijijiijijiiijjjiijjijiiiljiijiijilji" plain_text = decoder.decode(cipher_text)print (plain_text)
得到:0qksrtuw0x74r2n3s2x3ooi4ps54r173k2os12r32pmqnu73r1h432n301twnq43prruo2h5
fish2.c(可见字符范围内爆破)
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 #include <stdio.h> int main () { char s[72 ]; char t[]="0qksrtuw0x74r2n3s2x3ooi4ps54r173k2os12r32pmqnu73r1h432n301twnq43prruo2h5" ; int p; for (int i=0 ;i<72 ;i++) { for (int b=40 ;b<=128 ;b++) { if (b >= 'a' && b <= 'f' ) p= (b-'1' ) + (i% 4 ); else p= (b+'7' )+ (i% 10 ); if (p==t[i]) { s[i]=b; break ; } } } for (int i =71 ; i > 0 ; i -= 2 ) { char temp = s[i]; s[i] = s[i-1 ]; s[i-1 ]=temp; } for (int i =0 ;i<72 ;i++) { printf ("%c" , s[i]); } return 0 ; }
得到:9a9287988abfb9a3b6a978b075bda3afb274bba38c7493afa3b1bda3aa7597ac6575b0c1
fish3.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include<stdio. h>int main() { int d[]={ 0x9a , 0x92 , 0x87 , 0x98 , 0x8a , 0xbf , 0xb9 , 0xa3 , 0xb6 , 0xa9 , 0x78 , 0xb0 , 0x75 , 0xbd , 0xa3 , 0xaf , 0xb2 , 0x74 , 0xbb , 0xa3 , 0x8c , 0x74 , 0x93 , 0xaf , 0xa3 , 0xb1 , 0xbd , 0xa3 , 0xaa , 0x75 , 0x97 , 0xac , 0x65 , 0x75 , 0xb0 , 0xc1 } for(int i=0 { d[i]-=68 printf("%c" ,d[i]) } return 0 }
拆开之后-68运行即可
得到flag:
1 VNCTF {u_re4l1y_kn0w_H0Ok_my_f1Sh!1 l}
Fuko’s starfish 一堆反调试去不干净,没办法进行动调,硬着头皮运行程序
先猜数,二分直接过掉
然后是个贪吃蛇,玩不过去
ida找到贪吃蛇的逻辑发现是score达到23过关
直接patch一下改到0
这样就直接过了第二关,看到如下提示
去审贪吃蛇胜利之后的逻辑看到函数sub_1800025F0
直接反编译会出问题,有call $5指令,全部nop
前面有字符串输出,是加密字符串后(xor 0x17)再输出的
赛博厨子证实了猜测
审后面的加密逻辑,发现是AES
但是这里有个反调试,检测到调试器的时候直接用key,没检测到的时候异或0x17
动调不起来,但是key的值又缺四个字节的,查看交叉引用发现一个可疑的rand函数,但是显示出的几个不是密钥
往下追nop掉一个retn,发现了srand和真正的rand
直接抄出来跑一下异或0x17得到密钥
exp如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <stdio.h> #include <stdlib.h> #include <stdint.h> int main () { srand(0x1BF52 u); uint8_t a[16 ]; for (int i=0 ;i<16 ;i++) { int r=rand(); a[i]=(r+r/255 )^0x17 ; } for (int i=0 ;i<16 ;i++) printf ("%02X" ,a[i]); return 0 ; }
运行得到key:09E5FDEB683175B6B13B840891EB78D2
算法没有魔改,赛博厨子一把梭
得到flag:
1 VNCTF{W0w_u_g0t_Fuk0's_st4rf1sh }
抽奖转盘 人生第一次完成鸿蒙逆向,略显兴奋的同时记录一下解题过程
先去看了so
一个base64,一个魔改的rc4,最后多xor了40,key是Take_it_easy
jadx-dev-all.jar反编译modules.abc,看到了承接MyCry的地方
加密过程有了开始翻密文(翻了很久)
找到了密文,发现密文里明显不是base64的可见字符,推测java层有加密
继续审java层逻辑发现了一个对于密文的小加密,+1再xor7
解密流程是先解Java层,然后解base64,然后解魔改rc4
homo.c
1 2 3 4 5 6 7 8 9 10 11 12 #include <stdio.h> int main () { int a[]={101 , 74 , 76 , 49 , 101 , 76 , 117 , 87 , 55 , 69 , 118 , 68 , 118 , 69 , 55 , 67 , 61 , 83 , 62 , 111 , 81 , 77 , 115 , 101 , 53 , 73 , 83 , 66 , 68 , 114 , 109 , 108 , 75 , 66 , 97 , 117 , 93 , 127 , 115 , 124 , 109 , 82 , 93 , 115 }; for (int i=0 ;i<44 ;i++) { a[i]^=7 ; a[i]-=1 ; printf ("%c" ,a[i]); } return 0 ; }
解得:aLJ5aJqO/ApBpA/C9S8gUIsa1MSDBtijKDeqYwsziTYs
解base64有:68b279689a8efc0a41a40fc2f52f20508b1ad4c48306d8a32837aa630b3389362c
最后解rc4
homo.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 27 28 29 30 31 32 def initialize_key_schedule (key ): key_length = len (key) sbox = list (range (256 )) j = 0 for i in range (256 ): j = (j + sbox[i] + key[i % key_length]) % 256 sbox[i], sbox[j] = sbox[j], sbox[i] return sboxdef generate_keystream (sbox ): i = 0 j = 0 while True : i = (i + 1 ) % 256 j = (j + sbox[i]) % 256 sbox[i], sbox[j] = sbox[j], sbox[i] keystream_value = sbox[(sbox[i] + sbox[j]) % 256 ] yield keystream_value key = b'Take_it_easy' ciphertext = bytes .fromhex('68b279689a8efc0a41a40fc2f52f20508b1ad4c48306d8a32837aa630b3389362c' ) sbox = initialize_key_schedule(key) keystream_generator = generate_keystream(sbox) plaintext = "" .join(chr (byte ^0x28 ^next (keystream_generator)) for byte in ciphertext)print (plaintext)
解得YQFWI~MXvwb’qhbLdvwbgdqfhb5358$,不太对的样子,猜测还有没看到的加密,由flag头VNCTF猜要减3,尝试一下
1 2 3 4 5 6 7 8 9 10 11 #include <stdio.h> int main () { char a[]="YQFWI~MXvwb'qhbLdvwbgdqfhb5358$" ; for (int i=0 ;i<strlen (a);i++) { a[i]-=3 ; printf ("%c" ,a[i]); } return 0 ; }
解得:VNCTF{JUst_$ne_Iast_dance_2025!
故最后得到flag:
1 VNCTF{JUst_$ne_Iast_dance_2025!}
kotlindroid 很奇怪的非常规题,最后才出的
jadx看逻辑
翻了很久不断交叉引用找到了加密逻辑:AES/GCM/NoPadding
iv是114514,密文是MTE0NTE0HMuJKLOW1BqCAi2MxpHYjGjpPq82XXQ/jgx5WYrZ2MV53a9xjQVbRaVdRiXFrSn6EcQPzA==,缺少add和key
然后尝试frida hook
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Java .perform (() => { const JNI = Java .use ("com.atri.ezcompose.JNI" ); JNI .getAt .implementation = function ( ) { console .log ('[+] JNI.getAt() called' ); const result = this .getAt (); console .log (`[*] JNI.getAt() returned: ${result} ` ); return result; }; const SearchActivityKt = Java .use ("com.atri.ezcompose.SearchActivityKt" ); SearchActivityKt .check .implementation = function (text, context, key ) { console .log ('[+] check() called' ); console .log (` Text: ${text} ` ); console .log (` Context: ${context} ` ); console .log (` Key: ${key} ` ); const isValid = this .check (text, context, key); console .log (`[*] check() result: ${isValid} ` ); return isValid; }; });
hook结果
1 2 3 4 5 6 7 8 9 [+] check () called Text: hhh Context: com.atri .ezcompose .SearchActivity@af9c1c8 Key: 97 ,116 ,114 ,105 ,107 ,101 ,121 ,115 ,115 ,121 ,101 ,107 ,105 ,114 ,116 ,97 [*] check () result: undefined[+] generateIV () called[*] generateIV () returned: 49 ,49 ,52 ,53 ,49 ,52 [+] JNI.getAt () called[*] JNI.getAt () returned: mysecretadd
hook得到key是atrikeyssyekirta
add是mysecretadd
这里有点小坑,密文解完base64前6位是iv,后面才是真正的密文
真正的密文是1ccb8928b396d41a82022d8cc691d88c68e93eaf365d743f8e0c79598ad9d8c579ddaf718d055b45a55d4625c5ad29fa11c40fcc(16进制)
赛博厨子梭了
得到flag:
1 VNCTF{Y0U_@ re_th3_Ma5t3r_0f_C0mp0s3}
AndroidLux(赛后做出) jadx审逻辑
flag验证
起了一个socket环境
把/assets下的busybox和env复制到指定路径,env其实是个tar.xz,并解压
没有看到有加载native so,所以去/assets目录下复制出来env并解压
1 2 mv env env.tar.xz tar -xJvf env.tar.xz
然后得到一大堆文件
翻了半天无果,然后有小提示,用Everything去看文件的修改时间,有
有一个env的elf文件,然后还有一个libexec.so比较可疑
先分析env
有个魔改且变表的base64
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 v16 = 0 ; if ( len % 3 ) v3 = 4 ; else v3 = 0 ; v13 = malloc (4 * (len / 3 ) + 1 + v3); for ( i1 = 0 ; i1 < len; ++i1 ) { if ( len - i1 <= 2 ) { v13[v16] = base64[input[i1] >> 2 ]; if ( len - i1 == 2 ) { v7 = input[i1++] & 3 ; v13[v16 + 1 ] = base64[v7 | (input[i1] >> 2 ) & 0x3C ]; v13[v16 + 2 ] = base64[input[i1] & 0xF ]; } else { v13[v16 + 1 ] = base64[input[i1] & 3 ]; v13[v16 + 2 ] = 61 ; } v8 = v16 + 3 ; v16 += 4 ; v13[v8] = 61 ; } else { v13[v16] = base64[input[i1] >> 2 ]; v4 = input[i1] & 3 ; i2 = i1 + 1 ; v13[v16 + 1 ] = base64[v4 | (input[i2] >> 2 ) & 0x3C ]; v5 = input[i2] & 0xF ; i1 = i2 + 1 ; v13[v16 + 2 ] = base64[v5 | (16 * (input[i1] >> 6 ))]; v6 = v16 + 3 ; v16 += 4 ; v13[v6] = base64[input[i1] & 0x3F ]; } } v13[v16] = 0 ; result = a3; *a3 = v13; return result;
然后去浏览libexec.so
发现hook了read和strncmp
read里面多了一个xor 1
strncmp里面多了一个ROT13
所以最后解密的顺序是先ROT13,再解魔改base64,最后xor 1
rot13.c
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 #include <stdio.h> #include <stdlib.h> #include <string.h> char *rot13_decrypt (const char *input) { int len = strlen (input); char *output = malloc (len + 1 ); if (output == NULL ) { return NULL ; } for (int i = 0 ; i < len; i++) { char c = input[i]; if (c >= 'A' && c <= 'M' ) { output[i] = c + 13 ; } else if (c >= 'N' && c <= 'Z' ) { output[i] = c - 13 ; } else if (c >= 'a' && c <= 'm' ) { output[i] = c + 13 ; } else if (c >= 'n' && c <= 'z' ) { output[i] = c - 13 ; } else { output[i] = c; } } output[len] = '\0' ; return output; }int main (void ) { const char *encrypted = "RPVIRN40R9PU67ue6RUH88Rgs65Bp8td8VQm4SPAT8Kj97QgVG==" ; char *decrypted = rot13_decrypt(encrypted); if (decrypted) { printf ("%s\n" , decrypted); free (decrypted); } else { fprintf (stderr , "error\n" ); } return 0 ; }
得到:ECIVEA40E9CH67hr6EHU88Etf65Oc8gq8IDz4FCNG8Xw97DtIT==
b64.c
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 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <ctype.h> static int base64_rev[256 ] = {0 };void init_base64_rev (void ) { int i; for (i = 0 ; i < 256 ; i++) { base64_rev[i] = -1 ; } const char *alphabet = "TUVWXYZabcdefghijABCDEF456789GHIJKLMNOPQRSklmnopqrstuvwxyz0123+/" ; for (i = 0 ; i < 64 ; i++) { base64_rev[(unsigned char )alphabet[i]] = i; } }unsigned char *decode_custom_base64 (const char *encoded, int *out_len) { int encoded_len, i, pad = 0 ; unsigned char *decoded; int decoded_len; if (!encoded) return NULL ; encoded_len = strlen (encoded); if (encoded_len % 4 != 0 ) { fprintf (stderr, "length error\n" ); return NULL ; } if (encoded_len >= 1 && encoded[encoded_len-1 ] == '=' ) pad++; if (encoded_len >= 2 && encoded[encoded_len-2 ] == '=' ) pad++; decoded_len = (encoded_len / 4 ) * 3 - pad; decoded = malloc (decoded_len); if (!decoded) return NULL ; int decoded_index = 0 ; for (i = 0 ; i < encoded_len; i += 4 ) { int d1, d2, d3, d4; char ch1 = encoded[i], ch2 = encoded[i+1 ], ch3 = encoded[i+2 ], ch4 = encoded[i+3 ]; d1 = (ch1 == '=' ) ? 0 : base64_rev[(unsigned char )ch1]; d2 = (ch2 == '=' ) ? 0 : base64_rev[(unsigned char )ch2]; d3 = (ch3 == '=' ) ? 0 : base64_rev[(unsigned char )ch3]; d4 = (ch4 == '=' ) ? 0 : base64_rev[(unsigned char )ch4]; if ((ch1 != '=' && d1 < 0 ) || (ch2 != '=' && d2 < 0 ) || (ch3 != '=' && d3 < 0 ) || (ch4 != '=' && d4 < 0 )) { fprintf (stderr, "NO!\n" ); free (decoded); return NULL ; } if (ch3 == '=' ) { unsigned char a = (unsigned char )((d1 << 2 ) | d2); decoded[decoded_index++] = a; } else if (ch4 == '=' ) { unsigned char a = (unsigned char )((d1 << 2 ) | (d2 & 3 )); unsigned char b = (unsigned char )(((d2 >> 2 ) << 4 ) | d3); decoded[decoded_index++] = a; if (decoded_index < decoded_len) decoded[decoded_index++] = b; } else { unsigned char a = (unsigned char )((d1 << 2 ) | (d2 & 3 )); unsigned char b = (unsigned char )(((d2 >> 2 ) << 4 ) | (d3 & 0xF )); unsigned char c = (unsigned char )(((d3 >> 4 ) << 6 ) | d4); decoded[decoded_index++] = a; if (decoded_index < decoded_len) decoded[decoded_index++] = b; if (decoded_index < decoded_len) decoded[decoded_index++] = c; } } if (out_len) *out_len = decoded_len; return decoded; }int main (void ) { init_base64_rev (); const char *encoded_str = "ECIVEA40E9CH67hr6EHU88Etf65Oc8gq8IDz4FCNG8Xw97DtIT==" ; int decoded_len; unsigned char *decoded = decode_custom_base64 (encoded_str, &decoded_len); if (!decoded) { fprintf (stderr, "error\n" ); return 1 ; } for (int i = 0 ; i < decoded_len; i++) { printf ("%c" , decoded[i]^1 ); } printf ("\n" ); free (decoded); return 0 ; }
得到flag:
1 VNCTF{Ur_go0d_@ndr0id&l1nux_Reve7ser}