【CTF对抗-2025 HGAME WEEK1 RE WP】此文章归类为:CTF对抗。
过完年了也开学了,该玩的也都玩了,在盛大的欢乐之后就是无尽的空虚。收收心回归老本行,两周没有做题了还好宝刀未老,花了两天的时间ak了week1的逆向题目,个人不太满意,还是有点慢。
这道题意外的卡了很久,做的时候猜测是哈夫曼编码了,但是当时不太符合我心中的哈夫曼预期,后面专门看了下哈夫曼算法然后结合代码发现匹配,这才确定是。。。。忘的太快了,想当年也是408考119的男人。
题目很新颖,给的是nu脚本文件,还有个加密文件,通过理解脚本文件的加密逻辑,解密文件
这里因为我哈夫曼算法记忘了,在这里卡了很久,结合chatgpt和加入输出语句“调试”的情况下,得出是哈夫曼算法。
加密脚本由两部分组成,第一行为加密内容字符哈夫曼树,第二行则是根据第一行哈夫曼树的生成的字符转义。
写脚本求解:
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 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 | # 假设我们已经有了哈夫曼树 def decode_huffman_tree(huffman_tree, binary_path): flag = "" node = huffman_tree for bit in binary_path: if "s" in node: flag + = chr (node[ 's' ]) node = huffman_tree if bit = = '0' : node = node[ 'a' ] elif bit = = '1' : node = node[ 'b' ] return flag # 示例的路径和哈夫曼树(简化版本) huffman_tree = { "a" : { "a" : { "a" : { "a" : { "a" : { "s" : 125 }, "b" : { "a" : { "s" : 119 }, "b" : { "s" : 123 } } }, "b" : { "a" : { "s" : 104 }, "b" : { "s" : 105 } } }, "b" : { "a" : { "s" : 101 }, "b" : { "s" : 103 } } }, "b" : { "a" : { "a" : { "a" : { "s" : 10 }, "b" : { "s" : 13 } }, "b" : { "s" : 32 } }, "b" : { "a" : { "s" : 115 }, "b" : { "s" : 116 } } } }, "b" : { "a" : { "a" : { "a" : { "a" : { "a" : { "s" : 46 }, "b" : { "s" : 48 } }, "b" : { "a" : { "a" : { "s" : 76 }, "b" : { "s" : 78 } }, "b" : { "a" : { "s" : 83 }, "b" : { "a" : { "s" : 68 }, "b" : { "s" : 69 } } } } }, "b" : { "a" : { "a" : { "s" : 44 }, "b" : { "a" : { "s" : 33 }, "b" : { "s" : 38 } } }, "b" : { "s" : 45 } } }, "b" : { "a" : { "a" : { "s" : 100 }, "b" : { "a" : { "s" : 98 }, "b" : { "s" : 99 } } }, "b" : { "a" : { "a" : { "s" : 49 }, "b" : { "s" : 51 } }, "b" : { "s" : 97 } } } }, "b" : { "a" : { "a" : { "a" : { "s" : 117 }, "b" : { "s" : 118 } }, "b" : { "a" : { "a" : { "s" : 112 }, "b" : { "s" : 113 } }, "b" : { "s" : 114 } } }, "b" : { "a" : { "a" : { "s" : 108 }, "b" : { "s" : 109 } }, "b" : { "a" : { "s" : 110 }, "b" : { "s" : 111 } } } } } } # 示例路径 binary_path = '00010001110111111010010000011100010111000100111000110000100010111001110010011011010101111011101100110100011101101001110111110111011011001110110011110011110110111011101101011001111011001111000111001101111000011001100001011011101100011100101001110010111001111000011000101001010000000100101000100010011111110110010111010101000111101000110110001110101011010011111111001111111011010101100001101110101101111110100100111100100010110101111111111100110001010101101110010011111000110110101101111010000011110100000110110101011000111111000110101001011100000110111100000010010100010001011100011100111001011101011111000101010110101111000001100111100011100101110101111100010110101110000010100000010110001111011100011101111110101010010011101011100100011110010010110111101110111010111110110001111010101110010001011100100101110001011010100001110101000101111010100110001110101011101100011011011000011010000001011000111011111111100010101011100000' # 假设这是一个从根到叶子的路径 # 解码 decoded_char = decode_huffman_tree(huffman_tree, binary_path) print (decoded_char) # 输出 '}' |
之前做的题目都是找工具脱壳,第一次遇到手动脱壳的,也当练练手,好在不难
题目说明的暗示也给够了,也是一道考脱壳的题。脱壳之后的逻辑一目了然
放入DIE查一下壳,发现是UPX 3.91 modified,使用ida打开查看节区信息,发现是.add0, .add1等很奇怪的名字,也能猜个差不多是个魔改的加壳
接下来就是脱壳,不过我并没有发现可以使用esp定律定位oep的地方。不过我发现程序运行起来有个io,在程序等待输入的时候回到x64dbg中,发现代码已经脱壳完毕,此时正是一个很好的demp时机,使用插件一键dump,完成脱壳
将脱完壳的程序放入ida分析,程序很简单,两段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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | from Crypto.Cipher.ARC4 import * enc_key = bytes([ 0xCD , 0x8F , 0x25 , 0x3D , 0xE1 , ]) enc_key + = b 'QJ' cipher = new(b 'yekyek' ) dec_key = cipher.decrypt(enc_key) print (dec_key) #ecg4ab6 enc_flag = [ 0 for i in range ( 40 )] v2 = enc_flag v2[ 0 ] = 0xF8 v2[ 1 ] = - 43 v2[ 2 ] = 98 v2[ 3 ] = - 49 v2[ 4 ] = 67 v2[ 5 ] = - 70 v2[ 6 ] = - 62 v2[ 7 ] = 35 v2[ 8 ] = 21 v2[ 9 ] = 74 v2[ 10 ] = 81 v2[ 11 ] = 16 v2[ 12 ] = 39 v2[ 13 ] = 16 v2[ 14 ] = - 79 v2[ 15 ] = - 49 v2[ 16 ] = - 60 v2[ 17 ] = 9 v2[ 18 ] = - 2 v2[ 19 ] = - 29 v2[ 20 ] = - 97 v2[ 21 ] = 73 v2[ 22 ] = - 121 v2[ 23 ] = - 22 v2[ 24 ] = 89 v2[ 25 ] = - 62 v2[ 26 ] = 7 v2[ 27 ] = 59 v2[ 28 ] = - 87 v2[ 29 ] = 17 v2[ 30 ] = - 63 v2[ 31 ] = - 68 v2[ 32 ] = - 3 v2[ 33 ] = 75 v2[ 34 ] = 87 v2[ 35 ] = - 60 v2[ 36 ] = 126 v2[ 37 ] = - 48 v2[ 38 ] = - 86 v2[ 39 ] = 10 add_bytes = new(b 'ecg4ab6' ).encrypt(b '\x00' * 40 ) flag = '' for i in range ( 40 ): flag + = chr ((enc_flag[i] + add_bytes[i]) & 0xff ) print (flag) |
这道题也很新,或者说,我没做过,完全就是现挂(看今年春晚学的词)
一道考察Delta的题目,通过应用Detla还原出文件,但是Detla的hash被修改了,需要找到正确的hash,才能还原flag。
求解本题的关键就是找到正确的hash,在寻找hash的时候废了很多时间,网上的文档也有限,最后无奈尝试土办法,在hash的内存起始处下入内存断点,然后成功在memcmp中定位到了真正的hash,我这是正确的解法吗??? 感觉我这样做出来有点简单。
恢复出来hash,后面的加密很简单,循环xor
写脚本求解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | enc_flag = bytes([ 0x3B , 0x02 , 0x17 , 0x08 , 0x0B , 0x5B , 0x4A , 0x52 , 0x4D , 0x11 , 0x11 , 0x4B , 0x5C , 0x43 , 0x0A , 0x13 , 0x54 , 0x12 , 0x46 , 0x44 , 0x53 , 0x59 , 0x41 , 0x11 , 0x0C , 0x18 , 0x17 , 0x37 , 0x30 , 0x48 , 0x15 , 0x07 , 0x5A , 0x46 , 0x15 , 0x54 , 0x1B , 0x10 , 0x43 , 0x40 , 0x5F , 0x45 , 0x5A ]) xor_value = bytes([ 0x53 , 0x65 , 0x76 , 0x65 , 0x6E , 0x20 , 0x73 , 0x61 , 0x79 , 0x73 , 0x20 , 0x79 , 0x6F , 0x75 , 0x27 , 0x72 , 0x65 , 0x20 , 0x72 , 0x69 , 0x67 , 0x68 , 0x74 , 0x21 , 0x21 , 0x21 , 0x21 , 0x00 ]) flag = '' for i in range ( 43 ): flag + = chr (enc_flag[i] ^ xor_value[i % 28 ]) print (flag) |
这道也很有意思,题目也很新颖(我没做过)
给你一个apk,你就逆吧
这道题看了比较久,一直没找到关键逻辑,因为一开始我以为copyDexFromAssets是官方实现就没管,后面一看是藏东西了。现在长教训了,应当平等的怀疑每一个native函数
查看toast的反编译代码,
1 2 3 4 5 | public void setText(CharSequence s) { super .setText(s); String s = (String)DexCall.callDexMethod( this .mycontext, this .mycontext.getString(string.dex), this .mycontext.getString(string.classname), this .mycontext.getString(string.func1), s); toast.check( this .mycontext, s); } |
可以发现这里调用了 zunjia.dex 中的 encode函数
但是题目中的zunjia.dex 并没有encode函数,这让我不得不怀疑这是生成的新的dex,这个新的dex由encode函数。
写个脚本获取这个dex文件,使用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 25 26 27 28 29 30 31 32 33 | Java.perform( function () { var DexCall = Java.use( "com.nobody.zunjia.DexCall" ); var File = Java.use( "java.io.File" ); var FileInputStream = Java.use( "java.io.FileInputStream" ); var FileOutputStream = Java.use( "java.io.FileOutputStream" ); DexCall.copyDexFromAssets.implementation = function (ctx, dexName, dexDir) { var result = this .copyDexFromAssets(ctx, dexName, dexDir); var dexPath = result.getAbsolutePath(); console.log( "[*] Dex Path: " + dexPath); // 目标路径 /sdcard/zunjia.dex var outFile = File.$ new ( "/sdcard/zunjia.dex" ); // 读取原 dex 文件 var fis = FileInputStream.$ new (result); var fos = FileOutputStream.$ new (outFile); var buffer = Java.array( 'byte' , new Array(4096)); var bytesRead; while ((bytesRead = fis.read(buffer)) !== -1) { fos.write(buffer, 0, bytesRead); } fis.close(); fos.close(); console.log( "[*] Dex file copied to /sdcard/zunjia.dex" ); return result; }; }); |
这里我本意是保存到 /sdcard 下,但是程序以为权限问题崩溃没有保存到,但是塞翁失马焉知非福,程序崩溃,没有走到 file.delete(), 所以在cache dir下可以找到这个文件。
jadx 反编译下这个dex文件,逻辑一目了然,是个换表base64。
最后便是 check函数逻辑,在ida中找到注册check函数反向定位,恢复JNI 函数符号,逻辑很简单,就是个字符base64加密 作为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 | unsigned int __cdecl check( int a1, int a2, int a3, int a4) { _DWORD v5[ 16 ]; // [esp+0h] [ebp-B8h] BYREF int v6; // [esp+44h] [ebp-74h] int v7; // [esp+48h] [ebp-70h] int v8; // [esp+4Ch] [ebp-6Ch] int v9; // [esp+50h] [ebp-68h] int v10; // [esp+54h] [ebp-64h] int v11; // [esp+58h] [ebp-60h] int v12; // [esp+5Ch] [ebp-5Ch] int v13; // [esp+60h] [ebp-58h] int v14; // [esp+64h] [ebp-54h] int rc4_key; // [esp+68h] [ebp-50h] int v16; // [esp+6Ch] [ebp-4Ch] Elf32_Dyn **v17; // [esp+70h] [ebp-48h] int v18; // [esp+74h] [ebp-44h] _BYTE *v19; // [esp+78h] [ebp-40h] char v20; // [esp+7Ch] [ebp-3Ch] BYREF _BYTE enc[ 43 ]; // [esp+7Dh] [ebp-3Bh] BYREF unsigned int v22; // [esp+A8h] [ebp-10h] v22 = __readgsdword(0x14u); rc4_key = GetStringUTFChars(a1, a4, ( int )&v20); v14 = FindClass(a1, ( int ) "com/nobody/zunjia/DexCall" ); v13 = GetMethodID(a1, v14, ( int ) "<init>" , ( int ) "()V" ); v12 = NewObjectV(a1, v14, v13); v11 = GetStaticMethodID( a1, v14, ( int ) "callDexMethod" , ( int ) "(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object;" ); v10 = NewStringUTF(a1, ( int ) "zunjia.dex" ); v9 = NewStringUTF(a1, ( int ) "com.nobody.zundujiadu" ); v8 = NewStringUTF(a1, ( int ) "encode" ); v19 = enc; v18 = 43 ; v17 = &off_2764 + 0x40B ; v16 = 43 ; v5[ 10 ] = enc; __memcpy_chk(enc, &off_2764 + 0x40B , 43 , 43 ); rc4_enc(( int )enc, rc4_key); v7 = NewByteArray(a1, 43 ); SetByteArrayRegion(a1, v7, 0 , 43 , ( int )enc); v5[ 15 ] = a1; v5[ 14 ] = v10; v5[ 13 ] = v9; v5[ 12 ] = v8; v5[ 11 ] = v5; v6 = CallStaticObjectMethodV(a1, v14, v11, a3, v10, v9, v8, v7); // call encode v5[ 3 ] = GetStringUTFChars(a1, v6, ( int )&v20); __android_log_print( 4 , "Native" , "Result is %s\nTry decrypto it, you will get flag! But realy?" ); return __readgsdword(0x14u); } |
密钥长度为36位,写个程序枚举:
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 | rc4_enc = bytes([ 0x7A , 0xC7 , 0xC7 , 0x94 , 0x51 , 0x82 , 0xF5 , 0x99 , 0x0C , 0x30 , 0xC8 , 0xCD , 0x97 , 0xFE , 0x3D , 0xD2 , 0xAE , 0x0E , 0xBA , 0x83 , 0x59 , 0x87 , 0xBB , 0xC6 , 0x35 , 0xE1 , 0x8C , 0x59 , 0xEF , 0xAD , 0xFA , 0x94 , 0x74 , 0xD3 , 0x42 , 0x27 , 0x98 , 0x77 , 0x54 , 0x3B , 0x46 , 0x5E , 0x95 ]) combinations = [] def find_combinations(current_combination, remaining_length): if remaining_length = = 0 : combinations.append(current_combination) # print(current_combination) return if remaining_length > = 3 : find_combinations(current_combination + "0.o" , remaining_length - 3 ) if remaining_length > = 3 : find_combinations(current_combination + "o.0" , remaining_length - 3 ) # 调用函数,开始寻找所有组合 find_combinations("", 36 ) print (combinations) print ( len (combinations)) import base64 import Crypto.Cipher.ARC4 # 自定义字符集 CUSTOM_ALPHABET = "3GHIJKLMNOPQRSTUb=cdefghijklmnopWXYZ/12+406789VaqrstuvwxyzABCDEF5" # 标准Base64字符集 STANDARD_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=" # 自定义Base64编码 def custom_base64_encode(data): # 使用标准Base64进行编码 base64_bytes = base64.b64encode(data.encode( 'utf-8' )) base64_str = base64_bytes.decode( 'utf-8' ) # 创建字符替换表 translation_table = str .maketrans(STANDARD_ALPHABET, CUSTOM_ALPHABET) # 替换字符 return base64_str.translate(translation_table) for comb in combinations: new_comb = '' for i in range ( 36 ): new_comb + = chr ( ord (comb[i]) ^ i) key = custom_base64_encode(new_comb).encode( 'ascii' ) dec = Crypto.Cipher.ARC4.new(key).encrypt(rc4_enc) if dec.startswith(b "hgame" ): print (comb) print (dec) exit() |
更多【CTF对抗-2025 HGAME WEEK1 RE WP】相关视频教程:www.yxfzedu.com