XCTF-TPCTF2025

portable #

A magic executable file that can run on both Linux and Windows.

actually protable executable

该题使用的不是传统的 libc 而是静态链接的 cosmopolitan libc

而我们手上没有它的符号信息,所以库函数是几乎没法分析的

寻找字符串引用,找到函数 sub_407f30

但在 F5 之前需要先调整调用约定

本题需要使用 GNU C 的调用约定,而 IDA 是以 PE 文件打开的所以默认 MSVC 的调用约定

直接在 IDA 选项里修改

Options -> Compiler -> Compiler

然后 F5 可以看到一大堆 cin 和 cout 的构造和销毁代码

它们虽然是内联的不过长的差不多,可以折叠起来

直接跳到最后的比较

string 的规则:

union std_string {
  struct {
    _BYTE byte0;  // 如果长度小于 16 个字符,首字节低位为 0,剩下 7 位为长度
    _BYTE c_str[15];
  } small; // 长度 16 字节
  struct {
    _BYTE byte0 // 如果长度大于等于 16 字符,首字节低位为 1
    _BYTE dummy[7];
    _QWORD len; // 长度从第 8 字节开始(出于对齐考虑)
    _BYTE *c_str; // 指向堆
  } big; // 长度 24 字节
};

可以看出这里其实就是和 Cosmopolitan 循环 XOR

TPCTF{wE1com3_70_tH3_W0RlD_of_αcτµαlly_pδrταblε_εxεcµταblε}

obfuscator #

Protect `JavaScript` is hard, so let's use obfuscator!

找字符串有

Copyright 2018-2025 the Deno authors. MIT license.

可以找到原项目 deno,是一个 JS 引擎

根据它的源码可以注意到在文件的末尾存在一个以 d3n0l4nd 开头的数据块

该数据块中存储了 JS 的源码

with open('flag_checker','rb') as fp:  
    fp.seek(0x04bb2889)  
    b=fp.read(0x04dc3b9d-0x04bb2889)  
  
with open('protected.js','wb') as fp:  
    fp.write(b)

源码很大而且根据题目它是混淆过的

head -c 128 protected.js

// Job ID: z8qehse5zx5t

