参加活动的第二个年头,今年的题不管是数量还是范围都多了不少,这意味着今年更卷了😂
这篇题解就放在博客作个记录,希望明年还能有空参加
Windows 初级题 1/23
cpp逆向,简单移位
bytearray([i >> 2 for i in [0x00000198, 0x000001B0, 0x00000184, 0x0000019C, 0x000001EC, 0x000000D4, 0x000000C8, 0x00000140, 0x000001BC, 0x00000128, 0x000001A4, 0x00000194, 0x000000C8, 0x000000C0, 0x000000C8, 0x000000CC, 0x00000120, 0x00000184, 0x000001C0, 0x000001C0, 0x000001E4, 0x00000138, 0x00000194, 0x000001DC, 0x00000164, 0x00000194, 0x00000184, 0x000001C8, 0x000001F4]])
Android 初级题 1/24
Java逆向,简单位操作
bytearray([i - 2 for i in b"hnci}|jwfclkczkppkcpmwckng\x7f"])
Android 初级题 1/25
frida hook
objection -g com.zj.wuaipojie2023_1 explore android hooking watch class_method com.zj.wuaipojie2023_1.C.cipher --dump-return
Windows 中级题 1/26
魔改upx,oep定位,dump&fix,关闭ASLR。
MFC程序,SEH异常处理,调用结构类似以下:
void DivException(int a1, int a2, int* a3) { __try { *a3 = a1 / a2; } __except (GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO) { } } void DbgBrk() { __try { DebugBreak(); } __except (GetExceptionCode() == EXCEPTION_BREAKPOINT) { getUSER32BaseAddr(); // +0x2410 } } int entry() { __try { RaiseException(1u, 0, 0, 0); } __except (filterLoadApiTable() /* +0x2DC0 */) { DbgBrk(); // +0x2FC0 } __finally { DivException(1, 0, 0); // +0x2D80 } divZero(); // +0x2FF0 return 0; }
这里注意到了divZero
并没有对应注册SEH。
而是在_tmainCRTStartup
初始化程序时,调用SetUnhandledExceptionFilter
注册了UEH,具体位置在偏移0x100B
上。
同时,由于SetUnhandledExceptionFilter
内部调用了ntdll!NtQueryInformationProcess
检测调试器,在这里结合divZero
实现了反调试。
将call divZero
patch 为call TopLevelExceptionFilter
完成反反调试
流程分析中的几个点:
通过PEB获取USER32.dll
基址,进而拿到函数表。
API表位于偏移0x17C90
存储解密字符串表指针位于0x17D30
,内容为宽字符形式
粗略分析,疑似check两处:0x2110
和0x2A50
tea算法两处
- tea encrypt
0x1D70
- tea decrypt
0x26E0
懒虫方案
猜能省大量时间(
frida hook俩函数,修改返回值 & 打表
发现只有修改0x2A50
函数返回值为4时,弹出Success
,逻辑部分简单描述如下
- 十六进制转字符数组
- tea解密
- 与明文对比
调试拿到明文
frida hook拿到k
和delta
const addr = Module.findBaseAddress("【2023春节】解题领红包之五_dump_SCY.exe"); function buf2hex(buffer) { return [...new Uint8Array(buffer)] .map(x => '0x'+x.toString(16).padStart(2, '0').toUpperCase()) .join(' '); } Interceptor.attach(addr.add(0x26e0), { onEnter: function (args) { console.log("[+] onEnter"); console.log("[+] v: " + buf2hex(args[0].readByteArray(8))); console.log("[+] k: " + buf2hex(args[1].readByteArray(16))); console.log("[+] delta: " + args[2]); console.log("[+] sum: " + args[3]); }, onLeave: function (retval) { console.log("[+] retval: " + retval); } });
跑一遍标准tea,转成十六进制拿到flag
void tea_encrypt(uint32_t*a1, uint32_t *a2, uint32_t a3, uint32_t a4) { unsigned int v5; // [rsp+0h] [rbp-28h] unsigned int v6; // [rsp+4h] [rbp-24h] unsigned int i; // [rsp+8h] [rbp-20h] v5 = *a1; v6 = a1[1]; for ( i = 0; i < 0x20; ++i ) { a4 += a3; v5 += (a2[1] + (v6 >> 5)) ^ (a4 + v6) ^ (*a2 + 16 * v6); v6 += (a2[3] + (v5 >> 5)) ^ (a4 + v5) ^ (a2[2] + 16 * v5); } *a1 = v5; a1[1] = v6; } int main() { char inp[] = "flag{!!!_HAPPY_NEW_YEAR_2023!!!}"; auto *v = (uint32_t*) inp; uint32_t k[4] = {0x8C7BD7F8, 0x18F7AFF0, 0xA57387E8, 0x31EF5FE0}; uint32_t delta = 0x8c7bd7f7; uint32_t sum = 0x8f7afee0; for (int i = 0; i < 4; ++i) { tea_encrypt(v+i*2, k, delta, 0); printf("%08X%08X", v[i*2], v[i*2+1]); } printf("\n"); }
正常方案
程序调用DialogBoxParamW
创建对话框,0x11D0
为DialogFunc
WM_INITDIALOG
初始化窗口
单击按钮,首先执行WM_CREATE
处内容
分析0x2110
部分看似进行了tea encrypt并进行了check,实际上只是混淆视听,其真正的用途是传递sum
而后调用PostMessageW
执行WM_CUT
部分,此时lParam
即为sum
进入default分支,v6
恒为0,能看到真正的check在0x2A50
处
check算法主要是根据uid
,计算delta
,以及k
,而后tea解密,大致算法如下
v11 = 0x11111111; for ( i = 0; i < 14; ++i ) v11 += 0x11111111; for ( delta = v11 + uid; delta >= 0; delta = 2 * delta + 9 ) ; for ( n = 0; n < 4; ++n ) k[n] = (n + 1) * (delta + 1); tea_decrypt(&flag[m], k, delta, sum)
附keygen
from ctypes import c_int32 import struct def calc_delta(uid): i = c_int32(uid - 1) while i.value >= 0: i.value = 2 * i.value + 9 return i.value & 0xffffffff def calc_key(delta): k = [0] * 4 for i in range(4): k[i] = (i + 1) * (delta + 1) k[i] &= 0xffffffff return k def tea_encrypt(v, k, delta): v0, v1 = v k0, k1, k2, k3 = k sums = 0 for i in range(32): sums += delta sums &= 0xffffffff v0 += ((v1 << 4) + k0) ^ (v1 + sums) ^ ((v1 >> 5) + k1) v0 &= 0xffffffff v1 += ((v0 << 4) + k2) ^ (v0 + sums) ^ ((v0 >> 5) + k3) v1 &= 0xffffffff return v0, v1 def keygen(uid): FLAG = struct.unpack('<8L', b'flag{!!!_HAPPY_NEW_YEAR_2023!!!}') flag = [0] * 8 delta = calc_delta(uid) k = calc_key(delta) for i in range(4): flag[i*2], flag[i*2+1] = tea_encrypt(FLAG[i*2:i*2+2], k, delta) return "".join(f"{x:08x}" for x in flag) if __name__ == '__main__': print(keygen(1150835))
Android 中级题 1/27
ndk逆向,java层没啥用
流程完全没有串起来,需要脑洞
根据符号猜作者意图:找到RealKey解密assets/aes.png
算法为AES-128-ECB PKSC7 padding
JNI注册了函数get_RealKey
,结尾调用的strcmp
上有个不明显的hint:thisiskey
,其意为执行getRealKey
的算法得到RealKey
k = b'|wfkuqokj4548366' xmmword_3030 = [0xFB, 0xFE, 0xFB, 0xFE, 0xFB, 0xFE, 0xFB, 0xFE, 0xFB, 0xFE, 0xFB, 0xFE, 0xFB, 0xFE, 0xFB, 0xFE] v5 = [(i + j) & 0xff for i, j in zip(k, xmmword_3030)] real = bytearray(v5) print(real) # bytearray(b'wuaipojie2023114')
AES解出图片
import base64 from Crypto.Cipher import AES def pad(s): return s + (16 - len(s) % 16) * chr(16 - len(s) % 16) def unpad(s): return s[:-ord(s[len(s) - 1:])] cipherText = open('com.zj.wuaipojie2023_2/assets/aes.png', 'rb').read() cipherText = base64.urlsafe_b64decode(cipherText) ctx = AES.new(key=b'wuaipojie2023114', mode=AES.MODE_ECB) dec = unpad(ctx.decrypt(cipherText)) pic = bytearray.fromhex(dec.decode('utf-8')) with open('aes.dec.png', 'wb') as f: f.write(pic)
然后misc行为:文件末尾塞了一个png图片
扫码得到flag
Android 高级题 1/28
ndk逆向,JNI动态注册checkSn
,ollvm sub fla bcf拉满,流程混淆的稀碎
我选择摆烂unidbg trace
贴个片段
public static void main(String[] args) { emulator = AndroidEmulatorBuilder.for64Bit().setRootDir(new File("target/rootfs")).setProcessName("com.zj.wuaipojie2023_2").build(); Memory memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver(23)); Logger.getLogger("com.github.unidbg.linux.ARM32SyscallHandler").setLevel(Level.DEBUG); Logger.getLogger("com.github.unidbg.unix.UnixSyscallHandler").setLevel(Level.DEBUG); Logger.getLogger("com.github.unidbg.AbstractEmulator").setLevel(Level.DEBUG); Logger.getLogger("com.github.unidbg.linux.android.dvm.DalvikVM").setLevel(Level.DEBUG); Logger.getLogger("com.github.unidbg.linux.android.dvm.BaseVM").setLevel(Level.DEBUG); Logger.getLogger("com.github.unidbg.linux.android.dvm").setLevel(Level.DEBUG); vm = emulator.createDalvikVM(); vm.setVerbose(true); new AndroidModule(emulator, vm).register(memory); DalvikModule dm = vm.loadLibrary(new File("lib52pojie.so"), false); vm.setJni(new MyJni()); dm.callJNI_OnLoad(emulator); module = dm.getModule(); // PrintStream printStream = null; // try { // printStream = new PrintStream(new FileOutputStream("log.txt")); // } catch (FileNotFoundException e) { // throw new RuntimeException(e); // } // // emulator.traceCode().setRedirect(printStream); String uid = "01150835"; String flag = "MWYxODIxOThmYWFmM2ZlYjYxM2Q5ZDVlZTExMzg4MTM="; List<Object> list = new ArrayList<>(); list.add(vm.getJNIEnv()); list.add(0); list.add(vm.addLocalObject(new StringObject(vm, uid))); list.add(vm.addLocalObject(new StringObject(vm, flag))); Debugger dbg = emulator.attach(); // dbg.addBreakPoint(0x4001064c); Number number = module.callFunction(emulator, 0xe830, list.toArray()); System.out.println("result: " + number.intValue()); }
分析下jni注册,checkSn
偏移E830
调用一次checkSn大概86w行,ida+trace日志+unidbg调试分析,改变同一位置字节、不同位置字节
对比两者trace差异,简单贴几个关键偏移
0x184B8 -> md5_transform 0x1B32C -> 0x17834 -> md5(const uint8_t* uid) 0x1ECD8 -> zuc_F(pzuc_context context) 0x1D454 -> zuc_init(pzuc_context context, const uint8_t* key, const uint8_t* iv) 0x10668 -> md5_hexdigests(uid) xor zuc_encrypt(base64_decode(flag)) == 0
md5、base64为标准算法
zuc算法为流加密,参考代码 https://github.com/zhiyuan-lin/zuc/blob/master/c/zuc.c
key
与时间戳相关,iv
来自偏移4D22B
,具体组成如下
char key[16]={}; char iv[16]={}; char buf[6]={}; int t = timestamp/20000000; // 0x185f69b0a37 / 20000000 == 83743 sprintf(buf, "%d", t); memcpy(key, 0x4D220, 0xA); // 0x3d,0x99,0x40,0x0e,0x05,0x50,0x7c,0x81,0x2e,0x3f memcpy(key+0xA, buf, 0x5); // 0x38,0x33,0x37,0x34,0x35,0x00 memcpy(iv, 0x4D22B, 0x5); // 0xf2,0x3d,0xa0,0x96,0x02,0x2b,0x26,0x63,0xe1,0x5f,0xc1,0x28,0x6a,0x33,0x70,0x25
流加密,即
crypt(crypt(inp)) == inp
直接偷懒
uid、flag输入格式
uid # 8位,不足补0 flag = base64.b64encode(md5(uid).hexdigest()) # 44 bytes after base64 encode
unidbg跑起来dump
final byte[] res = new byte[32]; final int[] i = {0}; dbg.addBreakPoint(0x40010668, new BreakPointCallback() { @Override public boolean onHit(Emulator<?> emulator, long address) { res[i[0]++] = (byte) emulator.getContext().getLongByReg(Arm64Const.UC_ARM64_REG_X15); return true; } });
然后转成base64,输回去就成了
至于keygen,分析这一坨太费时间,略
Web 初/中/高
应该改名misc+web+古典crypto套娃题
直接挂个草稿吧(不完全)
2023challenge.52pojie.cn
adg dns重定向 52pojie.cn
flag1 放视频 flag1{52pojiehappynewyear} ---------------------------------------------------------------------------- flag2 视频二维码 https://2023challenge.52pojie.cn/?flag=flag2{878a48f2} flag2{878a48f2} ---------------------------------------------------------------------------- flag3 视频右下角水印 iodj3(06i95dig) 凯撒 offset 3 flag3(06f95afd) ---------------------------------------------------------------------------- ---------------------------------------------------------------------------- flag5 morse code flag5{eait} ---------------------------------------------------------------------------- flag6 flag6{590124} 视频开头 拨号盘 https://blog.xhyeax.com/2019/02/03/friend-ctf/ --------------------------------------------------------------------------- flag7 网页 二进制 flag7{5d06be63} ---------------------------------------------------------------------------- flag8 音频频谱 flag8{c394d7} ---------------------------------------------------------------------------- flag9 倒着放音频 flag9{21c5f8} ---------------------------------------------------------------------------- ---------------------------------------------------------------------------- flag11 网页下方brainfuck flag11{63418de7} ---------------------------------------------------------------------------- flagA curl "https://2023challenge.52pojie.cn/?uid=1150835" \ -H "X-52PoJie-Uid: 1150835" \ -v ---------------------------------------------------------------------------- flagB dns解析 dig 2023challenge.52pojie.cn @8.8.8.8 -t txt 2023challenge.52pojie.cn. 600 IN TXT "\"_52pojie_2023_happy_new_year=flagB{substr(md5(uid+\\\"_happy_new_year_\\\"+floor(timestamp/600)),0,8)}\"" ---------------------------------------------------------------------------- flagC jwt伪造 eyJ1aWQiOiIxMTUwODM1Iiwicm9sZSI6ImFkbWluIn0 curl 'https://2023challenge.52pojie.cn/home' \ -H 'Cookie: 2023_challenge_jwt_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiIxMTUwODM1Iiwicm9sZSI6ImFkbWluIn0.U2Yd0PGhcJq_D0QAicnA8xTb1menooqWBaLCt65y1Cw' \ -v
发表回复