Lingze's blog Lingze's blog
timeline
about
friends
categories
tags

lingze

bin不是垃圾桶的意思!
timeline
about
friends
categories
tags
  • ctf_wp
lingze
2019-11-12

syc二面 2019

# syc二面 -2019

这是syc-19年二面题目。 以下为自己的报告, 二面结束后又了解到的关于这道题更多的信息算法是base58编码和c++的做法,目前先鸽,以后更新。 很不错的题目,学到了许多, 附赠文件:⬇⬇ ⬇⬇ 链接:https://pan.baidu.com/s/1aP3KaZFQD0P4T-xKn5n-jQ 提取码:1ahh 复制这段内容后打开百度网盘手机App,操作更方便哦

@[TOC]

以下是二面最后提交的报告。

# syc二面 -- 令则

本报告是题目完成后写成,步骤都由复现后写就,可能会有在所难免的细节的疏漏,但基本完全还原整个解题思路和遇到的问题及解决。并且将自己遇到的小的细节和具体的操作原理写了些。

有部分工作是动调和标注,报告忽略了这些繁琐的部分,写出了这些工作的思路,并写出了分析出来的程序逻辑。

# 题目分析-整体分析

拿到题目后解压, 首先注意到一个exe文件就是我们的题目,然后两个dll是环境配置,然后一个后缀jpg_encode文件,我们首先会了解这是一个加密的jpg文件,在文件内解压,flag极可能在里面, 然后检查题目文件,32位无壳exe文件,其实应该庆幸这样的还可以动调,肝也能有方向, 载入ida,观察主函数,这是个c++写的程序,伪代码的可读性不高。 我们发现字符串sorry和wait的位置,发现sorry以后会直接结束程序,而wait以后会处理第三方文件,那么这个判定的位置就是一个关键, 后面的一部分使用的c语法,可读性极高,我们观察到载入了我们的jgp_encode文件和另一个jgp文件,然后一个读取一个写入,我们需输入一个值,用于异或解密,得到最后的jgp文件。 这样我们有一个大概的想法,使用动调确定了下整个函数的流程,然后答题思路大致有了:

  • 首先我们要得到flag,应该第一个字符串str1输入正确,
  • 应该会在第一个字符串中提示第二个字符串str2,
  • 得到第二个字符串用于解密第三方文件,得到正确解密的end.jgp文件
  • 就是图片直接显示flag或者隐写flag。

# 逆向1-第一层逆向

由于c++伪代码的复杂,分析直接看的汇编, 工具: ida,特别是图形界面 od/x32dbg的动态调试, 其实在我的使用中差不多更推荐x32一些,双击后,在栈中可以偏移和地址一起观察到: X64 分析的两条主线:

  • 根据判定一步步逆向得到正确的数据和字符串,
  • 使用一串abcdefghijklmnopqr来正向了解程序中对字符串的处理。

# 逆向得到加密后字符串str2

首先从sorry字符串判定的位置入手: 在这里插入图片描述 我们看到是判定的是两个寄存器的值,我们要在这里下断点,,观察整个判定过程 在这里插入图片描述 图示中蓝色为数据的准备,橙色部分为判定,红色部分为错误法显示“sorry”并退出程序,绿色是整个循环的判定,判定字符串是否已经检测完, 注意循环计数器,这里为[ebp-20]; 审视判定的两个值,ebx和eax: 在这里插入图片描述 我们可以发现每次的ebx的值就是这个明文字符串s的按计数器选到的值。 审查eax的值,发现前面经历了一系列的计算,然后将这个值和[ebp-70]变量中储存的内存地址传参,调用一个函数,然后将储存eax中的函数返回值拿出来进行比较。

  • 计算部分,涉及到两个:循环计数器[ebp-20] 、已定义的变量[ebp-5c]形成的数组。
  • 函数调用,我们先观察[ebp-70]指向的地址,然后看到这里是一串字符串,观察函数调用后的内存、栈、寄存器,发现这个函数作用:按照参数的数值在指向的内存地址的字符串中选值,并返回这个值。