let HbGI;!function(){const Aevx=Array.prototype.slice.call(arguments);return eval("(function Y4QD(HkYv){

这个开头有明显的特征,查询开头可以找到一个解混淆器

Deobfuscate PreEmptive’s JSDefender Demo

又是 gzip 的套娃

from base64 import b64decode  
import re  
import gzip  
  
with open('protected_deobf.js','r') as fp:  
    fp.readline()  
    fp.readline()  
    line=fp.readline()  
    m=re.search(r',([0-9a-zA-Z+/=]+)"',line)  
    e64=m.group(1)  
    e64=e64.encode()  
  
  
with open('test','wb') as fp:  
    gz=b64decode(e64)  
    test=gzip.decompress(gz)  
    fp.write(test)  

又是 wasm 的套娃

wasm-decompile test -o dec.txt

https://github.com/golangbot/webassembly/

其实就是把 go 语言编译成了 wasm

在 dec.txt 寻找 ‘Flag’ 可以找到这一段

"0w\af\0c\92t\08\02A\e1\c1\07\e6\d6\18\e6path\09command-line-arguments\0a"
"dep\09github.com/fatih/color\09v1.18.0\09h1:S8gINlzdQ840/4pfAwic/ZE0dj"
"QEH3wM94VfqLTZcOM=\0adep\09github.com/mattn/go-colorable\09v0.1.13\09h"
"1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=\0adep\09github.com/matt"
"n/go-isatty\09v0.0.20\09h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY"
"=\0abuild\09-buildmode=exe\0abuild\09-compiler=gc\0abuild\09-ldflags=""
" -X 'main.encryptedFlag=5ab82be11ac991707e166bbfcbf05cb5776b0ecb34ce"
"659e6209542fecba7ab6ec82f44c1ab55e4f4fb06b37ef935b08' -X 'main.maske"
"dKey=960937ed01e77d7662565988a67abbfe0a2c11742abd00d6cf74de094447f7d3'"
" -X 'main.maskedIV=a7ccfe57d2af1deb1a0088517c0a9240' -X 'main.mask"
"=5dabc0b2b7296eefeaadd177746adb4acb2439e7990792cc7fdb7124b1a2633d' -"
"s -w "\0abuild\09CGO_ENABLED=0\0abuild\09GOARCH=wasm\0abuild\09GOOS=js"

调试信息(大概是吧)里泄漏了编译时使用的命令

而 flag 密文是通过命令行传入的

还有个 mask 应该提示要作 XOR

在 dec.txt 里寻找运行程序时显示的字符串 ‘S3CUR3’

可以在附近看到 AES-CBC 相关的报错字符串

于是尝试 AES-CBC

from Crypto.Cipher import AES  
  
encryptedFlag=bytes.fromhex("5ab82be11ac991707e166bbfcbf05cb5776b0ecb34ce659e6209542fecba7ab6ec82f44c1ab55e4f4fb06b37ef935b08")  
maskedKey=bytes.fromhex("960937ed01e77d7662565988a67abbfe0a2c11742abd00d6cf74de094447f7d3")  
maskedIV=bytes.fromhex("a7ccfe57d2af1deb1a0088517c0a9240")  
mask=bytes.fromhex("5dabc0b2b7296eefeaadd177746adb4acb2439e7990792cc7fdb7124b1a2633d")  
Key=bytes(a^b for a,b in zip(maskedKey,mask))  
IV=bytes(a^b for a,b in zip(maskedIV,mask))  
aes=AES.new(key=Key,mode=AES.MODE_CBC,iv=IV)  
x=aes.decrypt(encryptedFlag)  
print(x)
#TPCTF{m47r3shk4_h4ppy_r3v3r53_g@_w45m}

magicfile #

try to crack this magic file

文件比较大,搜索字符串可以知道该题目静态链接了 libmagic

objdump 显示实际上比较大的是 .rodata 而不是 .text

因此直接拖到 IDA 中分析

  s = (char *)malloc(0x64uLL);  
  printf("Please input the flag: ");  
  __isoc99_scanf("%100s", s);  
  if ( strlen(s) == 48 )  
  {  
    ptr = (void *)sub_5890(0LL);  
    sub_58E0(ptr, &off_119B018, &off_119B010, 1LL);  
    v4 = strlen(s);  
    v7 = (char *)sub_59A0(ptr, s, v4);  
    puts(v7);  
    sub_58A0(ptr);  
  }  
  else  
  {  
    puts("Try again.");  
  }

flag 长度 48

测试一下

perl -e "print 'a'x48" | ./magicfile

Please input the flag: ASCII text, with no line terminators

推测本题是套皮了 libmagic 的识别函数,并植入了一个可以识别 flag 的文件规则

容易知道出题的操作系统是 Ubuntu 22.04

在 docker 里下载 libmagic-dev 并拷贝 /usr/lib/x86-64-linux-gnu/libmagic.a

pelf.exe libmagic.a

sigmake.exe libmagic.pat libmagic-ubuntu-jammy

File -> Load File -> FILRT signature file -> Load SIG file …

导入 libmagic-ubuntu-jammy.sig 即可

看了一下似乎没有魔改

sub_58e0 就是 libmagic 的加载函数

unk_21004 存储了 0x1c041ef1 也就是 .mgc 的开头

经过检查 0x116f9f8 不是地址,而是这个 .mgc 的长度

将它 dump 下来

阅读源码里的 magic 结构体,可以知道

  • offset 位于 12-15 字节
  • 需要比较的值位于 32 字节
  • 输出位于 160 字节
  • 操作符位于第 4 字节
  • 从 = 操作符重现的频率可以知道 magic 结构体在文件中的长度是 0x178

这时候注意到

  • 输入的开头必须是 ‘TPCTF{’
  • 绝大部分的输出是 ‘Try again’

于是寻找不是 Try again 的输出

检查它附近的 magic 结构体

缺第一个字符,但可以很容易猜测出来是 Y

superbooru #

Tired of tagging your images manually? Try Superbooru!!!

Hint:

1. z3 is a powerful tool, perhaps with some extra help...
2. Trace down the substitution chain of check_flag
3. Clock signal

看了官方解法才知道这题其实就差一点了,就当复现吧

WIP