真的很菜菜的ROP
栈上的简单小玩意们
随着 NX 保护的开启,以往直接向栈或者堆上直接注入代码的方式难以继续发挥效果。攻击者们也提出来相应的方法来绕过保护,目前主要的是 ROP(Return Oriented Programming,面向返回编程),其主要思想是在栈缓冲区溢出的基础上,利用程序中已有的小片段( gadgets )来改变某些寄存器或者变量的值,从而控制程序的执行流程。
所谓 gadgets 就是以 ret 结尾的指令序列,通过这些指令序列,我们可以修改某些地址的内容,方便控制程序的执行流程。
实施ROP的条件
- 程序存在栈溢出,且可以控制返回地址
- 可以找到满足程序的 gadgets 以及相应的 gadgets 的地址
都是copy来的)看到有的人说这个叫“面向返回地址”编程哈哈。
ret2text
ret2text 即控制程序执行程序本身已有的的代码(.text),比如system("/bin/sh")
或者system("cat flag")
。我们需要做的就是把这些代码段的调用地址覆盖到返回地址上。
我们在控制程序执行的时候也可以控制程序执行好几段不相邻的已有代码,这是我们需要知道对应返回的代码的位置。程序如果开启了某些保护,我们就需要想办法去绕过它。
🌰_1:
点击下载: ret2text
使用ida反编译main函数,发现gets()语句,可以利用栈溢出
而后我们在secure中可以找到调用system('"/bin/sh")
的代码,如果我们控制程序返回到这个指令,就可以获得系统的shell
查看代码地址为0x0804863A,接下来构造playload。
1 |
|
🌰_2:
来自CTFHUB的ret2text ,我不知道怎么复制题目链接,自己点进去找叭CTFHub
查看文件信息
拖进32位IDA反编译发现main函数存在gets栈溢出漏洞
shift+F12打开字符串窗口发现/bin/sh语句
去到对应的语句部分可以发现该程序将/bin/sh放到了rdi后,并且调用了sysytem
64位系统中rdi,rsi,rdx,rcx,r8,r9作为调用函数的前6个参数,如果参数多于6个,其余参数放入栈中。与此对比,32位系统中由于寄存器有限,调用函数时参数都放入栈中
找一个变量双击可以看到各个变量对应的虚拟内存地址,构造playload就可以先用0x70个自节填满s变量,再用8个字节填满r,最后加上/bin/sh的地址。
1 |
|
‘+4’ 是因为32位程序要覆盖的ebp是四个字节,64位程序需要覆盖八字节的rbp
ret2shellcode
就是篡改栈帧上的返回地址为攻击者手动传入的shellcode所在缓冲区地址。我们可以利用pwntools中的shellcraft.sh()编写shellcode。
在栈溢出的基础上,要想执行 shellcode,需要对应的 binary 在运行时,shellcode 所在的区域具有可执行权限。
🌰_1:
点击下载题目: ret2shellcode
查看文件信息:
进入main函数反编译一下:
发现gets和strncpy存在栈溢出漏洞,但系统没有现成的‘bin/sh’指令给我们用。我们要自己写🥲
那怎么写呢?这时就要请出我们的得力助手pwntools,在pwntools下可以自动生成shellcode脚本。
gets获得输入的s变量,并将其复制到buf2处。双击buf2可以跳转到其所在位置,我们发现buf2在.bss段,所在段地址为0x0804a080
调试下程序,看看这一个 bss 段是否可执行,显示rwxp就是可以。
有权限就意味着我们可以将shellcode通过strncpy函数放进buf2这个区域,在触发溢出后将返回地址指向buf2这里即可拿到shell。
确定要覆盖的 return address 的偏移量相对于栈顶为 112 个字节。
EXP:
1 |
|
🌰_2:
还是CTFHub的题目。放进ubuntu查看文件信息:
NX disabledz,即将shellcode放在数据段,即可执行
出现了一个新的变量RWX!
用IDA反编译题目main函数发现栈溢出漏洞
我们可以知道buf相对于ebp的偏移量是0x10,所以我们需要填充(0x10+8)的数据。
双击buf查看程序中的变量信息
r即为返回地址,有0x8的偏移量,所以总共需要填充的数据长度是 (0x10+0x8+0x8)
1 |
|
这也太深奥了没搞懂,有缘再说
ret2syscall
ret2syscall即控制程序执行系统调用,从而获取shell。前提是程序需要有int 0x80这样的系统调用的gadget.
在ret2shellcode的情境下,如果开启了NX,那我们写到栈中的shellcode就会被CPU报错而不可执行。这种情况下,我们可以尝试使用ret2syscall的方法。
ret2syscall就是指通过手机带有ret指令的指令片段拼接成我们需要的shellcode。
ret 指令可以理解为取栈顶的数据作为下次跳转的位置,即 eip=[esp] esp=[esp+4]
或者理解为 pop eip,jmp
取栈顶数据作为下次跳转的位置,然后跳转;
同理call也可以理解为 push rip, jmp
将call指令的下一条指令地址压入栈,然后跳转
🌰:
bamboofox 中的 ret2syscall
首先检测程序开启的保护:
查看其源码寻找利用点:
main 函数里有 gets 可以实现栈溢出,计算偏移量为112。
接下来进行系统调用构造,利用ROPgadget查找对应的gadget:
选择0x080bb196的这段
同样,找到其他的gadgets
这里可以同时控制三个寄存器,我们选它
选好所有的gadgets就拼接起来构造payload就好喽
1 |
|
关于linux系统调用的实现
系统调用的步骤
Linux的系统调用需要通过 int 80 实现,用系统调用号来区分入口函数。操作系统实现调用的基本过程如下:
- 应用程序调用库函数
- API将系统调用号存入EAX,然后通过中断调用时系统进入内核态
- 内核中的中断处理函数根据系统调用号,调用到对应的内核函数(系统调用)
- 系统调用完成相应的功能,将返回值存入EAX,返回到中断处理函数
- 中断处理函数返回到API
- API将EAX返回给应用程序
系统调用号:
在Linux系统中,每个系统调用都被赋予一个系统调用号。系统调用号一旦分配就不能再有任何变更,否则编译好的应用程序就会崩溃;此外,如果一个系统调用被删除,它所占用的系统调用号也不允许被回收利用。这样,通过系统调用号就可以关联系统调用。
比如32位下调用 execve(“/bin/sh”,NULL,NULL) ,Linux系统调用通过int 0x80指令开始系统调用,exceve对应的系统调用号是0xb
举个栗子🌰:
函数execve("/bin/sh",null,null)
其函数调用过程应该是:
- 系统调用号存入EAX,即eax应该是0xb
- 依次传入三个参数,即ebx指向/bin/sh的地址或者sh的地址;ecx为0;edx为0
我们可以知道系统在调用时会用到eax,ebx,ecx,edx四个寄存器,那么我们就可以将以上的内容写为int 0x80(eax,ebx,ecx,edx)
。只要把对应的参数放到相应的寄存器中,再执行int 0x80就可以执行相应的系统调用。
那该怎么控制呢?🤨 把刀架在寄存器脖子上
我们们可以使用pop和ret指令组合来控制寄存器的值以及执行方向。
工具
ROPgadget和ropper,两个都可可以找,目标汇编代码片段,ROPgadget速度更快但是查找结果并不完整;ropper速度相对较慢但查找结果精准。
对于静态生成的程序,我们可以不用每一次都一条一条的去找命令,可以直接生成一条ROP链
1 |
|
ret2libc
ret2libc即控制函数执行libc中的函数,通常是返回值某个函数的plt处或者函数的具体位置(即函数对应的got表项的内容)。一般情况下,我们会选择执行 system(“/bin/sh”),我们需要知道 system 函数的地址。
🌰_1:
CTF Wiki 的题,超简单的所有信息都给出的新手题目 我都会做
首先查看安全保护
源程序为 32 位,开启了 NX 保护。看一下程序源代码,确定栈溢出利用位置
gets可以栈溢出。
我们可以找到system函数和’bin/sh’字符串
我们就可以直接返回 system 的地址
计算偏移量为112,构造payload。需要注意,我们调用system函数,会有一个对应的四字节的返回地址,我们需要将其填充后再附上我们的 ‘/bin/sh’ 字符串
1 |
|
🌰_2:
同样是32位的程序开启NX保护,有一个gets可以利用。有system函数但没有 ‘/bin/sh’ 字符串,但是有一个gets()函数,我们可以自己读取。同时我们在.bss段发现一个buf可以传参,可以把字符串填在这里
编写exp:
1 |
|
payload解释:
'A'*112
:填充112个字符’A’溢出缓冲区,覆盖返回地址。gets_addr
:将gets
函数的地址作为原本程序的gets()函数的返回地址,控制程序流程跳转到gets
函数。sys_addr
:system
函数的地址,将作为gets
函数返回后的下一个地址,控制程序流程跳转到system
函数。buf_addr
:缓冲区的地址,作为system
函数的参数,传递给system
函数的命令字符串所在的内存地址。buf_addr
:再次使用缓冲区的地址,作为gets
函数的参数,使得gets
函数将用户输入的命令字符串写入到缓冲区。
🌰_3:
这次system也没了🤨,其他的保护和漏洞都和例一例二一样。除此之外,题目中又给了一个 libc.so 动态链接库。
那怎么得到system的地址呢?
补课时间到!
- system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。
- 即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变。而 libc 在 github 上有人进行收集,https://github.com/niklasb/libc-database
除此之外我们还要知道,A 真实地址 (内存物理地址) - A 偏移地址 = B 真实地址 (内存物理地址) -B 偏移地址 = 基地址。也就是说,B的真实地址=基地址+B的偏移地址。
所以如果我们知道 libc 中某个函数的地址,那么我们就可以确定该程序利用的 libc。进而我们就可以知道 system 函数的地址。
那该如何获得已知函数的地址呢?
got表泄露!输出某个函数对应的got表内容,注意libc的延迟绑定机制,got表中只有已经执行过的函数有真实地址,我们需要泄露已经执行过的函数地址。再在程序中查询偏移进一步获得system地址。
但是!这样太麻烦啦我们可以用工具:https://github.com/lieanu/LibcSearcher
exp:
1 |
|
ret2csu
了解ret2csu之前先了解一下attached code的概念。
我们编译一个简单的只有一个main函数的程序:
1 |
|
查看可执行文件的函数符号:
1 |
|
可以发现除了 main 函数还有很多其它函数,这些函数是编译器附加到可执行文件中的,称之为 attached code 。这些 attached code 在main 函数之前执行,负责加载或者链接库文件。我们可以从 attached code 中寻找可以利用的 gadgets。
我们利用 objdum -D [filename]
命令查看文件反汇编代码可以看到 __libc_csu_init()
函数中存在以下gadget:
在 64 位程序中,函数的前 6 个参数是通过寄存器传递的,参数从左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9。但是大多数时候,我们很难找到每一个寄存器对应的 gadgets。 这时候,我们可以利用 __libc_csu_init
中的 gadgets。我们可以在执行完第二个gadget后ret到第一个gadget,这样就可以控制很多关键寄存器的值。
需要注意的是,这种方法不能控制rax的值,也就无法进行系统调用,因为系统调用号在rax里。但我们可以通过write等函数泄露got表中的函数地址,然后计算出libc地址。
有以下几种利用场景:
- ret2csu泄露libc地址后利用libc中的gadget
- ret2csu配合pop rax;syscall;等gadget直接getshell
- 开启pie的情况下,利用offset2lib进行ret2csu,或者直接利用libc中的gadget getshell
offset2lib简单来说就是泄露任意代码段地址即可推得所有共享库地址,因为共享库之间的offset是固定的.
🌰_1:
题目链接:https://github.com/zhengmin1989/ROP_STEP_BY_STEP/blob/master/linux_x64/level5。
题目没有开启pie和canary保护。
蒸米师傅给了源码,我们自己拖到 ida 里也能判断出代码的基本逻辑,main() 函数里还有一个 vulnerable_function()
函数 :
1 |
|
在read处有明显的栈溢出可以利用。.plt
表里就只有write函数和read函数。
我们的目的是调用 system("/bin/sh")
,可以先泄露出libc函数的地址,用write打印出来,通过计算偏移就可以求出system函数的地址。然后使用read函数将真实的system函数地址和/bin/sh字符串写入bss段,最后调用system函数即可。
我们知道在64位的程序中,前六个参数使用寄存器RDI, RSI, RDX, RCX, R8和 R9传递
在__libc_csu_init()函数中有可以利用的gadgets
我们控制rbx,rbp,r12,r13,r14和r15的值,再将r15的值赋值给rdx,r14的值赋值给rsi,r15的值赋值给edi。简单来说对应关系就是:
1 |
|
随后就会调用 call qword ptr [r12+rbx*8]
。这条指令的含义是向 [r12+rbx*8]
间接寻址,跳转到所指的函数地址。我们就可以将rbx赋值为0,这样的寻址结果就是r12所指向的地址。
接下来的汇编代码片段含义为:执行call指令结束后,程序对rbx加一,然后对比rbx和rbp的值,如果相等就会继续向下执行。为了让rbx和rbp相等,我们需要将rbp赋值为1。
- payload-1
利用 read()
读入我们的payload,write()
输出其got表中的地址。除了泄露地址,为了返回到原程序中重复利用 buffer overflow
的漏洞,我们需要继续覆盖栈上的数据,直到把返回值覆盖成目标函数的main函数为止。
总结一下我们要实现的payload需求:rbx=0,rbp=1,r12=write_address,rdi=edi=r13,rsi=r14,rdx=r15
,write(1,write_got,8)
1 |
|
write(1,write_got,8) 的含义为将 writ 函数的地址写入标准输出流中,写入的字节数为 8
在收到write()在内存中的地址后,就可以计算出system()在内存中的地址。借此就可以将execve的地址以及“/bin/sh”读入到.bss段内存中。
1 |
|
read(0,bss_base,16) 的含义为从标准输入中读取最多 16 个字节的数据,并将数据存储到位于
bss_base
地址处的缓冲区中。
最后调用执行 execve('/bin/sh',0,0)
1 |
|
总的exp为:
1 |
|
很菜菜的ROP,把笔记收拾收拾发一下。
本来想说栈上全部东西都搞完再发,但最近github上的绿点点都少了就提前发一部分喽😢