由此,我们大致可以猜测,这个[ebp-70]指向的地址就是加密完以后的字符,然后按照计算得到的数值选择出值,和明文的字符串s比较。 由此我们可以确定这个加密后字符串的长度,并得到这个加密后的字符串。 但还差一步,即这个计算出的值,我们可以找到[ebp-5c]和周围几个值组成的数组,配合循环计数器,通过汇编语言和动调分析,得到计算过程,并写出脚本得到这些值组成的列表,但里面出现了部分较复杂的汇编语句,我使用了下面更简单的方式。 我们进行动调,并记录下这些值,得到列表:

arr = [0x1,0x5,0x4,0x2,0x3,0x0,0x7,0xB,0xA,0x8,0x9,0x6,0xD,0x11,0x10,0xE,0xF,0xC]
1

我们可以知道,加密后的字符串str1的第arr[i]位等于s的第i位,可以得到str1:

 s = 'PrSDvCZirMmpygyGhb'
 arr = [0x1,0x5,0x4,0x2,0x3,0x0,0x7,0xB,0xA,0x8,0x9,0x6,0xD,0x11,0x10,0xE,0xF,0xC]
 v20 = [0]*18
 str1 = ''
 for i in range(18):
     v20[arr[i]] = s[i]
 for i in range(18):
     str1 += v20[i]
 print(str1)
1
2
3
4
5
6
7
8
9

如此我们得到了加密后的字符串str1:

str2 = 'CPDvSrpZMmribyGhyg'
1

下一步继续向上需要定位生成str2的关键加密部分,我们使用一个自己生成的字符串进行动调测试来进行寻找。 ###程序流程分析 在main函数最开始下断点,并一直跟踪main函数。 观察调用函数: *关注函数传入的参数,相对应的内存、栈中的变量和寄存器, 参数、参数指向的地址和寄存器一般就是函数作用到的所有内容了。

