2020 网鼎杯_signal

0x00 前言

找了一道好久之前网鼎杯的逆向题,说实话,这是我第一次见虚拟机的逆向

也是搞了好久才做出了,写篇博客纪念一下o( ̄▽ ̄)o

现在已经深夜了,害怕第二天就忘了,所以赶紧记下来

还有刚刚考完操作系统,我裂开了…….


已经第二天了,刚刚写出脚本,回来接着写

说实话第一次见虚拟机的题目还挺有新鲜感的

不过真正吃透一道题也不容易(还是自己太菜)

本题三种解法我了解的差不多了,当作笔记好好记录一下


0x01 程序分析

主函数:

1
2
3
4
5
6
7
8
9
10
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [esp+18h] [ebp-1D4h]

__main();
qmemcpy(&v4, &unk_403040, 0x1C8u);
vm_operad(&v4, 114);
puts("good,The answer format is:flag {}");
return 0;
}

不管别的,把&unk_403040的值扣下来再说

主函数做的是先把&unk_403040的值赋给&v4,然后进入vm_operad函数

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
int __cdecl vm_operad(int *a1, int a2)
{
int result; // eax
char v3[100]; // [esp+13h] [ebp-E5h]
char v4[100]; // [esp+77h] [ebp-81h]
char v5; // [esp+DBh] [ebp-1Dh]
int v6; // [esp+DCh] [ebp-1Ch]
int v7; // [esp+E0h] [ebp-18h]
int v8; // [esp+E4h] [ebp-14h]
int v9; // [esp+E8h] [ebp-10h]
int v10; // [esp+ECh] [ebp-Ch]

v10 = 0;
v9 = 0;
v8 = 0;
v7 = 0;
v6 = 0;
while ( 1 )
{
result = v10;
if ( v10 >= a2 )
return result;
switch ( a1[v10] )
{
case 1:
v4[v7] = v5;
++v10;
++v7;
++v9;
break;
case 2:
v5 = a1[v10 + 1] + v3[v9];
v10 += 2;
break;
case 3:
v5 = v3[v9] - LOBYTE(a1[v10 + 1]);
v10 += 2;
break;
case 4:
v5 = a1[v10 + 1] ^ v3[v9];
v10 += 2;
break;
case 5:
v5 = a1[v10 + 1] * v3[v9];
v10 += 2;
break;
case 6:
++v10;
break;
case 7:
if ( v4[v8] != a1[v10 + 1] )
{
printf("what a shame...");
exit(0);
}
++v8;
v10 += 2;
break;
case 8:
v3[v6] = v5;
++v10;
++v6;
break;
case 10:
read(v3);
++v10;
break;
case 11:
v5 = v3[v9] - 1;
++v10;
break;
case 12:
v5 = v3[v9] + 1;
++v10;
break;
default:
continue;
}
}
}

第一次见,感觉这么多数一定很麻烦,但其实如果理解了其中的操作并不难

粗略的看一下,有 what a shame 这样的字符串,所以应该是要比较字符

再仔细分析程序,switch 这里用的参数是 a1(0x403040的值),v10 相当于是索引,从0开始递增,

进而遍历 0x403040之后的所有数(四字节为单位)。

所以说 0x403040 这里面储存的都是操作数,然后一个一个取出来,找相应的case进行操作。

##特别要注意的是case7,操作码最后部分的07H都是进行比较用的

image-20200614135000672


接下来只要根据相应的操作数一步一步分析,

首先通过分析知道,a1是存入的操作数,v3是你输入的字符,v4,v5用于暂存数据

1
2
3
4
5
6
7
8
9
10
11
12
13
case1:进行赋值操作,将v5的值赋给v4,标号索引全加1  
//后面可以知道v4内数据用来作最后的比较
case2:v3与a1的下一个值相加,索引加2(跳过用于运算的数值)
case3:v3减去a1的下一个值,索引加2
case4:a1的下一个数值与v3异或,索引加2
case5:a1下一个数值与v3相乘,索引加2
case6:空操作
case7:v4的数值分别与a1的下一个数值进行比较,全正确则通过
case8:相当于一个复制操作,保存当前值
case9:刚发现没有case9
case10:输入15位数值,放在v3中
case11:v3-1
case12:v3+1

