Geek-10h-re-wp

geek-10h—wp-re

这是Syclover组办的成信校内面向新生的geek大赛,很开心参与进感受到了这些题目的强大,也学到了好多好多,
往后会更新这一篇,因为已经看到了大佬们完全逆向的做法,我的这个题解还是有些连蒙带猜的元素,利用flag格式这样的做题技巧,等完成了其他方式的复现就整理更新。
题目分享链接:
⬇⬇⬇⬇
链接:https://pan.baidu.com/s/1BLjMdm9yVTPiYH-h-1r16A
提取码:0uqp
复制这段内容后打开百度网盘手机App,操作更方便哦

文章目录

  • geek-10h---wp-re
  • hello
  • 2333
  • Easy VB
  • 冰菓
  • re_py
  • dll_reverse
  • Win32 Progra
  • 你看阅兵了吗?
  • python1
  • python2
  • python3
      • 非预期解
      • 预期

hello

放入ida中,发现main函数中是铭文比较,查看字符串shift+f12
然后就查找到了flag
Geek-10h-re-wp_第1张图片

2333

载入ida,看到main函数的关键位置
Geek-10h-re-wp_第2张图片
其中base是加密函数,然后生成的字符串和cipher进行比较。base函数如下示:
Geek-10h-re-wp_第3张图片
其中我们看到a1[v4]&0xf和 a1[v4]>>4,这两个运算分别是取字符的前4位和后4位,我们知道若将C按此方式分别储存前四位在A和后四位在B中,只需要A*16+B即可复原。
由此写出脚本,运行得到flag:

 v6 = 0x3736353433323130
 a2 = '5379637B6E30775F794F755F6B6E6F775F6234736531367D'
 q = 0
 h = 0
 c = 0
 for i in range(0,len(a2),2):
     q = (int(a2[i],16) - v6) & 0xf
     h = (int(a2[i+1], 16) - v6) & 0xf
     c = q *16 + h 
     print(chr(c),end='')
     
 #Syc{n0w_yOu_know_b4se16}

Easy VB

