攻防世界逆向高手题之notsequence
继续开启全栈梦想之逆向之旅~
这题是攻防世界逆向高手题的notsequence
这道题,算是我真正接触逆向算法的题目了,有点兴奋。
.
.
下载附件,照例扔入exeinfope中查看信息:
.
无壳,32位ELF文件,照例扔入32位IDA中查看伪代码,有Main函数看main函数:
.
.
题型是与用户输入有关的生成型flag,逻辑是经过两个check,分析第一个check函数v2 = sub_80486CD(&unk_8049BE0):
这里一开始我没看懂,按照反向逆向逻辑来看双重循环的话我得知道v5的值,但是这里并没有,所以我在主函数处发现了v2=20,但是又不确定v2在第二个check函数里有没有改变过,结果是没有。所以v5=v2=20,有了v5的值就可以进行反向第一个check中双层循环中的 for ( j = 0; j <= v5; ++j )循环了。
.
.
可是之后我还是逆向不了,我知道v3的个数和v5一样,可是v3 += *(_DWORD *)(4 * (j + i) + input_flag)这句代码标识v3是在input_flag中跳着取的啊,那这个逻辑逆向起来就相当麻烦了,我不会。(哭~)
.
.
查了资料才发现这是杨辉三角,算法逆向题,没办法,只能跟着wp走了,并附上我自己的见解。
.
.
首先这里积累第一个经验,附上杨辉三角解析:
[1] #0 /1 |2^0=1
[1, 1] #1 /2 |2^1=2
[1, 2, 1] #3 /3 |2^2=4
[1, 3, 3, 1] #6 /4 |2^3=8
[1, 4, 6, 4, 1] #10 /5 |2^4=16
[1, 5, 10, 10, 5, 1]
[1, 6, 15, 20, 15, 6, 1]
[1, 7, 21, 35, 35, 21, 7, 1]
[1, 8, 28, 56, 70, 56, 28, 8, 1]
[1, 9, 36, 84, 126, 126, 84, 36, 9, 1]
[1, 10, 45, 120, 210, 252, 210, 120, 45, 10, 1]
.
.
这样来看杨辉三角第一批特征:
(1)最左边代表行号,0就是第0行。(行号从0开始)
(2)第1个字符在数组中的位置,#后的数字。
(3)一行的有几个数字 /后的内容。
(4)整行的和。|后的内容,是2的行号次方(行号得从0开始)
.
.
.
.
这样来看杨辉三角第二批特征:
第n行数字的和为2^(n) ,行号从0开始
1=2^(0-0), 1+1=2^(1-0), 1+2+1=2^(2-0), 1+3+3+1=2^(3-0) ,1+4+6+4+1=2^(4-0), 1+5+10+10+5+1=2^(5-0)。
.
.
斜线上数字的和等于其向左(从左上方到右下方的斜线),拐角上的数字。(在图中以用红线标好)
1+1=2,1+1+1=3,1+1+1+1=4,1+2=3,1+2+3=6,1+2+3+4=10,1+3=4,1+3+6=10,1+4=5
.
.
.
接下来分析check1函数代码:
int __cdecl sub_80486CD(int input_flag)
{
int j; // [esp+0h] [ebp-14h]
int v3; // [esp+4h] [ebp-10h]
int i; // [esp+8h] [ebp-Ch]
int v5; // [esp+Ch] [ebp-8h]
v5 = 0; //这里积累第二个经验:通过v5的0、1、2、3……然后退出循环中 i 表达式的前几个值0、1、3、6、10……可以发现问题,因为这不是遍历或者有规律的遍历(每次检查第四个),而且v5 * (v5 + 1) / 2 是等差数列的公式,结合前面的逻辑逆向麻烦性,由此要知道考的是算法。
for ( i = 0; i <= 1024 && *(_DWORD *)(4 * i + input_flag); i = v5 * (v5 + 1) / 2 )
{
v3 = 0;
for ( j = 0; j <= v5; ++j ) //这里积累第三个经验:在杨辉三角那里,每一行的数的总和等于2的以该行号的次方,行号从0开始算起。
v3 += *(_DWORD *)(4 * (j + i) + input_flag);
if ( 1 << v5 != v3 ) // 所以这里v5是行上数的个数,这里1 << v5就表示2的v5次方,就是2的行号次方(从0开始)。 v3 += *(_DWORD *)(4 * (j + i) + input_flag)中i是v5行前的杨辉三角的个数,因为我们是一维排列杨辉三角的,所以只能用(4 * (j + i)这种表达式来遍历第v5上的v5+1个数(杨辉三角行从0开始!)
return -1;
++v5;
}
return v5;
}
所以这段代码check1函数的作用是检测每一行求和结果为2`k
可以抽象成一个二维结构,有[k] 行(第一行k=0),每行开头为第k*(k+1)/2个数。
.
.
.
接着分析check2函数的代码:
int __cdecl sub_8048783(int input_flag, int k_20)
{
int v3; // [esp+10h] [ebp-10h]
int v4; // [esp+14h] [ebp-Ch]
int i; // [esp+18h] [ebp-8h]
int v6; // [esp+1Ch] [ebp-4h]
v6 = 0;
for ( i = 1; i < k_20; ++i ) //这里i总0、1、2……这样连续递增
{
v4 = 0;
v3 = i - 1; //这里v3从0、1、2、这样连续递增,
if ( !*(_DWORD *)(4 * i + input_flag) )
return 0;
while ( k_20 - 1 > v3 )
{
v4 += *(_DWORD *)(4 * (v3 * (v3 + 1) / 2 + v6) + input_flag); //这里积累第四个经验:这里等差数列表达式v3 * (v3 + 1) / 2前面说过了,是杨辉三角的行,v6从0开始递增,表示取杨辉三角v3行的v6列的值,而在这个循环中v3是变换的,也就是取得杨辉三角的行是变化的,而v6在此一个该循环中是固定的,所以可以看成是取每一行(v3)的同一个列(v6)
++v3;
}
if ( *(_DWORD *)(4 * (v3 * (v3 + 1) / 2 + i) + input_flag) != v4 ) //这里由于前面循环++v3后表明行号向下了一行,而 i 从1开始,v6从0开始,所以 i 永远比 v6大1。所以这里可以看作[0]-[k-1]行的 [v6] 列求和等于[k]行的 [i]
return 0;
++v6;
}
return 1;
}
这里check2函数验证的就是杨辉三角的这个特征:
.
.
.
所以答案很明显了,是杨辉三角的前20行就是答案,这里积累第5个经验,写杨辉三角生成脚本:(代码标注很详细了,希望对自己日后有帮助!)
def triangles():
s=[1] #这里s[1]作为杨辉三角函数起始值
while True: #无限循环生成杨辉三角
yield s #每次返回一行的杨辉三角列表
s.append(0) #给杨辉三角下一列扩充一个数的空间,因为每一行比上一行多1个
s=[s[i-1]+s[i] for i in range(len(s))] #覆盖生成杨辉三角行列表,满足杨辉三角的下一行的第n个数等于上一行的第n和n-1的和
n=0 #设置计数器,因为只打印前20行
flag=''
for i in triangles(): #每次获取从triangels函数的yield返回的一行列表
#print(i) #打印每一行杨辉三角
flag+=''.join(map(str,i)) #返回通过指定字符连接序列中元素后生成的新字符串,以str为间隔,默认为逗号。而列表就是逗号间隔的
n+=1
if n==20:
break
import hashlib
flag=hashlib.md5(flag.encode()).hexdigest() #这里把flag的列表流变成了字节流,就去掉了列表保留了每个元素了,然后直接加密
print("RCTF{"+flag+"}")
.
.
结果:
.
.
总结:
1:
首先这里积累第一个经验,附上杨辉三角解析:
[1] #0 /1 |2^0=1
[1, 1] #1 /2 |2^1=2
[1, 2, 1] #3 /3 |2^2=4
[1, 3, 3, 1] #6 /4 |2^3=8
[1, 4, 6, 4, 1] #10 /5 |2^4=16
[1, 5, 10, 10, 5, 1]
[1, 6, 15, 20, 15, 6, 1]
[1, 7, 21, 35, 35, 21, 7, 1]
[1, 8, 28, 56, 70, 56, 28, 8, 1]
[1, 9, 36, 84, 126, 126, 84, 36, 9, 1]
[1, 10, 45, 120, 210, 252, 210, 120, 45, 10, 1]
.
.
这样来看杨辉三角第一批特征:
(1)最左边代表行号,1就是第1行。
(2)第1个字符在数组中的位置,#后的数字。
(3)一行的有几个数字 /后的内容。
(4)整行的和。|后的内容,是2的行号次方(行号得从0开始)
.
.
.
.
这样来看杨辉三角第二批特征:
第n行数字的和为2^(n-1) 。1=2^(1-1), 1+1=2^(2-1), 1+2+1=2^(3-1), 1+3+3+1=2^(4-1) ,1+4+6+4+1=2^(5-1), 1+5+10+10+5+1=2^(6-1)。
.
.
斜线上数字的和等于其向左(从左上方到右下方的斜线)或向右拐弯(从右上方到左下方的斜线),拐角上的数字。(在图中以用红线标好)
1+1=2,1+1+1=3,1+1+1+1=4,1+2=3,1+2+3=6,1+2+3+4=10,1+3=4,1+3+6=10,1+4=5
2:
这里积累第二个经验:通过v5的0、1、2、3……然后退出循环中 i 表达式的前几个值0、1、3、6、10……可以发现问题,因为这不是遍历或者有规律的遍历(每次检查第四个),而且v5 * (v5 + 1) / 2 是等差数列的公式,结合前面的逻辑逆向麻烦性,由此要知道考的是算法。
for ( i = 0; i <= 1024 && *(_DWORD *)(4 * i + input_flag); i = v5 * (v5 + 1) / 2 )
3:
这里积累第三个经验:在杨辉三角那里,每一行的数的总和等于2的以该行号的次方,行号从0开始算起。 所以这里v5是行上数的个数,这里1 << v5就表示2的v5次方,就是2的行号次方(从0开始)。 v3 += *(_DWORD *)(4 * (j + i) + input_flag)中i是v5行前的杨辉三角的个数,因为我们是一维排列杨辉三角的,所以只能用(4 * (j + i)这种表达式来遍历第v5上的v5+1个数(杨辉三角行从0开始!)
4:
v4 += *(_DWORD *)(4 * (v3 * (v3 + 1) / 2 + v6) + input_flag); //这里积累第四个经验:这里等差数列表达式v3 * (v3 + 1) / 2前面说过了,是杨辉三角的行,v6从0开始递增,表示取杨辉三角v3行的v6列的值,而在这个循环中v3是变换的,也就是取得杨辉三角的行是变化的,而v6在此一个该循环中是固定的,所以可以看成是取每一行(v3)的同一个列(v6)。
.
if ( *(_DWORD *)(4 * (v3 * (v3 + 1) / 2 + i) + input_flag) != v4 ) //这里由于前面循环++v3后表明行号向下了一行,而 i 从1开始,v6从0开始,所以 i 永远比 v6大1。所以这里可以看作[0]-[k-1]行的 [v6] 列求和等于[k]行的 [i]
5: 这里积累第5个经验,写杨辉三角生成脚本:(代码标注很详细了,希望对自己日后有帮助!)
`
def triangles():
s=[1] #这里s[1]作为杨辉三角函数起始值
while True: #无限循环生成杨辉三角
yield s #每次返回一行的杨辉三角列表
s.append(0) #给杨辉三角下一列扩充一个数的空间,因为每一行比上一行多1个
s=[s[i-1]+s[i] for i in range(len(s))] #覆盖生成杨辉三角行列表,满足杨辉三角的下一行的第n个数等于上一行的第n和n-1的和
n=0 #设置计数器,因为只打印前20行
flag=’’
for i in triangles(): #每次获取从triangels函数的yield返回的一行列表
#print(i) #打印每一行杨辉三角
flag+=’’.join(map(str,i)) #返回通过指定字符连接序列中元素后生成的新字符串,以str为间隔,默认为逗号。而列表就是逗号间隔的
n+=1
if n==20:
break
import hashlib
flag=hashlib.md5(flag.encode()).hexdigest() #这里把flag的列表流变成了字节流,就去掉了列表保留了每个元素了,然后直接加密
print(“RCTF{”+flag+"}")
解毕!敬礼!