软件安全 – 若水斋 https://blog.werner.wiki Try harder Mon, 29 Jul 2019 09:29:22 +0000 zh-Hans hourly 1 https://wordpress.org/?v=6.8.3 https://blog.werner.wiki/wp-content/uploads/2018/11/cropped-ql1-1-32x32.jpg 软件安全 – 若水斋 https://blog.werner.wiki 32 32 意外发现一个溢出漏洞 https://blog.werner.wiki/accidentally-found-an-overflow-vulnerability/ https://blog.werner.wiki/accidentally-found-an-overflow-vulnerability/#comments Mon, 15 Aug 2016 12:06:42 +0000 http://blog.werner.wiki/?p=136 昨天下午在用OllyDbg分析sdemo2.0 缓冲区溢出漏洞时不小心将大约几千个字符”A”粘贴进了Command,OllyDbg随即运行出错, 点调试后惊奇地发现EIP被覆盖为41414141。又测试了几次,确定OllyDbg的插件CmdBar存在溢出漏洞。我的第一个漏洞就这样在意外中发现了!

一、漏洞重现

实验环境:Windows7旗舰版 Service Pack 1,OllyDbg1.10(汉化第二版)以兼容Windows Xp SP3 方式运行。WindowsXP下运行OllyDbg也有此漏洞。

复制粘贴250个字符“A”到Command中就会出现上图所示的错误提示,点击调试后会发现EIP被覆盖为0x41414141,如下图所示:

这是很典型的溢出漏洞。改变字符串长度,会发现当字符串长度小于等于238时不会有错误产生,当字符串长度大于等于250时EIP被覆盖为0x41414141,字符串长度为239到250时,错误各不相同,总结为下表:

“A”的个数 EIP 错误提示
<239 无错误
239 0xC02F0001 访问违规:正在执行[C02F0001]
240 0x00000000 访问违规:正在执行[00000000]
241 0x04CA2DFA 访问违规:读取[00002E36]
242 0x05062DFA 访问违规:读取[002E2234]
243 0x05062D00 访问违规:正在写入到[00000001]
244 0x0439002E 访问违规:正在写入到[00000001]
245 0x03002E22 访问违规:正在执行[03002E22]
246 0x002E2220 访问违规:正在执行[002E2220]
247 0x2E222041 访问违规:正在执行[2E222041]
248 0x22204141 访问违规:正在执行[22204141]
249 0x20414141 访问违规:正在执行[20414141]
≥250 0x41414141 访问违规:正在执行[41414141]

二、漏洞产生原因分析

来用OllyDbg调试OllyDbg,找出产生漏洞的代码。程序出错后选择调试,在栈中可以看到大量的“41”,以及一个注释——“CmdBar.05172C90”。(上图中是“CmdBar.03CF2C90”,但这次调试中是“CmdBar.05172C90”,这个值每次都可能不一样。)既如此,就把断点设在这里好了。试着运行一下,发现中断过于频繁,又注意到0x05172C90是一个函数的开始,它的前面是另一个函数,尝试将断点设在这个函数中,就设在0x05172C5F处好了。重启OllyDbg,打开OllyDbg.exe,按F9运行,再在0x05172C5F处设置断点,然后向被调试的OllyDbg的Command中复制粘贴超长字符串(250个”A”),程序中断:

可以看到<jmp.&USER32.SetWindowTextA>是要设置某个窗口的提示字符串。先不管它,继续按F8单步运行,直到retn:

可以看到返回地址被41414141覆盖,更准确的说是被第247、第248、第249和第250个”A”覆盖。这就显示出了用250个字符测试的好处,若太长,覆盖了栈中的“CmdBar.05172C60”的话,就没这么好调试了。但到目前为止只找到了EIP被劫持为41414141的关键指令retn,还没有找到产生漏洞的罪魁祸首。

向上翻阅反汇编代码,发现有一个repne指令和两个rep指令,在这三个指令处下断点,如下图所示:

重新来过,程序果然停在了repne处:

REPNE SCAS BYTE PTR ES:[EDI]
扫描 ES:[EDI]字符串中与 AL的值相同的字符,遇到相同的后停止
参考:REPNE SCAS BYTE PTR ES:[EDI] 指令详解

紧接着执行NOT ECX后ECX的值便是字符串长度0xFB(十进制的251,250个”A”和一个截止符)。继续单步运行,依次执行完两次rep,却发现返回地址并没有被覆盖,这一点与预想不同。继续按F8单步运行,当执行完<jmp.&USER32.wsprintfA>后,发现返回地址被覆盖。