main函数的作用:(对照下图

  • 赋值了几个栈内地址,当作定义的局部变量(主要在绿色部分
  • 接收输入的字符串,并调用函数将输入的字符串赋值到内存的三处位置(主要在黄色部分和几个函数直接调用
  • 将内存地址3,内存地址3的最后一位字符,内存地址1分别传入一个lam函数,这个函数会将地址3的数值加密,形成我们得到的加密后的函数。(橙色框中的红色的断点位置
  • 然后进行判定,即我们首先关注并分析完成的sorry的判定位置。(后面紫色粉色部分
  • 判定成功进入第三方文件的处理部分。(蓝色部分 在这里插入图片描述

这样我们知道了整个加密的关键在于这个lam函数,进入分析: lam函数的作用:(对照下图

  • 首先处理数据赋值一些局部变量,其中有一部分函数的作用是清空栈内数据,一部分函数是将某变量指向的地址同样赋值给另一变量。(蓝色部分
  • 然后进入循环,共两个大循环,一个负责处理数值,一个使用处理出的数值根据一串字符串进行替换字符的加密。
  • 处理数值的大循环,内部嵌套一个小循环,每个字符会经历小循环,然后结束后进入大循环获取到下一个字符,计算的结果会保留在内存中。(大循环即绿色部分,小循环为黄色部分
  • 替换字符的循环,从内存中取出前面保留下的计算结果,然后得到字符串中的对应位,替换掉main函数内存地址3中的原字符串。替换完成的这个字符串也是main函数sorry判定部分判定的那个字符串。(紫色部分 在这里插入图片描述

我们分析得到了加密过程的程序流程。

# 字符测试text

由于是较为复杂且细节并没有太明确的程序,我们使用自己已知测试字符串进行测试,尝试正向实现和逆向求解加密过程,对比动调中的数据,得到正确的逆向脚本。 我们的测试字符串:

str3 = 'abcdefghijklmnopqr'
1

# 得到对应数据

先在main函数sorry判定位置下断点。找到变量[ebp-70]指向的内存地址,得到测试字符串str3加密后得到的字符串:

cipher = 'YDXEJ7jpxdNwq9Tj8c4HbeAs'
1

然后重新载入程序,我们在lam函数的生成字符串处下断点。 通过变量[ebp-64]查找到存放加密后数据的地址,并得到加密后的数据:

 arr = [0x02,0x0F,0x34,0x29,0x2C,0x0B,0x2E,0x16,0x38,0x1F,0x08,0x2F,0x13,0x35,0x0E,0x2E,0x25,0x31,0x30,0x05,0x10,0x3,0x32,0x22]
1

于是我们可以检测我们对程序逻辑的判断: 写出一个正向的测试脚本,从计算后的数据得到加密后的字符串:

arr = [0x02,0x0F,0x34,0x29,0x2C,0x0B,0x2E,0x16,0x38,0x1F,0x08,0x2F,0x13,0x35,0x0E,0x2E,0x25,0x31,0x30,0x05,0x10,0x3,0x32,0x22]
 base = 'vrYenHCzNgu7FRTDbLiqtBpQZoUS3f5dKWsaM8Gm1EyVJkjw4cA6X92Pxh0OLl+/'
 for i in range(len(arr)):
     print(base[arr[i]],end='')
 #YDXEJ7jpxdNwq9Tj8c4HbeAs
1
2
3
4
5

两个字符串相同,我们对这部分程序的判定是正确的。 **我们逆向这个算法:写出一个逆向的测试脚本,从加密后的字符串得到计算后的数据: **

cipher = 'YDXEJ7jpxdNwq9Tj8c4HbeAs'
 base = 'vrYenHCzNgu7FRTDbLiqtBpQZoUS3f5dKWsaM8Gm1EyVJkjw4cA6X92Pxh0OLl+/'
 arr = []
 for i in range(len(cipher)):
     arr.append(base.find(cipher[i]))
 print(arr)
1
2
3
4
5
6

得到的数据是正确的,我们这个逆向脚本没有问题,那么就可以得到正确的str2对应的数据: 正式逆向脚本,从正确的加密后字符串str2得到对应数据

arr:cipher = 'CPDvSrpZMmribyGhyg'
base = 'vrYenHCzNgu7FRTDbLiqtBpQZoUS3f5dKWsaM8Gm1EyVJkjw4cA6X92Pxh0OLl+/'
arr = []
for i in range(len(cipher)):
	arr.append(base.find(cipher[i]))
print(arr)
1
2
3
4
5
6

得到正确的加密数据:

arr = [6, 55, 15, 0, 27, 1, 22, 24, 36, 39, 1, 18, 16, 42, 38, 57, 42, 9]
1

# 得到正确字符串str1

我们进行动调,并分析整个数据处理流程: 在这里插入图片描述 整个数据处理的循环关键位置在这里:

  • 大循环:
  • 首先地址指针指向的内存中的字符取出赋值给[ebp-18]
  • 这里的字符就是输入的字符串的。
    • 进入小循环:
    • 根据循环计数器,将原来保存的值取出,左移八位,和[ebp-18]相加,
    • 将[ebp-18]和[ebp-38]相除(地板除),
    • 将结果继续赋值给[ebp-18]继续下一次计算,
    • 将余数保存到循环计数器对应的内存中,更新数据。
    • 直到[ebp-18]为0,跳出小循环。
  • 将地址指针向下移动,指向下一个输入的字符。

我们可以写出这样一个正向的数据处理脚本: 写出一个正向测试脚本,将输入的数值处理为最后的数据:

arr1 = [0] * 50
str1 = 'abcdefghijklmnopqr'
v18 = 0
v38 = 0x3a
eax = 0 
edx = 0 
j = 0
for i in range(len(str1)):
	v18 = ord(str1[i])
	while v18 :
		v18 += ( arr1[j] << 8)
		eax = v18 // v38 
		edx = v18 % v38
		arr1[j] = edx 
		v18 = eax 
		j += 1
	j = 0
for i in range(50):
	print(hex(arr1[i]),end=',')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

但要注意这个生成的是倒序的数组,因为内存中也是同样的生成方式,但内存中寻址进行字符替换的时候是倒序的。 我们得到正向算法,便可以写出逆向算法: 写出逆向测试脚本,从数据再生成字符串:

arr = [0x02,0x0F,0x34,0x29,0x2C,0x0B,0x2E,0x16,0x38,0x1F,0x08,0x2F,0x13,0x35,0x0E,0x2E,0x25,0x31,0x30,0x05,0x10,0x3,0x32,0x22]
v18 = 0 
b = 0x4 
c = 0x3a 
a = 0 
d = 0 
str1 = ''
v10 = 0 
for i in range(len(arr)):
	for j in range(v10 ,len(arr)):
		d = arr[j]
		a = b * c + d 
		b = a & 0xff 
		arr[j] = (a >> 8)
	str1 += chr (b)
	if arr[v10] == 0 :
		v10 += 1
		b = 0
print(str1[::-1])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

注意一些细节:

  • h3: b的初始值是在动态调试时得到的最后剩余的值,
  • 关于空值:
    • 一定要判定列表前面的值是否已经为0,
    • 程序运行时最开始全是0,而随着处理数据才慢慢有一个列表,
    • 我们逆向去处理了在正向中原本为0,后面又扩充的那些值,就会出现错误,
    • 所以我们应该进行判定是否为0,并在此后做出规避,如变量v10,其实在做完题目后还发现了python中的del语句,用在这个位置极为合适;
    • 我使用pycharm进行调试,记录数据,并在动调过程中记录数据,进行对比,发现要去除的空值出现在我的列表的前面,并一步步dbg,完善了整个脚本
    • 另一个点时当出现空值时,我们消去列表一位时,对应正向开创除列表这一位的时候,这一位开辟后,小循环结束,下一次进入大循环更新[ebp-18],也就是说这时候的[ebp-18]值为0,如h18
  • 另外我们逆向处理肯定是逆向求出的整个字符串,打印时直接逆序就好了,如h19

我们得到整个正确的逆向测试脚本,就可以得到正确的正式逆向脚本了: 正式逆向脚本,从对应数据arr还原出正确字符串str1:

arr = [6, 55, 15, 0, 27, 1, 22, 24, 36, 39, 1, 18, 16, 42, 38, 57, 42, 9]
v18 = 0 
b = 0
c = 0x3a 	
a = 0 
d = 0 
str1 = ''
v10 = 0 
for i in range(len(arr)):
	for j in range(v10 ,len(arr)):
		d = arr[j]
		a = b * c + d 
		b = a & 0xff 
		arr[j] = (a >> 8)
	str1 += chr (b)
	if arr[v10] == 0 :
		v10 += 1
		b = 0
print(str1[::-1])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

最后注意: *前面我们b的初始值是在程序中动调得到的,而如果这个未知字符串加密过程中最后也这样剩余一个值,我们除非猜到这个值,否则就是不可逆的算法。所以这个题目应该是没有剩余值,我们直接b赋初值为0。

最后的到正确的第一层字符串:

str1 = 'Syc_Syc_Syc!!'
1

# 得到第一层解答(回顾)

我们上面顺着分析得到了正确的字符串str1,整体回顾,脚本如下:

  • 从题目逻辑得到加密后字符串str2:
s = 'PrSDvCZirMmpygyGhb'
arr = [0x1,0x5,0x4,0x2,0x3,0x0,0x7,0xB,0xA,0x8,0x9,0x6,0xD,0x11,0x10,0xE,0xF,0xC]
v20 = [0]*18
str1 = ''
for i in range(18):
	v20[arr[i]] = s[i]
for i in range(18):
	print(v20[i],end='')
1
2
3
4
5
6
7
8
str2 = 'CPDvSrpZMmribyGhyg'
1

由str2得到数据处理后的列表arr:

cipher = 'CPDvSrpZMmribyGhyg'
base = 'vrYenHCzNgu7FRTDbLiqtBpQZoUS3f5dKWsaM8Gm1EyVJkjw4cA6X92Pxh0OLl+/'
arr = []
for i in range(len(cipher)):
	arr.append(base.find(cipher[i]))
print(arr)
1
2
3
4
5
6
arr = [6, 55, 15, 0, 27, 1, 22, 24, 36, 39, 1, 18, 16, 42, 38, 57, 42, 9]
1

由arr得到正确字符串str1:

arr = [6, 55, 15, 0, 27, 1, 22, 24, 36, 39, 1, 18, 16, 42, 38, 57, 42, 9]
v18 = 0 
b = 0
c = 0x3a 	
a = 0 
d = 0 
str1 = ''
v10 = 0 
for i in range(len(arr)):
	for j in range(v10 ,len(arr)):
		d = arr[j]
		a = b * c + d 
		b = a & 0xff 
		arr[j] = (a >> 8)
	str1 += chr (b)
	if arr[v10] == 0 :
		v10 += 1
		b = 0
print(str1[::-1])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
str1 = 'Syc_Syc_Syc!!'
1

# 逆向2-第二层逆向

其实这个第二层逆向我原来的思路是会在第一层得到的str1中提示,然后输入得到文件隐写,结果str1是个,,口号?(hhh 然后就有点蒙了,其实做完题以后师傅说题目有写:

在这里插入图片描述

其实就是得到一个syc标志,但是我一开始以为是要寻找一个密码,和syc有关,猜了大半个晚自习,最后试了下中午因为和str1无关而被否决的文件头的想法,结果得到了图片,原以为是彩蛋给师傅说了下,结果告诉我写报告吧,已经结束了。 这里是个审题的问题,但整体思路是差不多的,关键在于输入的那个值。

我们分析下程序,由于这里伪代码已经是c,可读性很高。 在这里插入图片描述 注意:

  • 输入的值,v29会拆分为两个值,而且可以看出是二进制前8位和后8位
  • 所以我们输入的v29是一个四位十六进制数!

然后其实是想到一个二进制文件的文件头都是固定的,这个文件头表示的它的文件类型,我们最后得到的是一个.jpg文件,我们在winhex中随便打开几个.jpg文件就可以得到这一类型的文件头,然后打开我们的第三方文件syclover.jpg_encode,也可以看到它的文件头部分。 我们根据程序逻辑写成脚本将每次应该异或的值打印出来:

arr1 = [0xFF,0xD8,0xFF,0xE0,0x00,0x10,0x4A,0x46,0x49,0x46,0x00,0x01,0x01,0x01,0x00]
arr2 = [0xE5,0x05,0xE5,0xCD,0x18,0x3D,0x52,0x73,0x4F,0x73,0x18,0x2E,0x17,0x2E,0x18]
arr3 = []
for i in range(len(arr1)):
	if i & 1 :
		arr3.append(hex(arr1[i] ^ (arr2[i] - 13)))
	else:
		arr3.append(hex(arr1[i] ^ (arr2[i] + 1)))
print(arr3)
1
2
3
4
5
6
7
8
9

最后的结果:

arr3 = ['0x19', '-0xe0', '0x19', '0x20', '0x19', '0x20', '0x19', '0x20', '0x19', '0x20', '0x19', '0x20', '0x19', '0x20', '0x19']
1

其实可以看到大概就是两个值,我们可以确定两个异或的值,并得到我们应该输入的第二个code:2019 然后解密得到最后的a symbol of syc如下:

# 得到结果

已设为桌面

在这里插入图片描述

# 题目心得

  • 首先是这个c++,的确是可读性不高,然后硬靠汇编写出来的题目,师傅说c++其实可以看,但自己还是经验不够,目前也在看关于重载等,希望看看这些增长一些c++的经验吧。
  • 在调试注解分析过程中对汇编熟悉了很多,特别对内存,栈,寄存器的观察更熟练了一些。
  • 关于加密的计算部分感觉还是很难得一部分,余数这样的算数加密的逆向还是有些难度,特别本题目中生成列表后列表内的值会出现0,然后还要控制不读取这一位,模仿正向算法中列表一点点的增加过程,逆向算法中保持列表一点点减少,还是很厉害的地方。
  • 第二层牵扯到文件头知识还是很棒的,虽然一开始没懂,不过解出来后对这样的题目设计思路还是挺惊叹。

# 二面经历

  • 二面题目在19号下午放出,晚上稍微看了看,有一个大致思路。
  • 20号几乎一天都在处理事情,没弄题目。
  • 21--22,肝了两天的汇编部分, 捋清了整个函数流程,加密算法。
  • 23号处理完了最后数据处理部分,写出来了正向的脚本,并开始尝试逆向这个算法。
  • 24号中午完成了逆向的算法,得到第一个字符串str1。晚上晚自习得到了三叶草的图片,做出来整个题目。
  • 25号完成报告的主体部分,后来陆续做出了补充。
#wp#reverse
上次更新: 2/18/2021, 2:22:06 PM
Theme by Vdoing | Copyright © 2019-2021 lingze | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式