LCTF 2016-pwn100
1.常规checksec,开了NX保护。打开IDA,找漏洞,逐次进入后,sub_40068E()函数中的sub_40063D函数中存在栈溢出:
1 | #注释头 |
这里传的是局部变量v1的地址,所以进入sub_40063D后,修改a1指针对应的内存的值其实就是修改之前局部变量v1的值,就是传指针。这个函数每次读取一个字节,直到读取满200字节,其实就可以直接把它当成read(v1,200)完事。
(题外话:汇编代码中当局部变量传参时,需要用到lea,即:lea rax, [rbp+var_40],就是将栈上的变量var_40的地址给rax,然后传参mov rdi, rax;利用rdi来传函数参数。进入到函数内部后就会有:mov [rbp+var_18], rdi,也就是在该函数栈上创建一个局部变量来保存传入变量的栈上的地址,也就是之前var_40的栈上地址,保存在[rbp+var_18]这个局部变量中。这是这个程序中,不同程序可能不太一样。)
2.所以这个栈溢出的覆盖返回地址应该是sub_40068E函数的返回地址,简单远程调试一下,看看v1所在栈地址和rbp下一地址的距离就是偏移量,为0x48,看汇编计算就可以得到0x40+0x8。
3.现在需要system和binsh,这个程序中这两个都没有带,而只有Libc中才有,但是这个程序并没有泄露Libc的地址。分析程序发现,程序中.plt段中导入了puts函数,IDA中函数名那一块可以看到:所以可以用pwntools中的DynELF,调用该puts函数,从而泄露出libc中puts或者read的地址。由于大多教程选择泄露read,所以这里选择泄露puts函数在Libc中的被加载的地址。这里用read,setbuf,甚至__libc_start_main函数也都可以,因为都导入了plt表和外部引用了。
4.开始构造泄露地址的第一段payload:
1 | #注释头 |
5.之后开始运行payload来实际得到Puts函数被libc加载的实际内存地址:
1 | #注释头 |
6.现在得到了puts函数被libc加载的实际内存地址,那么puts函数与其它函数的偏移量也就可以通过用IDA打开题目给的libc查出来,从而得到其它我们需要的函数被libc加载的实际内存地址。
1 | #注释头 |
得到libc被加载的首地址:puts_addr 减去 puts_in_libc 等于libc_start。于是libc_start加上各自函数对应的in_libc也就可以得到被libc加载的实际内存地址。
7.现在都有了就可以尝试在执行一次栈溢出来开shell,64位程序,有了system函数和binsh地址,那么栈溢出覆盖用pop rdi;ret的方法可以直接getshell。
8.这里假设没有binsh,来使用一下万能gadget:通过我们的输入读到内存中。同样这张图,万能Gadget1为loc_400616,万能Gadget2为loc_400600
以下为用来读取binsh字符串的代码,这里需要在程序中找到一段可以写入之后不会被程序自动修改的内存,也就是binsh_addr=0x60107c,这个地址其实是extern的地址,里面原来保存的内容是read函数发生延迟绑定之前的地址。而延迟绑定发生之后,got表中保存的内容已经被改成了被Libc加载的真实地址,这个extern也就没用了,可以随意用。但如果某个函数没有被首次调用,即还没发生延迟绑定,而我们却先一步改掉了extern的内容,那么它就再也没办法被调用了。
1 | #注释头 |
这里万能Gadget中给r12赋值,传入的一定是该函数的got表,因为这里的call和常规的call有点不太一样。我们在IDA调试时按下D转换成硬编码形式,(这里可以在IDA中选项-常规-反汇编-操作码字节数设置为8)可以看到这个call的硬编码是FF,而常规的call硬编码是E8。(这里call硬编码之后的字节代表的是合并程序段之前的偏移量,具体可以参考静态编译、动态编译、链接方面的知识)在这个指令集下面:
FF的call后面跟的是地址的地址。例如call [func], 跳转的地方就应该是func这个地址里保存的内容,也就是*func。
E8的call后面跟的是地址。例如call func,跳转的地方就是func的开头。
这里可以不用非得看硬编码,可以直接看汇编也可以显示出来:一个有[],一个没有[]。
9.所以万能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。(实际情况got和plt来回调着用呗,哪个好使用哪个)
10.那么现在有了system_addr和binsh_addr,而程序又是从最开始运行,所以现在尝试getshell:
1 | #注释头 |
11.另外由于在libc中查找也比较繁琐,所以有个libcSearch可以简化使用,具体查资料吧。
▲
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查也可以
▲mov和lea区别:
mov:对于变量,加不加[]都表示取值;对于寄存器而言,无[]表示取值,有[]表示取地址。
lea:对于变量,其后面的有无[]皆可,都表示取变量地址,相当于指针。对于寄存器而言,无[]表示取地址,有[]表示取值。
参考资料:
https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157