问题出在这里:在调用函数wsprintfA时未检查参数字符串的长度,所以导致溢出。原来会复制一段内存的函数不仅有strcpy,strncpy和memcpy,还有wsprintfA这样的函数。

再检查一次吧。删除其他所有断点,仅在调用wsprintfA处下断点,重新来过,粘贴250个”A”后,程序停在了调用函数wsprintfA处,如下图所示:

按一下F8执行完wsprintfA后,位于0x0018F14C处的函数返回地址已经被覆盖:

可以确定,确实是调用函数wsprintfA时未检查参数字符串的长度导致的溢出漏洞。至此,这个漏洞的产生原因分析完成。下一步本该是写出攻击代码,但我想先写写总结。如果要写攻击代码,会在另一片博文中展示。

三、总结

这篇文章阅读起来大约只需十几分钟,但我却花了超过8个小时才完成它。中途走了很多弯路,刚开始想,既然是溢出漏洞,一定涉及到字符串的复制,在给strcpy和strncpy函数都下断点无果后,寻找rep指令,给所有rep指令下断点,耐心调试后发现

    - rep -  movs dword ptr es:[edi], dword ptr [esi]

将过长的字符串复制到了栈中。按下Shift+F2,在该处下条件断点:[ESI]==41414141。当向被调试的OllyDbg的Command中复制过长的字符串后,程序会停在刚刚设置的条件断点处。这时查看调用堆栈(Alt+K),发现调用的是memcpy函数,而不是strcpy或strncpy。用bp memcpy来设置新的断点,但用鼠标一点Commamd中断就会发生,无法向Command中复制超长字符串,且不知为何,设置条件断点也没能成功。所以换个思路,超长字符串是复制粘贴进去的,所以用bp GetClipboardData来设置断点。

重启OllyDbg,打开OllyDbg.exe,按F9运行,再用bp GetClipboardData设置断点,然后向被调试的OllyDbg的Command中复制粘贴超长字符串,程序被中断在GetClipboardData函数的第一条指令处,之后按Ctrl+F9运行到返回,可以看到此时EAX指向超长字符串,然后用bp memcpy设置断点,再按F9运行程序,如此反复尝试,也没有什么结果。

又想设置内存断点,但不知何种原因,内存访问断点是可以正常工作的,但内存写入断点总是无效的。尝试了好多后只能放弃。山穷水尽之时忽然看到“CmdBar.05172C60”,这才柳暗花明。

灵活多变的思路很重要,一路不通,则换一路,兵者,诡道也。经验也很重要,知道都有哪些路可走,甚至创造出新的路来。

明显的感觉到,自己在看完《0day安全:软件漏洞分析技术》后,对于简单的漏洞原理都是掌握了的,但真的拿到一个庞杂的软件,想从中定位几行有漏洞的指令,还是很困难的。我缺乏的,是调试的知识和经验。

四、看雪论坛的OllyDbg系列教程

  1. CCDebuger的OllyDBG入门教学
  2. OllyDBG技巧汇集
  3. OD脚本教学
]]>
https://blog.werner.wiki/accidentally-found-an-overflow-vulnerability/feed/ 2
OllyDbg中的常用断点 https://blog.werner.wiki/common-breakpoints-in-ollydbg/ https://blog.werner.wiki/common-breakpoints-in-ollydbg/#respond Sat, 06 Aug 2016 12:04:01 +0000 http://blog.werner.wiki/?p=133 将《0day安全:软件漏洞分析技术》第5篇 漏洞案例分析 第25章 漏洞分析技术概述 表25-2-4 OllyDbg中的常用断点 整理在此以方便查询:

常用断点类别 命令 说明
拦截窗口 bp CreateWindowExA/W 创建窗口
bp ShowWindow 显示窗口
bp UpdateWindow 更新窗口
bp GetWindowTextA/W 获取窗口文本
拦截消息框 bp MessageBoxA/W 创建消息框
bp MessageBoxExA/W 创建消息框
bp MessageBoxIndirectA/W 创建定制消息框
拦截对话框 bp DialogBoxParamA/W 创建模态对话框
bp DialogBoxIndirectParamA/W 创建模态对话框
bp CreateDialogParamA/W 创建非模态对话框
bp CreateDialogIndirectParamA/W 创建非模态对话框
bp GetDlgItemTextA/W 获取对话框文本
bp GetDlgItemInt 获取对话框整数值
拦截剪贴板 bp GetClipboardData 获取粘贴板数据
拦截注册表 bp RegOpenKeyA/W 打开子健
bp RegOpenKeyExA/W 打开子健
bp RegQueryValueA/W 查找子健
bp RegQueryValueExA/W 查找子健
bp RegSetValueA/W 设置子健
bp RegSetValueExA/W 设置子健
功能限制拦截断点 bp EnableMenuItem 禁止或允许菜单项
bp EnableWindow 禁止或允许窗口
拦截时间 bp GetLocalTime 获取本地时间
bp GetSystemTime 获取系统时间
bp GetFileTime 获取文件时间
bp GetTickCount 获得自系统成功启动以来所经历的毫秒数
bp GetCurrentTime 获取当前时间(16位)
bp SetTimer 创建定时器
bp TimerProc 定时器超时回调函数
拦截文件 bp CreateFileA/W 创建或打开文件
bp OpenFile 打开文件
bp ReadFile 读文件
bp WriteFile 写文件
bp GetModuleFileNameA/W 获取当前模块路径
bp GetFileSize 获取文件大小
bp SetFilePointer 设置文件指针
bp FindFirstFileA/W 搜索文件
bp FindFirstFileExA/W 搜索文件
拦截驱动器 bp GetDriveTypeA/W 获取磁盘驱动器类型
bp GetLogicalDrives 获取逻辑驱动器符号
bp GetLogicalDriveStringsA/W 获取当前所有逻辑驱动器的根驱动器路径
]]>
https://blog.werner.wiki/common-breakpoints-in-ollydbg/feed/ 0
内存攻击小结 https://blog.werner.wiki/memory-attack-summary/ https://blog.werner.wiki/memory-attack-summary/#respond Sun, 31 Jul 2016 12:01:16 +0000 http://blog.werner.wiki/?p=129 我花了大约一个月的时间看了《0day安全:软件漏洞分析技术》的第 1 篇:漏洞利用原理(初级)和第 2 篇:漏洞利用原理(高级)。这部分主要讲解了Windows平台下的各种内存攻击技术,从简单的栈溢出到绕过GS、DEP等高级防护措施。内容庞杂而深刻,觉得一时难以消化,故在此进行总结。

依我自己的理解,按目的,将内存攻击分为两大类,第一类是以修改关键指令(书中实验1.4),关键变量(书中实验2.2)为最终目的的内存攻击,第二类是以控制EIP(32位机)为终极目标的内存攻击。第一类相对简单易懂,且往往无法执行shellcode(但并不意味着危害更小),故不详细讨论,主要总结第二类内存攻击。

CPU根据指令寄存器EIP的值来执行相应位置的指令,控制了EIP,便控制了程序流程。第二类内存攻击技术,以控制EIP,执行shellcode为目标,为达成这一目标,分两步走:

第一步:修改内存值

修改内存值的方法便是栈溢出,堆溢出等。堆溢出又可细分为二:连续覆盖(溢出)⇒ 精确打击(DWORD SHOOT)。无论是栈溢出,堆溢出还是其他修改内存值的方式,都是在为第二步做准备。

第二步:控制EIP

通过修改特定位置的内存值,借助某些特殊指令、机制,便可以控制EIP。书中提到的控制EIP的方法有:

  • 函数返回地址(实验2.4,4.6,10.5,12.3,12.4)
  • 函数指针(实验6.3,10.3)
  • 异常处理S.E.H(实验6.1,10.4,11.4,11.5,11.6,15.3,15.4)
  • 进程环境块P.E.B(实验5.4)
]]>
https://blog.werner.wiki/memory-attack-summary/feed/ 0
没有VC如何用CodeBlocks运行shellcode https://blog.werner.wiki/run-shellcode-with-codeblocks/ https://blog.werner.wiki/run-shellcode-with-codeblocks/#respond Thu, 21 Jul 2016 11:58:57 +0000 http://blog.werner.wiki/?p=127 在《0day安全:软件漏洞分析技术(第二版)》的第3章中,作者给出了一种便捷运行shellcode的方法:

    char shellcode[]="\x66\x81\xEC\x40\x04\x33\xDB......";//欲调试的十六进制机器码
    void main()
    {
        __asm
        {
             lea    eax, shellcode
             push   eax
             ret
        }
    }