这是一个vb编写的文件,使用ida打开会很混乱。
我们使用vb工具:vb decompiler
生成的代码已经具备了部分可读性,我们进行分析。
可以观察到几个关键位置:
(顺序是自下而上观察到的

  • loc_004023EF: Label2.Caption = “Sorry,you can try again.”
  • loc_004023C9: Label2.Caption = “Congratulation!You key word is true!”
  • loc_004023AC: If (var_74 <> “bKPObQ@goYBGRXjtVKVSn^@kFQh[V_]O”) <> 0 Then GoTo loc_004023D4
  • loc_00402355: var_74 = var_74 + Chr(CLng(var_54))
  • loc_004022BD: var_54 = CStr(Asc(ecx+esi*4) xor eax)
  • loc_0040213D: var_58 = “12345a789012345678g012345a789012”
    然后我们得到两个字符串和一个异或关系,尝试写脚本运行,得到了flag:
 a = "12345a789012345678g012345a789012"
 b = "bKPObQ@goYBGRXjtVKVSn^@kFQh[V_]O"
 for i in range(len(a)):
     print(chr(ord(a[i]) ^ ord(b[i])),end='')
    
 #Syc{W0w_Visual_Bas1c_ls_s0_cool}

冰菓

(非动漫玩家表示有点懵)
一直点一直点这个提示(我也不知道是哪位师傅的老婆)
会说到某个神器,然后去下载,并使用其分析这个程序。
这个工具52破解爱盘是有的。
Geek-10h-re-wp_第4张图片
左侧有一个mainwindow,
Geek-10h-re-wp_第5张图片
打开后发现关键部分,然后出现了加密函数,进入:
Geek-10h-re-wp_第6张图片
根据程序逆向写出脚本即可:

a = [119,77,103,79,21,115,133,97,115,87,22,115,103,89,88,93,22,89,119,81]
 b = 0 
 for i in range(20):
     b = (a[i] - 13) ^ 57
     print(chr(b),end='')
     
 #Syc{1_Am_s0_curi0uS}

re_py

得到.pyc文件就相当于得到了python文件的源码,
只需要简单的反编译即可,一般我喜欢使用在线工具
然后得到.py文件,分析源码:

 print 'This is a maze.'
 print 'Python is so easy.'
 print 'Plz Input The Shortest Way:'
 maze = '###########S#@@@@@@##@#@####@##@#@@@@#@##@####@#@##@@@@@@#@#########@##E######@##@@@@@@@@###########'
 way = raw_input()
 len = len(way)
 p = 11
 for i in way:
     if i == '&':
         p -= 10
     if i == '$':
         p += 10
     if i == '6':
         p -= 1
     if i == '3':
         p += 1
     if maze[p] == '#':
         print 'Your way is wrong'
         exit(0)
         break
     if maze[p] == '@':
         continue
     if maze[p] == 'E':
         print 'You do it,your flag is Syc\\{+Your Input+\\}.'
         exit(0)
 print 'May be something wrong.'

然后审查发现这是个地图题,其中四个字符表示上下左右,其中还可以看出迷宫宽度为10,然后#为墙,@是路,E是成功。
然后我们将地图打印下来,

 >>> maze = '###########S#@@@@@@##@#@####@##@#@@@@#@##@####@#@##@@@@@@#@#########@##E######@##@@@@@@@@###########'>>> for i in range(0,len(maze),10):
    print(maze[i:i+10])##########
#S#@@@@@@#
#@#@####@#
#@#@@@@#@#
#@####@#@#
#@@@@@@#@#
########@#
#E######@#
#@@@@@@@@#
##########

然后我们已经知道了路径,继续审查代码,和10相关会是上下,其中加为下,减为下,然后和1相关为左右,加为右,减为左。
于是得到flag。

dll_reverse

将exe文件载入ida中,发现主函数是将附赠的dll文件导入,然后调用了其中的TRicMx1r函数来判定输入的值的正确性。
Geek-10h-re-wp_第7张图片
将dll文件载入ida。分析关键函数:
Geek-10h-re-wp_第8张图片
然后我们观察关键函数:
Geek-10h-re-wp_第9张图片
前面部分为base64加密过程,且可以看到这是一个变表的base64:
在这里插入图片描述
后半部分为,数组间进行异或运算,并最终和另一数组进行比较:
Geek-10h-re-wp_第10张图片
我们使用ida内置python脚本打印出两个数组,并求得v14,但我们随即发现,v14并没有在arr3010中。这里思路断了,然后尝试动调,来观察下具体的运行。
我们使用动态调试,直接在异或位置下断,然后观察其中的值,就会发现,每次进行异或的v14其实就是加密以后的字符串,所以我们就可以直接写出脚本得到v14,这就是加密后的密文,然后转化为原表,在使用base64解密:

from base64 import *
 diy_base = 'ABCDEFGHIJKLMNOPQSVXZRWYTUeadbcfghijklmnopqrstuvwxyz0123456789+/'
 base = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
 arr3010 = [69, 106, 67, 52, 86, 59, 79, 103, 71, 67, 25, 35, 67, 117, 108, 103, 59, 101, 84, 70, 66, 55, 1, 80, 85, 96, 73, 36, 24, 74, 39, 31, 9, 29, 74, 0]
 arr3034 = [34, 89, 50, 94, 56, 11, 66, 86, 38, 112, 77, 69, 19, 34, 45, 29, 91, 55, 112, 3, 18, 96, 124, 54, 7, 83, 3, 83, 79, 120, 86, 38]
 v14 = 0
 v15 = 0
 s = ''
 flag = ''
 for i in range(32):
     v15 = arr3034[i]if i % 2:
         v14 = v15 ^ arr3010[i]
     else:
         v14 = (v15 - 3) ^ arr3010[i]
 ​
     s = chr(v14)
     flag += base[diy_base.find(s)]
 print (flag.decode("base64"))
 ​
 ​
 #Syc{Just_Easy_D1l_Cr0ck}

Win32 Progra

这个题目首先搜索字符串找到关键位置,在ida中使用shift+f12,然后找到提示正确或错误的位置:
Geek-10h-re-wp_第11张图片
我们可以看到这就是关键判定flag的位置,主要使一个函数的作用,但我们进入函数什么也没有发现,点击空格离开图形视图,然后一大堆的垃圾数据:
Geek-10h-re-wp_第12张图片
我们使用取消分析:单击U,和分析为代码:单击C,将这个部分转化为代码,并单击P将这一部分整理为一个函数。
Geek-10h-re-wp_第13张图片
而且我们还看到了这种数据,这就是混淆我们ida分析的数据,将其跳过,或这转化为字符串:单击A。
注意这样的操作还是用鼠标选中要操作的部分再点击快捷键,可以控制分析的位置。
这里还要注意关于栈帧的知识,这个可以帮助我们快速确定哪里属于一个函数。
最后整理好以后:
Geek-10h-re-wp_第14张图片
这里的伪代码会停在前面部分,关键的后部分没有。我们直接分析汇编。
我们分析这个程序,原来是将字符前18位分奇偶的进行异或,然后和我们最开始进入这个函数时出现过的那些数据进行比较,于是我们可以写出脚本,得到前18位flag:

arr = [0xCC,0x16,0x0FC,0x14,0x0AE,0x0E,0x0F2,0x30,0x0E8,0x5E,0x0F1,0x5C,0x0AD,0x30,0x0EF,0x1D,0x0AF,0x8,0x1C,0x40,0x19,0x7D]
 for i in range(18):
       if i % 2 :
             print(chr(arr[i] ^ 0x6f), end = '')
       else:
             print(chr(arr[i] ^ 0x9f), end = '')
 #Syc{1am_w1n32_pr0g	

其实我们到这里已经知道了flag了,因为题目就叫做Win32 Program剩下的四个字符,为:ram},
其实这里已经可以猜到,但是我们注意到程序后四位异或的两个值是运算多次的,但可以看到有两个值赋值为0,我们就直接四个值全为0尝试一下:

arr = [0xCC,0x16,0x0FC,0x14,0x0AE,0x0E,0x0F2,0x30,0x0E8,0x5E,0x0F1,0x5C,0x0AD,0x30,0x0EF,0x1D,0x0AF,0x8,0x1C,0x40,0x19,0x7D]
 for i in range(18):
       if i % 2 :
             print(chr(arr[i] ^ 0x6f), end = '')
       else:
             print(chr(arr[i] ^ 0x9f), end = '')
 arr2 = [0, 0 ,0,0]
 for i in range(4):
       print(chr(arr[18 + i] ^ arr2[i]), end = '')
 #Syc{1am_w1n32_pr0g•@•}

中间出现了两个乱码。
但是最后一位的确正确了,然后我们发现了@,其实flag内的替换是有可能a换成@的,我们都尝试了下,发现@的是对的。
得到:Syc{1am_w1n32_pr0gr@m}
其实这里取巧了,后来写wp和现在也想起来估计是花指令还是没处理干净所以有个jmp eax,断开了整个函数导致伪代码看不到。

你看阅兵了吗?

那道题目检查,无壳64为elf文件,载入ida
发现main函数就是判定前面几个问题的答案,然后什么也没发生,并没有给出flag,我们看到md5,然后另外还有一串密文,我们尝试解密,得到ACBAD,输入显示这是正确的第一层字答案,我们看到关键位置是判定是否为合格粉红的函数:
Geek-10h-re-wp_第15张图片
我们进入这个函数查看,这里进入后查看到这个函数进行的是一个较复杂的运算,然后我们知道我们要使这个函数返回非0 ,则找到了关键判定位置:
Geek-10h-re-wp_第16张图片
其实这里看到的返回正确前面还跟了一个函数,
我们进入后发现出现了大量的数据,然后使用异或,打印出了几个数的对应字符:
Geek-10h-re-wp_第17张图片
这里的关键在于putchar,我们在这里可以知道这个位置会打印出上面的数据和五个数进行异或后的值,我们分析其他的部分,发现其实都在判定:
Geek-10h-re-wp_第18张图片
尤其使这个位置,必须三个函数都返回1才会使返回值为1,这里还是很有趣的地方。
另外,这几个函数,其运算相似,都是在说明一种不等于关系,
而他们限制的值就是用来打印字符的函数的五个异或的数据的,因此我们猜想被花费心机计量的这个字符串,应该就是flag了。
在这里插入图片描述
注意这一部分,这个v3是被消掉了,其实异或的数只有数组以v2检索到的五个值,我们知道其实整个字符串之和五个值进行了异或,然后得到flag。
此时我们还应该想到,flag是有固定的部分的,即flag格式为:“Syc{”最后为“}”,且字符串总数为40,五个一组异或,我们知道前四位和最后一位,就可以的到这五个数据了:
Geek-10h-re-wp_第19张图片
我们最后得到脚本:

s = 'R{ezS1mYxkt]`0je]ri7^dj5c^Uc0gnoc^pn]Tdy'
arr = [1,2,6,1,4]
for i in range(len(s)):
	a = ord(s[i]) ^ arr [i % 5]
	print(chr(a),end='')
#Syc{W0o_you_f1nd_th3_fl4g_We1come_to_Re}