了解了全部操作,那么就可以分析它的实现流程了

0x02 实现流程

对照着0x403040的数据可以一步一步摸清它的脉络

首先第一个操作码是10,是输入15位数据(也就是要求的flag)

下一个操作码是4,flag[0]与a1的下一个数值(16)进行异或,索引加2

下一个操作码是8,保存当前状态。

下一个操作码是3,当前值减去a1下一个数据(5)

下一个操作码是1,将现在的数据赋值给v4,各个索引均加1

进行对下一位flag[1]的运算操作

所以可以从中找出规律,对于每一位数据的操作都结束于case1,也就是说,程序是将flag[0~14]这15位数据依次作运算,存入v4中,最后将v4的数值分别与对应的数值进行比较


0x03 脚本

1.手撕程序

那么可以动笔来算一下,写的有点乱,无伤大雅,勉强能冲

image-20200614133839058

由于题目的位数不多,逻辑不难,所以手撕还是挺快(乐)的,贴上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
print (chr((34+5)^16),end='')
print (chr((63//3)^32),end='')
print (chr(52+2+1),end='')
print (chr((50^4)-1),end='')
print (chr((114+33)//3),end='')
print (chr(51+1+1),end='')
print (chr((24+32)^9),end='')
print (chr((167^36)-81),end='')
print (chr(49+1-1),end='')
print (chr((241-37)//2),end='')
print (chr((40^65)-54),end='')
print (chr(132-32),end='')
print (chr((193-37)//3),end='')
print (chr((30+32)^9),end='')
print (chr(122-1-65))

##疯狂print,虽然看上去很low,但简单粗暴,效率很高

2.高级脚本

基本的逆向思维

image-20200614145058127

找出v4的值,分别反向做逆运算就可以找出flag

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
v4 = [34,63,52,50,114,51,24,167,49,241,40,132,193,30,122]
v4.reverse()
a=[10, 4, 16, 8, 3, 5, 1, 4, 32, 8, 5, 3, 1, 3, 2, 8, 11, 1, 12, 8, 4, 4, 1, 5, 3, 8, 3, 33, 1, 11, 8, 11, 1, 4, 9, 8, 3, 32, 1, 2, 81, 8, 4, 36, 1, 12, 8, 11, 1, 5, 2, 8, 2, 37, 1, 2, 54, 8, 4, 65, 1, 2, 32, 8, 5, 1, 1, 5, 3, 8, 2, 37, 1, 4, 9, 8, 3, 32, 1, 2, 65, 8, 12, 1]
a.reverse()
v9=0
v3=0#flag
v5=0
flag=[]
final=''
for i in range(0,len(a)):
if i ==len(a)-1:
flag.append(v3)
if a[i]==1 and a[i-1]!=1:
v5 = v4[v9]
v9+=1
flag.append(v3)
if a[i]==2:
v3 = v5 - a[i-1]
if a[i]==3:
v3 = v5 + a[i-1]
if a[i]==4:
v3 = v5^a[i-1]
if a[i]==5:
v3 = int(v5/a[i-1])
if a[i]==8:
v5 = v3
if a[i]==11:
v3 = v5+1
if a[i]==12:
v3 = v5-1
flag.reverse()
for j in flag:
final +=chr(j)
print (final)
#看上去很高级,但....有这个时间手算不香吗

为了写的通俗易懂,脚本可能会存在考虑不周的地方,不过解出本题还是可以的

附上队里一位Han Xu小姐姐的脚本,比赛时就解出来是真的强,

(厚脸皮要来的脚本,学习一下😁)

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
v1=[0xa,4,0x10,8,3,5,1,4,0x20,8,5,3,1,3,2,8,0xb,
1,0xc,8,4,4,1,5,3,8,3,0x21,1,0xb,0x8,0xb,1,4,9,8,3,
0x20,1,2,0x51,8,4,0x24,1,0xc,8,0xb,1,5,2,8,2,0x25,
1,2,0x36,8,4,0x41,1,2,0x20,8,5,1,5,3,8,2,0x25,
1,4,9,8,3,0x20,1,2,0x41,8,0xc,1,7,0x22,7,0x3f,7,
0x34,7,0x32,7,0x72,7,0x33,7,0x18,7,0xa7,
7,0x31,7,0xf1,7,0x28,7,0x84,
7,0xc1,7,0x1e,7,0x7a]
print(len(v1))
v4=[0]*15
v3=[0]*15
v7=0
v9=15
v6=14
v4=[122, 30, 193, 132, 40, 241, 49, 167, 24, 51, 114, 50, 52, 63, 34]
print(len(v4))
for i in range(len(v1)-1,-1,-1):
if(v1[i]==1):
v5=v4[v7]
v7+=1
v9-=1
if(v1[i]==12):
v3[v9]=v5-1
if(v1[i]==11):
v3[v9]=v5+1
if(v1[i]==8):
v5=v3[v6]
v6-=1
if(v1[i]==2):
v3[v9]=v5-v1[i+1]
if(v1[i]==3):
v3[v9]=v5+v1[i+1]
if(v1[i]==4 ):
v3[v9]=v5^v1[i+1]
if(v1[i]==5):
v3[v9]=v5//v1[i+1]
else :
continue
print(v4)
print(v3)
print ("".join(chr(i) for i in v3))

3.Angr

angr也叫符号执行暴力破解,用于解CTF题目,emmm实战中应该作用不大

这一块是我的知识盲区,直接贴上学习资源1学习资源2学习资源3…….学习资源N,先记下来,以后慢慢看(咕咕咕)

贴上大佬们的脚本

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
import angr

p = angr.Project('./signal.exe') #指定angr跑的程序
state = p.factory.entry_state() #新建一个SimState的对象,得到一个初始化到二进制入口函数的SimState对象。
simgr = p.factory.simgr(state) #创建simulation manager,angr的主要入口

simgr.explore(find=0x004017A5 ,avoid=0x004016E6) #争取跑到输出成功的地址,避免跑到输出wrong的地址
flag = simgr.found[0].posix.dumps(0)[:15] #得到flag
print(flag)

--------------------------------------------------------------------------------
#ideits小姐姐的脚本
import angr
p = angr.Project('./signal.exe')
state = p.factory.entry_state()
sm = p.factory.simulation_manager(state)
def good(state):
return b"good" in state.posix.dumps(1)

def bad(state):
return b"what" in state.posix.dumps(1)
sm.explore(find = good, avoid = bad)
if sm.found:
find_state = sm.found[0]
flag = find_state.posix.dumps(0)
print(flag)

运行结果:

image-20200614160910068


0x04 小结

这可能是我赛后复现过的第一题,还是时隔了这么久。。。

之前总感觉要从基础学起,从入门题慢慢进阶,后来发现进步速度实在缓慢,

小结做一下简单的反思

去翻一下炜哥的博客,发现他每次赛后都会写一篇博客复现一下当时没解出来的题目,

我们英语课经常会讨论一些逆向的话题,他说你必须要逼着自己做一些难题,大家一开始都不会,就是通过每一次复现题目来强制自己学习新的知识,否则会一直停留在入门水平。

大师,我顿悟了

我目前的状态确实不像想学习的态度,比如说比赛题目只要没见过、或者很复杂,我很难静下心来解题,一般这种情况我会直接关掉网页,然后安慰自己,嗯…这题不是给我做的。

逆向大佬子洋、炜哥遇到巨难的题目会慢慢地啃,就算一道题目花费几天的时间,真正搞懂原理和逻辑也是值得的,说实话,这道signal题难度远不及他们的博客的平均水平,可以说是最简单的虚拟机题目(炜哥比赛手撕半个小时解决),虽然花了几个小时才搞懂,但还是很有成就感的

张老师说过比赛和题目都不是关键,关键是你能从中学到什么,分清主次,明确学习规划才是当前最重要的

好好学逆向,争取比赛不再只是去纠结MISC

立flag:今后比赛无论pwn还是逆向,一定要抽出时间复现1-2道赛题

哦对,下一步学习动调,不会吧不会吧,不会真的有re手不会动调吧?

应该不会吧