ROP汇总

一、32位ROP:

1.如果是直接跳转plt表中的地址,那么栈的布置顺序应该是:

system函数-system函数的返回地址-sytem函数的参数。

2.但如果是跳转call system,那么由于call指令会自动push进eip,则栈布置应该为:

call system函数地址-system函数参数。

(两者不太一样,需要加以区分。后面会有got表和plt的详细讲解)

二、64位ROP:

需要传参指令:pop rdi;ret。这里就不用管是plt还是call了,因为传参是rdi传参,返回地址是啥都没关系,多参数的需要万能gadget。

▲64位程序中函数取参数是取rdi中内容指向的内存中的内容,相当于rdi,同样的32位程序中取参是取栈上的内容指向的内存中的内容,相当于[ebp+var_0xh],所以直接输入binsh字符串来赋值给rdi或者赋值给函数参数肯定是不行的,因为这不是一个有效的地址,而是字符串的二进制形式。

三、万能gadget

1.传入got表和plt表的区别:

万能gadget中调用我们想调用的函数为call qword ptr[r12+rbx*8],硬编码为FF,是取r12中保存的内容当作一个地址a,这个地址a保存的内容应该是一个地址b,该地址b指向的地方才是可以被执行的实际代码位置。

例如:

1
2
3
4
5
#注释头

r12: got_a
got_a: 0x111
0x111: mov a b

所以call qword ptr[r12+rbx*8]实际执行跳转到的位置是0x111,而执行的代码是mov a b;只能传入got表,如果传入plt表,那么应该如下:

1
2
3
4
5
6
#注释头

r12: plt_a
plt_a: jmp got_a
jmp got_a: 无效编码地址
#这个jmp got_a转换成硬编码就不是一个有效地址

2.不同的call区别:

FF的call后面跟的是地址的地址。例如call [func], 跳转的地方就应该是func这个地址里保存的内容,也就是*func。

E8的call后面跟的是地址。例如call func,跳转的地方就是func的开头。

▲普通call,EB编码:call fun_c(最常用的)

fun_c: mov a b

相当于直接跳转到fun_c这个地址来执行代码

四、main函数返回的ROP:

1.最开始启动程序时,main函数栈不是汇编代码写的那么大,而应该再大两个0x04用来存放全局偏移,所以计算偏移时就需要再加上两个0x04

2.通过再次进入main函数中之后,程序只会依照汇编代码来构造Mainh函数栈,所以这一次里的main函数栈中就没有全局偏移的东西了,正常计算偏移。

五、技巧性:

1.通过覆盖返回地址调用函数时,可以注意上一个函数栈中的esp的位置,然后直接通过Pop等操作继续往下retn到输入的payload中的函数地址。(RedHat 2017-pwn1)

2.rop主要是找system函数和binsh字符串,没有的话其实用int80可以代替system,然后sh\bash什么的也可以代替binsh字符串。

3.使用int80的话需要设置寄存器,同样如果有其它可以用到的系统调用,可以通过:

http://syscalls.kernelgrok.com/ 这个来查找

4.onegadget需要条件满足,可以直接查:one_gadget libc文件,然后通过调试或者IDA看汇编,观察到达调用onegadget的时候条件满不满足。

5.查找:

1
2
3
4
#注释头

one_gadget libc_so.6
ROPgadget --binary file | grep "pop eax ; pop ebx ; pop esi ; pop edi ; ret"

总结:

所以万能gadget中通过r12,传入跳转函数的地址只能是发生延迟绑定之后的got表地址,而不能是plt表地址或者是没有发生延迟绑定的got表地址,(延迟绑定只能通过plt表来操作,没有发生延迟绑定之前,该got表中的内容是等同于无效的,只是一个extern段的偏移地址,除非该函数func是静态编译进程序里面的,那么got表中的内容就是该函数的真实有效地址,不会发生延迟绑定。)因为plt表中的内容转换成硬编码压根就不是一个有效地址,更别说跳转到该地址保存的内容的地方了。有人说跳转到plt表执行的就是跳转got表,那应该是一样的啊,但FF的call并不是跳转到plt来执行里面的代码,而是取plt表中内容当作一个地址再跳转到该地址来执行代码,所以有时候需要看汇编代码来决定究竟是传入got表还是传入plt表。同样也可以看到plt表中的硬编码是FF,也就是并不是跳转got表,而是取got表中保存的内容当作一个地址再来跳转。

需要跳转函数时,有[]的-只能传got表,没[]的-传plt表(plt表更安全好使,但后面格式化字符串劫持got表又有点不太一样,情况比较复杂)。

需要打印真实函数地址时,传的一定是got表,这样就一定没错。

当有call eax;这类语句时,eax中保存的一定得是一个有效地址,因为这里的call硬编码也是0FF。

1.往puts函数中传入函数在got表中的地址(elf.got)参数可以打印出被加载在Libc中的实际内存地址。

2.用覆盖返回地址ret的形式调用函数需要用函数在plt表中的地址,(elf.plt)这是库函数地址,需要先到plt中,然后再到got表中,这是正常的函数调用。

3.但如果在gadget中,则可以通过给r12赋值来调用elf.got表中的函数,因为这个是call qword ptr[r12+rbx*8],指向的是函数在got表中真实地址,需要的是函数在got表中的地址。如果只是call addr,则应该是call函数在plt表中的地址。

4.万能gadget一般在_libc_csu_init中,或者init或者直接ROPgadget查也可以