原书作者是用Visual C++6.0编译代码的,操作系统是Windows XP SP2。我在Windows XP SP3下用CodeBlocks编译如上代码时则会出错,因为Visual C++6.0中的编译器和CodeBlocks中的默认编译器gcc关于内联汇编的语法有很大不同,可参考64位ubuntu中c与intel汇编混合编程之C语言内联汇编,虽然这篇文章是介绍Linux环境下的,但gcc编译器C语言内嵌汇编的语法在Windows下和Linux下是一样的。简单尝试后得到如下可行代码:

    int main() {
        char popup_general[]=
            "\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1"
            "\x4F\x68\x32\x74\x91\x0C\x8B\xF4\x8D\x7E"
            "\x0C\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33"
            "\x32\x53\x68\x75\x73\x65\x72\x54\x33\xD2"
            "\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B\x49\x1C"
            "\x57\x56\x8B\x69\x08\x8B\x79\x20\x8B\x09"
            "\x66\x39\x57\x18\x75\xF2\x5E\x5F\xAD\x3D"
            "\x6A\x0A\x38\x1E\x75\x05\x95\xFF\x57\xF8"
            "\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03"
            "\xCD\x8B\x59\x20\x03\xDD\x33\xFF\x47\x8B"
            "\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A\xC4"
            "\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1"
            "\x3B\x54\x24\x1C\x75\xE4\x8B\x59\x24\x03"
            "\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD"
            "\x03\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A"
            "\x0A\x38\x1E\x75\xA9\x33\xDB\x53\x68\x57"
            "\x21\x21\x21\x68\x5A\x5A\x5A\x47\x8B\xC4"
            "\x53\x50\x50\x53\xFF\x57\xFC\x53\xFF\x57"
            "\xF8";
        char *p=popup_general;
        // the asm code:
        asm (
             "push  %0;\n"
             "ret;"
              :"=r"(p)
              :
              :
            );
        return 0;
    }

编译命令是:

    gcc -masm=intel shellcode.c -o shellcode

参数-masm=intel表明内联汇编采用intel语法,而不是gcc默认的AT&T语法。内嵌汇编中的%0指的便是第一个参数p,gcc编译时会把p保存到某个通用寄存器中(关键字r指明这一点),故可以直接push %0,之后紧接着ret指令将EIP寄存器的值修改为刚刚压入栈中的值,即p的值,这样shellcode便会紧接着运行。这段shellcode会弹出一个消息框,之后正常退出,运行结果如下图所示:

补充说明一下gcc编译器的参数问题,你可以在codeblocks中通过图形化界面设置参数,也可以和我一样找到gcc所在的路径,设置环境变量,然后在cmd命令行窗口中手动输入整个编译命令。我的机器上gcc所在的目录是:C:\Program Files\CodeBlocks\MINGW\bin\。嫌每次都输命令麻烦可以写一个简单的批处理shellcode.bat:

    gcc -masm=intel shellcode.c -o shellcode
    pause

(2016年8月3日更新)

在exploit-db看到shellcode是这样运行的:

    #include<stdio.h>
    char shellcode[]=
            "\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
            "\x8B\xF4\x8D\x7E\x0C\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
            "\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
            "\x49\x1C\x57\x56\x8B\x69\x08\x8B\x79\x20\x8B\x09\x66\x39\x57\x18"
            "\x75\xF2\x5E\x5F\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95\xFF\x57\xF8"
            "\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59\x20\x03\xDD"
            "\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A\xC4\x74\x08"
            "\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75\xE4\x8B\x59"
            "\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03\x2C\xBB\x95"
            "\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB\x53\x68\x57"
            "\x21\x21\x21\x68\x5A\x5A\x5A\x47\x8B\xC4\x53\x50\x50\x53\xFF\x57"
            "\xFC\x53\xFF\x57\xF8"//181
            ;
    main()
    {
        (* (int(*)()) shellcode)();
    }

我的理解如下: int()() 是一个函数指针,该函数的参数为空,返回值为int型,( int()() ) shellcode 是强制类型转换,将原本为char [ ]的变量转换为函数指针,* ( int(*)() shellcode )取出该函数,最后的()表示函数调用。这样不用内联汇编,便可以执行shellcode,果然是人外有人,天外有天。

]]>
https://blog.werner.wiki/run-shellcode-with-codeblocks/feed/ 0
64位Linux下的栈溢出 https://blog.werner.wiki/stack-overflow-under-64-linux/ https://blog.werner.wiki/stack-overflow-under-64-linux/#respond Thu, 30 Jun 2016 11:50:07 +0000 http://blog.werner.wiki/?p=125 今天在看《捉虫日记》,其中讲解的Linux下的栈缓冲区溢出是32位机器的,和我的64位机器有些不同之处。下面是我在64位Linux (Ubuntu14) 下的栈缓冲区溢出实验的记录。

首先是有溢出漏洞的程序,这个程序来自于《捉虫日记》。

    #include<string.h>
    void overflow(char *arg){
        char buf[12];
        strcpy(buf, arg);
    }
    int main(int argc, char *argv[]){
        if(argc==2)
           overflow(argv[1]);
        return 0;
    }

