• CTF-Pwn丨栈溢出入门题目思路解析


    Pwn作为CTF比赛中的必考题型,用一句话概括就是难以入门。

    Pwn确实难,难点主要在于知识点不系统、考点太高深、对新手不友好。

    其实只要我们多做多练,了解出题套路,掌握答题思路,还是能够快速入门的。

    i 春秋论坛作家「SkYe231」表哥总结了几种Pwn入门栈溢出题目的解题思路,旨在为大家提供更多的学习方法与技能技巧,文章仅供学习参考。

    PS:栈溢出入门题目可能不局限下面总结的,诸如利用栈溢出修改局部变量触发某些条件getshell等没有总结到位,感兴趣的小伙伴可以在i春秋官网进行系统学习。

    Web端看课体验更佳,看课地址:https://www.ichunqiu.com/newRelease/webaqgcs

    CTF-Pwn丨栈溢出入门题目思路解析

     

    ip根据题目位数不同分别代指eip或rip ,bp及sp同理。以下小结均是基于程序为动态链接。

    完整后门&溢出长度大于等于ip


    非常基础入门题目,用于了解学习栈溢出控制寄存器ip ,关键代码如下:

    int vul()
    {
      char s; // [esp+Ch] [ebp-6Ch]
      puts("input:");
      gets(&s); //栈溢出漏洞,溢出长度不限
      return puts("OK,Bye!");
    }

    通过分析找到后门函数,且这个后门是一个完整可用的后门,就是只要我们调用这个后门函数,不需要其他额外的操作就能getshell,通常是system('/bin/sh'),write(1,'flag',32)等形式。

    int getshell()
    {
      return system("/bin/sh");
    }

    思路:

    有完整可用后门的情况下,只要溢出长度能够覆盖ip即可。直接利用栈溢出将getshell函数地址写入到eip位置,结束当前函数后就会去运行getshell函数payload结构:

    payload = [填充] + [get_shell_addr]

    残缺后门&溢出长度远大于ip


    栈溢出漏洞函数如下(和上一个小结一样):

    int vul()
    {
      char s; // [esp+Ch] [ebp-6Ch]
      puts("input:");
      gets(&s); //栈溢出漏洞,溢出长度不限
      return puts("OK,Bye!");
    }

    这里指的远大于ip,意思是可以溢出覆盖除了ip以外的3个以上的(一般情况,实际情况实际分析)机器字长(32位4 bit;64位8bit)。

    残缺后门的形式多种多样:提供诸如system等关键函数;flag、/bin/sh等字符串;某些gadget等。这里就整理前两种情况,刚好对应上面的完整后门。

    提供关键函数&字符串

    一般给出的是system函数,在ida中一般呈现形式:

    int getshell()
    {
      return system("echo no no no!!!");
    }

    这样就能通过system.plt调用system函数。

    字符串/bin/sh可以在ida中字符串界面可以查询到(shift+f12),存放在bss区,在源码中通过定义变量来提供。通过地址来调用这个字符串参数。

    思路:

    对比完整后门发现这里是将函数和参数分割,但是两者都在程序中,所以可以通过栈溢出自行构造完整后门,这里就涉及传参方式方面知识:32位栈传参;64位前6个参数寄存器传参,后续更多参数遵循栈传参。

    自行构造完整后门(调用链)对溢出长度有要求了,至少要能溢出除ip以外再多2个机器字长,用于写入调用步骤。

    32位程序:

    payload=[填充]+[system@plt]+[4bit填充]+[参数]

    64位程序:

    64位系统寄存器传参需要用到gadget,可以使用ROPgadget查找程序乃至libc库(需要泄露基地址)。一般来说前两个参数rdi rsi的gadget比较常见容易得到的。

    payload=[填充]+[pop_rdi_ret]+[参数]+[system@plt]

    提供关键函数

    关键函数不单单指的system、puts、read、write等我们用于写入读取的辅助函数一定程度上也算是关键函数,但是为了方便总结这里的关键函数指的是system这类函数。

    system函数在ida中呈现方式同上小结。与上面小结相比缺少的是字符串/bin/sh,对比完整后门来说就只剩下system。

    这里结合上面小结对比缺少一个/bin/sh,那就先构造一个输入的利用链将字符串输入到内存,然后再和上面一样构造system利用链,这里就涉及ROP利用思想。system利用链不再重复,与上面小结一致。输入利用链构根据题目出现输入函数不同而实际分析,一般用的都是read,也有scanf、gets 。

    根据溢出长度不同还能细分为两种情况:一次性利用链;ROP利用链。

    一次性利用链

    这种情况适用于溢出长度比较大,一般能有7个机器字长左右,具体情况具体分析。这种利用的思想是用输入函数read将/bin/sh写入到bss段后调用system('/bin/sh') 。

    32位程序:

    payload=[填充]+[read@plt]+[system@plt]+[参数1]+[binsh写入地址]+[参数3]+[binsh写入地址]

    64位程序很少用,因为寄存器传参需要用到gadget,这样会导致需要非常大的溢出空间,构造起来和32位思想一样不再重复。

    ROP利用链

    和一次性利用链思想上最大区别就是运行完自构写入函数后,返回main(或其他)函数,相当于让程序重新运行一次,有第二次利用漏洞机会,写入getshell利用链。

    相比之下优势:payload长度变短,有效应对溢出长度不足情况。

    32位程序:

    payload1=[填充]+[read@plt]+[main]+[参数1]+[binsh写入地址]+[参数3]
    payload2=[填充]+[system@plt]+[4bit填充]+[binsh写入地址]

    64位程序:

    pop_rdi都会有单独的gadget,rsi多数以pop_rsi_r15形式出现,rdx少有gadget,这里假设gadget情况如下,实际做题实际调整:

    • pop_rdi_ret
    • pop_rsi_r15_ret
    • pop_rdx_ret
    payload1=[填充]+[pop_rdi]+[参数1]+[pop_rdi_r15]+[binsh写入地址]+[8bit填充]+[pop_rdx]+[参数3]+[read@plt]
    payload2=[填充]+[pop_rdi]+[binsh写入地址]+[system@plt]

    可以看到payload1还是长的,且rdx gadget不一定有。这是可以尝试不传第三个参数,用寄存器rdx的原值,这个值有可能符合需要(若需要控制前三个参数了解一下ret2csu),payload简化如下:

    payload1=[填充]+[pop_rdi]+[参数1]+[pop_rdi_r15]+[binsh写入地址]+[8bit填充]+[read@plt]
    payload2=[填充]+[pop_rdi]+[binsh写入地址]+[system@plt]

    提供字符串

    就是给/bin/sh,其实这样等于没给,需要用到的函数都需要去libc中找,等同于没有后门,故这里不小结,思路和下面无后门一样。

     

    无后门&溢出长度远大于ip


    从这种题目开始就接近实际题目了。首先是无后门,也就是既没有system也没有/bin/sh等条件,只有一个栈溢出的漏洞。这个小结中溢出长度定义是远大于ip,就继续沿用上面的漏洞函数gets,实际中更多的是用read函数输入。

    int vul()
    {
      char s; // [esp+Ch] [ebp-6Ch]
      puts("input:");
      gets(&s); //栈溢出漏洞,溢出长度不限
      return puts("OK,Bye!");
    }

    思路:

    既然程序中没有system就到libc中找,也就是需要泄露libc基地址。方法是泄露一个位于libc中的函数的真实地址,将真实地址减去函数偏移得到基地址。/bin/sh也能在libc中找到不再赘述。

    泄露地址需要一个(程序中出现过的)输入函数,常见的有puts、write等。泄露完成后需要第二次利用漏洞的机会,所以也就是涉及ROP利用思想。

    32位程序:

    puts

    payload1=[填充]+[puts@plt]+[main]+[某函数@got]
    payload2=[填充]+[system@libc_addr]+[4bit填充]+[binsh@libc_addr]

    write

    payload1=[填充]+[puts@plt]+[main]+[某函数@got]
    payload2=[填充]+[system@libc_addr]+[4bit填充]+[binsh@libc_addr]

    64位程序:

    puts

    payload1=[填充]+[pop_rdi]+[某函数@got]+[puts@plt]+[main]
    payload2=[填充]+[pop_rdi]+[binsh@libc_addr]+[system@libc_addr]

    write

    稳妥来说需要控制3个寄存器:write(1,[输出地址],[输出长度]) 。如果找不到rdx的gadget和[上面小结](#ROP利用链)一样尝试看看rdx原值是否符合需求,如果要不满足就得用ret2csu技巧控制前三个寄存器。

     

    无后门&溢出长度等于ip


    溢出只能够刚好覆盖完ip就不能再溢出更多字节,显然是不能写入完整的利用链,这种情况可以考虑栈迁移。栈迁移就是将原本写在ip后面的利用链写到其他地方,通过控制bp、ip将栈迁移到利用链的位置,运行提前布置的利用链。

    首先这种题目会有一到两次输入的机会,输入内容可能会是局部变量或者全部变量(bss段)。栈迁移需要明确的迁移地址,基于存放位置不同难度也不同:

    全局变量

    写到bss段就比较简单了,迁移地址直接到ida查全局变量地址即可。

    局部变量

    局部变量在栈上,栈地址是动态的,所以要先泄露栈地址才能进行栈迁移,但是也有用sub esp,xx这种神奇gadget将栈抬高的操作。如果用迁移到栈上一般会有两次输入一次,一次是泄露栈地址,一次是写入利用链。

    需要注意一点:假如写入地址和迁移地址为addr,有效利用链需要从addr+一个机器周期开始写入,具体自行调试查看sp变化。

    思路

    为了便于总结假设一个32位程序有两次输入,第一次向全局变量var_1(地址为:addr)写入,第二次向局部变量s写入。

    // 32位程序
    int vul()
    {
      char s; // [esp+Ch] [ebp-6Ch]
      puts("input:");
      gets(&var_1); //栈溢出漏洞,溢出长度不限
      puts("input:");
      read(0,&s,0x74);  //栈溢出漏洞,溢出长度有限
      return puts("OK,Bye!");
    }

    提前用ROPgadget找到leave;ret gadget,然后构造payload如下:

    payload1='a'*4+p32(puts@plt)+p32(main)+p32(puts@got)
    payload2='a'*0x6c+p32(addr)+p32(leave;ret)

    泄露地址后返回main函数,第二次运行程序。后续getshell可以用onegadget或者system(/bin/sh)。溢出ip为onegadget就能getshell。system(/bin/sh)需要再次栈迁移。

    onegadget

    payload3=[任意内容]
    payload4='a'*0x6c+'a'*4+p32(onegadget)

    system(/bin/sh)

    payload3='a'*4+p32(system@libc_addr)+p32(main)+p32(binsh@libc_addr)
    payload4='a'*0x6c+p32(addr)+p32(leave;ret)

    对于32位程序如果输出函数只有write影响不大,而64位程序如果输出函数只有write没有puts等少参数的函数,可能会比较棘手,原因还是在于第三个参数rdx缺少gadget,需要实际调试查看rdx的原值是否需求,如果不符合需要赋值就要ret2csu,将csu利用链写到迁移地址上。

     

    总结


    这里归纳了几种入门题目常见套路,但是套路远远不局限于上文,比如栈溢出修改局部变量,绕过PIE保护等等,后续会分享相关内容,大家敬请关注。

  • 相关阅读:
    SegmentFault 巨献 1024 程序猿游戏「红岸的呼唤」第二天任务攻略
    POJ 2263 Heavy Cargo(ZOJ 1952)
    EEPlat 主子表和对象引用配置实例
    tornado websocket
    android android:duplicateParentState="true" "false"
    掌握4个有效的数据分析要点,切实解决用户痛点
    掌握4个有效的数据分析要点,切实解决用户痛点
    没有基础,如何成为数据分析师?
    没有基础,如何成为数据分析师?
    数据的无量纲化处理和标准化处理的区别是什么
  • 原文地址:https://www.cnblogs.com/ichunqiu/p/15476743.html
Copyright © 2020-2023  润新知