(后来师傅说数独有个多解要换表才知道这是一个非预期解法,这个程序的流程,中间多个不等于关系其实使表示了一个数独游戏。

python1

这是一个pyc文件,在线反编译得到py文件源码,如下:

import struct, time

def b(a):
    return a & 18446744073709551615L


def c(str):
    return struct.unpack(', str)[0]


def d(a):
    for i in range(64):
        a = a * 2
        if a > 18446744073709551615L:
            a = b(a)
            a = b(a ^ 12682219522899977907L)
    return a


if __name__ == '__main__':
    cmp_data = [
     7966260180038414229L, 16286944838295011030L, 8598951912044448753L, 7047634009948092561L, 7308282357635670895L]
    input = raw_input('plz input your flag:')
    if len(input) % 8 != 0:
        for i in range(8 - len(input) % 8):
            input += '\x00'

    arr = []
    for i in range(len(input) / 8):
        value = d(c(input[i * 8:i * 8 + 8]))
        arr.append(value)

    for i in range(5):
        if arr[i] != cmp_data[i]:
            print 'fail'
            time.sleep(5)
            exit()

    print 'success'
    time.sleep(5)
    exit()

首先看了下struct,看到了unpack和pack方法是相反的,后面的值都是参数标识格式,然后写一个测试脚本试下其作用:

import struct
var1 = struct.unpack(', 'abcdecgh')[0]
print(var1)
#7523090989672784481
print(hex(var1))
#0x6867636564636261L
str2 = struct.pack(",7523090989672784481)
print(str2)
#abcdecgh

那么我们明白函数c就是进行这个字符和数字互换,函数b进行异或,配合函数d进行算术加密操作,main函数中出现的三个循环,第一个调用函数c,返回转化后的数值,然后第二个循环进行算术加密,第三个循环比较确定正确与否。
我们逆向中,从第三个循环的到正确的加密后的数值,然后解密,最后使用pack就可以的到flahg。
分析加密的算法,并自己简化写出python脚本:

v1 = 18446744073709551615
v2 = 12682219522899977907
a = 7523090989672784481
for i in range(64):
	a *= 2
	if a > v1:
		a &= v1
		a = (a ^ v2) & v1
print(a)
#3375174038997573683

我们使用这个脚本将我们的测试字符串a~h对应的数据输入进去,然后得到加密后数据,我们动态单步走加密过程,发现一旦超过v1就会进行异或然后值减小了,其中v1的十六进制为0xffffffffffffffff,其中按位与运算舍去的数值,会一定是0x10000000000000000,我们逆向脚本就可以恢复这个按位与运算。
其次运算一直乘2,而且第一次进入的值一定不会超过v1,(v1十六进制为16个f,而八个字符unpack以后的值不会超过这个),所以第一次进入加密循环首先是乘2,在循环中的都会是偶数,
其次v2的十六进制为0xb0004b7679fa26b3,我们注意到这个值是奇数,一个偶数和一个奇数异或结果会是一个奇数,所以我们运行过程中从异或中得到一定都是奇数,
(这里说明,异或运算时按位,不同为1,奇数最后一位是1,偶数最后一位是0,异或一定是最低位为1,就是奇数。
我们是否要逆向还原异或的判断准则,就是这个数是否为奇数:

v1 = 18446744073709551615
v2 = 12682219522899977907
a = 3375174038997573683
v3 = 0x10000000000000000
for i in range(64):
	if  a % 2 :
		a ^= v2
		a += v3
	a //= 2
print(a)
#7523090989672784481

这样就成功还原了,我们将这个脚本改一下就可以得到正式脚本得到flag:

import struct
arr = [7966260180038414229, 16286944838295011030, 8598951912044448753, 7047634009948092561, 7308282357635670895]
flag = ''
v1 = 18446744073709551615
v2 = 12682219522899977907
v3 = 0x10000000000000000
for i in range(5):
	a = arr[i]
	for j in range(64):
		if  a % 2 :
			a ^= v2
			a += v3
		a //= 2
	flag += struct.pack(",a)
print(flag)
#Syc{L1fe_i5_sh0rt_y0u_n3ed_py7h0n}

python2

一道pyc的题目,得到py文件源码:

import struct, time

def fun(start, end, s):
    a = 32310901
    b = 1729
    c = s
    m = end - start
    while True:
        d = int((a * c + b) % m)
        yield d
        c = d


if __name__ == '__main__':
    arr = [
     77, 263, 394, 442, 463, 512, 667, 641, 804, 752, 885, 815, 1075, 1059, 1166, 1082, 1429, 1583, 1696, 1380,
     1987, 2263, 2128, 2277, 2387, 2670, 2692, 3255, 3116, 3306, 3132, 3659, 3139, 3422, 3600, 3584, 3343, 3546,
     3299, 3633, 3281, 3146, 2990, 2617, 2780, 2893, 2573, 2584, 2424, 2715, 2513, 2324, 2080, 2293, 2245, 2309,
     2036, 1944, 1931, 1817, 1483, 1372, 1087, 1221, 893, 785, 697, 586, 547, 324, 177, 184]
    flag = raw_input('plz input your flag:')
    length = len(flag)
    a = struct.unpack(', flag[length - 4:].encode())[0] & 255
    b = []
    c = fun(1, 255, a)
    for i in range(32):
        b.append(next(c))

    d = [ 0 for i in range(72) ]
    for i in range(length):
        for j in range(32):
            a = ord(flag[i]) ^ b[j]
            d[(i + j)] += a

    for i in range(len(d)):
        if d[i] != arr[i]:
            print 'fail'
            time.sleep(5)
            exit(0)

    print 'success'
    time.sleep(5)
    exit(0)

还是有使用的struct,我们单独拿出这几句来分析其作用:

import struct
flag = 'abcdefghijklmn'
length = len(flag)
a = struct.unpack(', flag[length - 4:].encode())[0] & 255
print(a)
print(ord(flag[-4]))

我们尝试多个字符串发现这串代码就只和字符串的倒数第四位有关,然后我们再尝试打印出来b,即出现yieldnext的部分:

import struct
def fun(start, end, s):
    a = 32310901
    b = 1729
    c = s
    m = end - start
    while True:
        d = int((a * c + b) % m)
        yield d
        c = d
flag = 'abcdefghijklmn'
length = len(flag)
a = struct.unpack(', flag[length - 4:].encode())[0] & 255
print(a)
print(ord(flag[-4]))
b = []
c = fun(1, 255, a)
for i in range(32):
    b.append(next(c))
print(b)

然后我们配合动调,发现两个特殊的位置作用是执行一段然后暂停返回mian函数,遇到next再继续执行一段,我们可以用以下的写法得到同样的效果:

b = []
c = a
for i in range(32):
    d = int((32310901 * c + 1729) % 254)
    b.append(d)
    c = d

我们继续就可以分析出来程序流程:

  • 首先选择除来flag倒数第四位,然后运算生成一个数组b,
  • flag每位和数组b异或的值,会累加到一个数组d中
  • 判定数组d和给出的数组arr是否相等。
    其实中间一个每位异或累加的部分,我们单独摘出:
for i in range(32):
	for j in range(41):
		print(i+j,end=',')

(解释下,这里使用41是因为数组d长度72,最大的检索为71,我们可以看到是会有重复的,
这样的累加法,逆向的计算量是极大的,题目也提示z3,我们使用z3就好了:
首先给出一个我自己整理的z3的模板:

from z3 import *

length = #设置下flag长度

solver = Solver()
flag = [Int("flag %d" %i) for i in range(length)]
#这里可以用Int,可以BitVec,后者可以异或运算,

#这里就是进行运算的位置。

solver.add()
#添加条件限制

#常用限制-可打印字符:
for i in range(length):
    solver.add(32 < flag[i] )
    solver.add(flag[i] < 127) 

#最后打印出falg
if solver.check() == sat:
	model = solver.model()
	s = [model[flag[i]].as_long() for i in range(length)]
	print ("".join(map(chr,s)))
else:
	print('unsat')

一般来说z3的题目,可以直接套上这个框架,照抄源码,让他自己解密,但是这个题我们也不知道flag长度,尝试使用爆破方法,将整个z3的flag设定以后全部放到for循环内,
但是并没有得到结果,我们开始考虑优化,
但是z3就是直接框架套上去求解,我们考虑继续挖掘程序逆向可得到的限制条件或者数据。
其实想到的点在于flag长度,一般遇到的题目给出的数组一般不会有无用的数据,我猜测flag生成的数组d,应该就是和arr同样长度,为72位,且我们通过之前生成的j+i可以看到71是最大值,而且不会出现重复,且这个量j=40,i=31,
这样我们可以知道,flag最后一位和数组b最后一位异或得到的数就是d[71],我们知道flag最后一位是‘}’,且d[71]=arr[71],我们可以得到数组b进而得到flag倒数第四位。

e = 184 ^ ord('}')
for i in range(33,127):
	b = []
	c = i
	for j in range(32):
		d = int((32310901 * c + 1729) % 254)
		b.append(d)
		c = d
	if b[31] == e :
		print(b)
		print(i)
        print(chr(i))

然后我们得到这个数组,重新写z3脚本:

from z3 import *
solver= Solver()
flag = [BitVec("flag %d" %i,8) for i in range(42)]
length = 41
b = [30, 243, 208, 79, 68, 71, 24, 83, 90, 65, 118, 219, 76, 115, 12, 17, 108, 37, 218, 7, 180, 179, 110, 175, 88, 181, 248, 45, 8, 249, 114, 197]
arr2 = [ 77, 263, 394, 442, 463, 512, 667, 641, 804, 752, 885, 815, 1075, 1059, 1166, 1082, 1429, 1583, 1696, 1380, 1987, 2263, 2128, 2277, 2387, 2670, 2692, 3255, 3116, 3306, 3132, 3659, 3139, 3422, 3600, 3584, 3343, 3546, 3299, 3633, 3281, 3146, 2990, 2617, 2780, 2893, 2573, 2584, 2424, 2715, 2513, 2324, 2080, 2293, 2245, 2309, 2036, 1944, 1931, 1817, 1483, 1372, 1087, 1221, 893, 785, 697, 586, 547, 324, 177, 184]

d = [ 0 for i in range(72) ]
for i in range(length):
    for j in range(32):
        a = (flag[i]) ^ b[j]
        d[(i + j)] += a
for i in range(length):
    solver.add(32 < flag[i] )
    solver.add(flag[i] < 127) 
for i in range(72):
    solver.add(d[i] == arr2[i])

if solver.check() == sat:
    model = solver.model()
    s = [model[flag[i]].as_long() for i in range(length)]
    print ("".join(map(chr,s)))
else:
    print('unsat')
#Syc{Y0u_S3e_Z3_1s_soooo00000_Interest1ng}

python3

反编译得到源码:

from unicorn import *
from unicorn import arm_const as ac
import time

def Unicorn(input):
    bytescode = '\x08\xb0-\xe5\x04\xe0\x8d\xe5\x04\xb0\x8d\xe2\x10\xd0M\xe2\x10\x00\x0b\xe5\x14\x10\x0b\xe5\x000\xa0\xe3\x080\x0b\xe5\x000\xa0\xe3\x080\x0b\xe5\x1b\x00\x00\xea\x080\x1b\xe5\x010\x03\xe2\x00\x00S\xe3\n\x00\x00\n\x080\x1b\xe5\x10 \x1b\xe5\x030\x82\xe0\x08 \x1b\xe5\x10\x10\x1b\xe5\x02 \x81\xe0\x00 \xd2\xe5\x07 \x82\xe2r \xef\xe6\x00 \xc3\xe5\t\x00\x00\xea\x080\x1b\xe5\x10 \x1b\xe5\x030\x82\xe0\x08 \x1b\xe5\x10\x10\x1b\xe5\x02 \x81\xe0\x00 \xd2\xe5\x04 \x82\xe2r \xef\xe6\x00 \xc3\xe5\x080\x1b\xe5\x010\x83\xe2\x080\x0b\xe5\x080\x1b\xe5\x1e\x00S\xe3\xe0\xff\xff\xda\x000\xa0\xe3\x080\x0b\xe5\x17\x00\x00\xea\x080\x1b\xe5\x10 \x1b\xe5\x030\x82\xe0\x000\xd3\xe5\x0c0\x0b\xe5\x080\x1b\xe5\x10 \x1b\xe5\x030\x82\xe0\x08 \x1b\xe5\x10 \x82\xe2\x10\x10\x1b\xe5\x02 \x81\xe0\x00 \xd2\xe5\x00 \xc3\xe5\x080\x1b\xe5\x100\x83\xe2\x10 \x1b\xe5\x030\x82\xe0\x0c \x1b\xe5r \xef\xe6\x00 \xc3\xe5\x080\x1b\xe5\x010\x83\xe2\x080\x0b\xe5\x080\x1b\xe5\x0e\x00S\xe3\xe4\xff\xff\xda\x000\xa0\xe3\x080\x0b\xe5\x17\x00\x00\xea\x080\x1b\xe5\x10 \x1b\xe5\x030\x82\xe0\x000\xd3\xe5\x0c0\x0b\xe5\x080\x1b\xe5\x10 \x1b\xe5\x030\x82\xe0\x08 \x1b\xe5\x01 \x82\xe2\x10\x10\x1b\xe5\x02 \x81\xe0\x00 \xd2\xe5\x00 \xc3\xe5\x080\x1b\xe5\x010\x83\xe2\x10 \x1b\xe5\x030\x82\xe0\x0c \x1b\xe5r \xef\xe6\x00 \xc3\xe5\x080\x1b\xe5\x020\x83\xe2\x080\x0b\xe5\x080\x1b\xe5\x1d\x00S\xe3\xe4\xff\xff\xda\x140\x1b\xe5\x00\x00S\xe3\x01\x00\x00\x1a\x010\xa0\xe3\x04\x00\x00\xea\x140\x1b\xe5\x010C\xe2\x03\x10\xa0\xe1\x10\x00\x1b\xe5\x8f\xff\xff\xeb\x03\x00\xa0\xe1\x04\xd0K\xe2\x00\xb0\x9d\xe5\x04\xd0\x8d\xe2\x04\xf0\x9d\xe4'
    cmp_data = [149, 187, 165, 189, 151, 176, 171, 165, 114, 180, 176, 161, 115, 181, 155, 174, 117, 163, 174, 115, 187, 161, 163, 175, 163, 116, 115, 176, 169, 99, 185]

    def hook_code(mu, address, size, user_data):
        if address == BASE + 420:
            data = mu.mem_read(0, 31)
            if [ data[i] for i in range(len(data)) ] == cmp_data:
                print ('success')
                time.sleep(5)
                exit(0)
            else:
                print ('fail')
                time.sleep(5)
                exit(0)

    mu = Uc(UC_ARCH_ARM, UC_MODE_ARM)
    BASE = 4194304
    STACK_ADDR1 = 0
    STACK_ADDR2 = 1024
    STACK_SIZE = 1048576
    mu.mem_map(BASE, 1048576)
    mu.mem_map(STACK_ADDR1, STACK_SIZE)
    mu.mem_write(STACK_ADDR1, input.encode())
    mu.reg_write(ac.UC_ARM_REG_R0, 0)
    mu.reg_write(ac.UC_ARM_REG_R1, 11)
    mu.reg_write(ac.UC_ARM_REG_SP, STACK_ADDR2 - 1)
    mu.mem_write(BASE, bytescode)
    mu.hook_add(UC_HOOK_CODE, hook_code)
    mu.emu_start(BASE, BASE + 424)


if __name__ == '__main__':
    input = input('plz input your flag:')
    Unicorn(input)

首先看到的是unicorn,这个库,是个虚拟引擎,模拟执行框架,简单说,这个库,我们可以手动设置好空间,栈,寄存器等,然后将二进制文件或者二进制代码导入进去,就可以让其运行,并配合hook技术,获取其运行中的数据等。
现在unicorn主要在安全方向是病毒的动态分析吧,毕竟这就相当于一个虚拟机,然后在安卓方向也有一些文章,unicorn可以模拟多种指令集,在ctf中也有出现于对某些非常见框架的模拟动调中,
像现在这一题,我们简单翻阅unicorn相关文章就会发现其实主要部分差不多的unicorn使用的框架,将我们输入的值,放到unicorn中执行,然后返回出来加密后数组和cmp_data进行对比,
我们没有发现关键加密函数,因为这个加密函数就是那串bytescode字符串,他将在unicron中作为汇编的opcode执行,我们使用工具将这串字符还原为汇编代码,但是另外要注意的是,程序有所提示的点:arm_const,这是arm的指令集。
arm是目前移动端和iot方向常用的平台,由于是RISC(精简指令集),arm对应的汇编代码其实要比x86/x64的intel平台要少一些,应该是更易学一些的。

非预期解

我们使用pyhton库capstone转化:
首先是安装:

pip install capst

使用,我给出一个capstone的arm指令的框架(直接将我们的bytescode赋值给code就好):

from capstone import *
from capstone.arm import *
 
CODE = b"..."
 
md = Cs(CS_ARCH_ARM, CS_MODE_ARM)
for i in md.disasm(CODE, 0x1000):
    print("%x:\t%s\t%s" % (i.address, i.mnemonic, i.op_str))

然后得到一个对应的arm汇编码,下面做出一些注解:

  • arm汇编码 对应含义
  • str r0, [fp, #-0x10] fp - 0x10 = r0
  • sub sp, sp, #0x10 sp -= 0x10
  • add fp, sp, #4 fp = sp + 4
  • b #0x109c jmp 109c
  • cmp r3, #0x1e r3 <= 0x1e
    ble #102c if yes >> jmp 102c

注意回跳(向前跳转的操作是循环。
这个函数中,存在3处循环处理。另外前后两端位载入和退出,我们分析三处循环,
其中三个循环都以[fp-8]作为循环计数器,且判定条件:

  • 1:[fp-8] <= 0x1e?
  • 2:[fp-8] <= 0xe?
  • 3:[fp-8] <= 0x1d?

我们通过cmp_data可以知道flag长度为31,即0x1f,所以第一层循环是所有值都进行,后面两次分别是断在了0xe(即14位)和最后一位上,
我们再分析循环内,三个循环都是做简单的加减运算,所以我们尝试运算cmp_data和“Syc{”的差值:

data = [149, 187, 165, 189, 151, 176, 171, 165, 114, 180, 176, 161, 115, 181, 155, 174, 117, 163, 174, 115, 187, 161, 163, 175, 163, 116, 115, 176, 169, 99, 185]
st1 = 'Syc{'
for i in range(4):
	print(data[i] - ord(st1[i]),end=',')
#66,66,66,66,

我们可以知道大部分数据减去的都是66,尝试全部如此解密开:

data = [149, 187, 165, 189, 151, 176, 171, 165, 114, 180, 176, 161, 115, 181, 155, 174, 117, 163, 174, 115, 187, 161, 163, 175, 163, 116, 115, 176, 169, 99, 185]

for i in range(31):
	print(chr(data[i] - 66),end='')
#Syc{Unic0rn_1sYl3al1y_ama21ng!w

其实flag的大体已经出现了,我们可以知道最后一位应该是”}“才对,而且中间is后面应该是”_“然后后面一个单词应该是really,而且我们查看发现两者对应的要减去的数是60,这也符合我们对程序的判定,所以得到flag:Syc{Unic0rn_1s_r3al1y_ama21ng!}

预期

看到了出题师傅优雅的思路。
其实看到二进制数据,我们可以使用ida将二进制代码反编译,甚至还可以看到伪代码,首先我们将数据写入到一个文件中,但是这里我们应该知道,如果我们将数据直接复制黏贴是写入的字符而非十六进制数,所以我们使用程序写入文件是最简单的,这里使用python: import os

file = open('py3.txt','wb')
ss = b'\x08\xb0-\xe5\x04\...'
file.write(ss)
file.close()

注意open的参数’wb‘指使用二进制格式打开一个文件用于读写。
然后我们得到了这个文件,载入ida,选择arm litter:
Geek-10h-re-wp_第20张图片
然后单击c转化为代码,然后点击tab,可以看到伪代码:
Geek-10h-re-wp_第21张图片

我们可以看到其实还存在一个递归,存在a1、a2:

mu.mem_write(STACK_ADDR1, input.encode())
 
mu.reg_write(ac.UC_ARM_REG_R1, 11)

这里传入的参数为我们输入的值和11,其中11即为a2,用于递归的计数,我们在脚本中可以用循环来替代。a1为我们输入的字符串,在三个循环中处理,然后我们可以看到对值得操作其实只有第一个循环而且较为简单,另外两个循环都是位置互换得作用,可以使用(x,y) = (y,x)我们写出脚本:

data  = [149, 187, 165, 189, 151, 176, 171, 165, 114, 180, 176, 161, 115, 181, 155, 174, 117, 163, 174, 115, 187, 161, 163, 175, 163, 116, 115, 176, 169, 99, 185]for l in range(12):
     for i in range(0,30,2):
         (data[i] ,data[i+1]) = (data[i+1] , data[i])
     for i in range(15):
         (data[i] ,data[i+16]) = (data[i+16] , data[i])
     for i in range(31):
         if i & 1 :
             data[i] -= 7
         else:
             data[i] -= 4
 print(''.join(map(chr,data)))

其实值得注意的是互换这个位置,我们循环是11+1倍,是个偶数,互换偶次其实相当于没有互换,一开始我就是原顺序以为不需要置换,但是我们看到后面的部分其实是按照奇偶进行了不同得运算,所以我们也得置换奇偶,另外就是注意第一个互换得循环中增量为2
得到flag:Syc{Unic0rn_1s_r3al1y_ama21ng!}

你可能感兴趣的:(题目的整理,逆向)