禁用堆栈保护编译

gcc -m64 stackoverflow.c -o stackoverflow -z execstack -fno-stack-protector

生成汇编文件

gcc -m64 stackoverflow.c -S -masm=intel -o stackoverflow.s -z execstack -fno-stack-protector

生成的.stackoverflow.s文件内容如下所示

        .file   "stackoverflow.c"
        .intel_syntax noprefix
        .text
        .globl  overflow
        .type   overflow, @function
overflow:
.LFB0:
        .cfi_startproc
        push    rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        mov     rbp, rsp
        .cfi_def_cfa_register 6
        sub     rsp, 32
        mov     QWORD PTR [rbp-24], rdi
        mov     rdx, QWORD PTR [rbp-24]
        lea     rax, [rbp-16]
        mov     rsi, rdx
        mov     rdi, rax
        call    strcpy
        leave
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE0:
        .size   overflow, .-overflow
        .globl  main
        .type   main, @function
main:
.LFB1:
        .cfi_startproc
        push    rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        mov     rbp, rsp
        .cfi_def_cfa_register 6
        sub     rsp, 16
        mov     DWORD PTR [rbp-4], edi
        mov     QWORD PTR [rbp-16], rsi
        cmp     DWORD PTR [rbp-4], 2
        jne     .L3
        mov     rax, QWORD PTR [rbp-16]
        add     rax, 8
        mov     rax, QWORD PTR [rax]
        mov     rdi, rax
        call    overflow
.L3:
        mov     eax, 0
        leave
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE1:
        .size   main, .-main
        .ident  "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
        .section        .note.GNU-stack,"",@progbits

在主函数中,38、39行将命令行参数放入栈中,第40行比较第一个参数(argc)的值是否等于2,若不等于则跳转(第41行)到L3,随后退出,若等于2,则取第二个参数加8后的地址单元中的值存放到寄存器rdi中。(第二个参数argv是char **型的,加8是因为我们传给函数overflow的参数是argv数组中的第二个值argv[1],它是一个字符串,也就是指向字符的指针。)在64位机器中,参数的传递会使用寄存器而不是堆栈。处理好参数之后便调用函数:call overflow。

在 函数overflow中,先执行“push rbp,mov rbp, rsp”(9-12行)称之为序幕(prolog)工作,对应的有第21行的leave指令完成收尾(epilog)工作。第14行的“sub rsp, 32”为局部变量留出空间。15-19行的指令为调用函数strcpy准备好了参数,参数依旧存在寄存器中(rsi是arg,rdi是buf,都是字符指针)。由于内存只能以字为单位寻址,64位机器中一个字是8个字节,buf[12]占12个字节,所以需要两个字的存储空间,也就是16字节,所以17行“lea rax, [rbp–16]”中减去了16 (最接近rbp的空间是留给buf的,因为它在函数中最先定义)。运行到第17行时,堆栈如下图所示:

函数strcpy会将rdi的值当做指针,将其指向的字符串复制到buf中,从上图可以看出,buf的大小很有限,若超出其长度,则会覆盖掉老rbp,老rip,使函数无法正常返回。尝试运行程序:

当参数字符串的长度小于等于15时运行不会出错

./stackoverflow 0123456789abcde

当参数字符串的长度大于15时运行才会报错

./stackoverflow 0123456789abcdef
Segmentation fault (core dumped)

为何不是16?C语言中的字符串是以\0作为结束标志的,所以实际的存储空间会比可见的长度多一个字节。

由于64位机器中的rip被设计成前47位有效,当指定一个大于0x00007fffffffffff的地址时会抛出异常,所以经典的0x4141414141414141无法实现。那就让我们把rip变成0x0000414141414141吧。

用gdb调试程序(-q使得gdb不输出gdb程序的版本等信息):

gdb -q ./stackoverflow

然后在gdb中输入命令

run $(python -c "print 'A'*30')

以30个大写字母A作为输入参数,其中16个用于填满buf,8个用于覆盖“老rbp”,最后6个用于覆盖rip。一运行就会看到这样的信息

Program received signal SIGSEGV, Segmentation fault.
0x0000414141414141 in ?? ()

可见rip的确被覆盖为了0x0000414141414141。若是想将rip覆盖为0x00007fffffffffff,则需要修改参数为

run $(python -c "print 'A'*24+'\xff'*5+'\x7f'")

运行结果为

Program received signal SIGSEGV, Segmentation fault.
0x00007fffffffffff in ?? ()
]]>
https://blog.werner.wiki/stack-overflow-under-64-linux/feed/ 0