180721 逆向-极客巅峰(Re)
Reverse
Simple Base-N
IDA打开,从pdb路径可以leek出现一些信息
可以看到有Base32的字样,估计算法里可能会出现相关信息
继续加载
简单重命名一***释上一眼就能看出来的结构
change函数里对Input做了变换,然后下面与一段字符串进行比较
注意这个地方break出去是继续跑下面的代码,并不是直接结束
从插件显示的花括号对应可以看到,break出去如果v7为true则会往下判断而不是直接到try a little bit harder那里return
因此上面的strcmp是个障眼法,实际上要在下面操作
分别查看sub_4012C0和sub_401310
做出一定整理后,4012C0如下所示
那个全局字符串变量估计就是base32的table了,没联想起来也不要紧,先放着就好
反正这里对全局变量进行了一些变换和修改,继续往下看
401310函数中的核心是另一个函数,长得比较乱,但是简单通过几个变量可以猜出来功能
&0x1f,取bin可以发现就是五位,这正符合base32的编码方法
“====”, 这个补齐方法也是base系的,而只有base32中才会出现四个等号
再根据之前看到的pdb信息,就基本可以确认该函数为base32_encode了
注意Table是跟正宗b32不同的,因为通过之前一个函数修改过表了,因此解密的时候也需要考虑这一点
main函数中将b32后的input与一串固定字符串比较,要求相等
解法显然就是做b32_decode后反change即可
这里的Table可以自己仿照运算生成,也可以直接用动态调试dump出Table来
然后b32的算法有点难找,我是直接找到python的lib里base64.py中b32decode函数的实现,扒过来以后改表即可
然后关于change变换函数,稍微理一理还是挺清晰的
针对大小写字母用不同的方法变换,要注意的是这里的%26会使得信息丢失,因此逆向的时候要做一下处理
逆change的脚本如下:
通过while判断是否为大/小写字母,然后不断累加26,将丢去的信息补回
得到Table"NoPqRsTuVwXyZaBcDeFgHiJkLm567234"
后通过源码进行b32decode,最后再change逆运算一下即可
py2脚本
```
from __future__ import print_function
#! /usr/bin/env python
"""RFC 3548: Base16, Base32, Base64 Data Encodings"""
# Modified 04-Oct-1995 by Jack Jansen to use binascii module
# Modified 30-Dec-2003 by Barry Warsaw to add full RFC 3548 support
import re
import struct
import binascii
__all__ = [
# Legacy interface exports traditional RFC 1521 Base64 encodings
'encode', 'decode', 'encodestring', 'decodestring',
# Generalized interface for other encodings
'b64encode', 'b64decode', 'b32encode', 'b32decode',
'b16encode', 'b16decode',
# Standard Base64 encoding
'standard_b64encode', 'standard_b64decode',
# Some common Base64 alternatives. As referenced by RFC 3458, see thread
# starting at:
#
# http://zgp.org/pipermail/p2p-hackers/2001-September/000316.html
'urlsafe_b64encode', 'urlsafe_b64decode',
]
_translation = [chr(_x) for _x in range(256)]
EMPTYSTRING = ''
def _translate(s, altchars):
translation = _translation[:]
for k, v in altchars.items():
translation[ord(k)] = v
return s.translate(''.join(translation))
str = "NoPqRsTuVwXyZaBcDeFgHiJkLm567234"
_b32alphabet = {i:str[i] for i in range(32)}
_b32tab = _b32alphabet.items()
_b32tab.sort()
_b32tab = [v for k, v in _b32tab]
_b32rev = dict([(v, (k)) for k, v in _b32alphabet.items()])
def b32decode(s, casefold=False, map01=None):
"""Decode a Base32 encoded string.
s is the string to decode. Optional casefold is a flag specifying whether
a lowercase alphabet is acceptable as input. For security purposes, the
default is False.
RFC 3548 allows for optional mapping of the digit 0 (zero) to the letter O
(oh), and for optional mapping of the digit 1 (one) to either the letter I
(eye) or letter L (el). The optional argument map01 when not None,
specifies which letter the digit 1 should be mapped to (when map01 is not
None, the digit 0 is always mapped to the letter O). For security
purposes the default is None, so that 0 and 1 are not allowed in the
input.
The decoded string is returned. A TypeError is raised if s were
incorrectly padded or if there are non-alphabet characters present in the
string.
"""
quanta, leftover = divmod(len(s), 8)
if leftover:
raise TypeError('Incorrect padding')
# Handle section 2.4 zero and one mapping. The flag map01 will be either
# False, or the character to map the digit 1 (one) to. It should be
# either L (el) or I (eye).
if map01:
s = _translate(s, {'0': 'O', '1': map01})
if casefold:
s = s.upper()
# Strip off pad characters from the right. We need to count the pad
# characters because this will tell us how many null bytes to remove from
# the end of the decoded string.
padchars = 0
mo = re.search('(?P<pad>[=]*)$', s)
if mo:
padchars = len(mo.group('pad'))
if padchars > 0:
s = s[:-padchars]
# Now decode the full quanta
parts = []
acc = 0
shift = 35
for c in s:
val = _b32rev.get(c)
if val is None:
raise TypeError('Non-base32 digit found')
acc += _b32rev[c] << shift
shift -= 5
if shift < 0:
parts.append(binascii.unhexlify('%010x' % acc))
acc = 0
shift = 35
# Process the last, partial quanta
last = binascii.unhexlify('%010x' % acc)
if padchars == 0:
last = '' # No characters
elif padchars == 1:
last = last[:-1]
elif padchars == 3:
last = last[:-2]
elif padchars == 4:
last = last[:-3]
elif padchars == 6:
last = last[:-4]
else:
raise TypeError('Incorrect padding')
parts.append(last)
return EMPTYSTRING.join(parts)
# RFC 3548, Base 16 Alphabet specifies uppercase, but hexlify() returns
# lowercase. The RFC also recommends against accepting input case
r = "weNTDk5LZsNRHk6cVogqTZmFy2NRP7X4ZHLTBZwg"
r = b32decode(r)
print(r)
for i in r:
v = ord(i)-ord('a')
if(v>=0 and v<26):
k = v+ord('T')
while(k<=ord('a')):
k += 26
print(chr(k), end='')
else:
v = ord(i) - ord('A')
if (v >= 0 and v < 26):
k = (v + ord('4'))
while(k<=ord('A')):
k += 26
print(chr(k), end='')
else:
print(i, end='')
flag{aaa@qq.comaaa@qq.comaaa@qq.comaaa@qq.com_r0t13}
Input your luncky number_string
main函数中通过cin接收输入,下面做了一大堆操作
但是通过对input变量的跟踪,可以发现都没有什么卵用
与输入无关,因此我们根本不需要去读,反正都是正向操作
只有箭头所指的部分才需要关注
于是查看sub_401100
v3 = _mm_cvtsi32_si128(input);
v4 = _mm_unpacklo_epi8(v3, v3);
v5 = _mm_shuffle_epi32(_mm_unpacklo_epi16(v4, v4), 0);
最开头这三个处理非常丑,要不挨个去查手册,要不就直接动调观察
汇编指令是三条
punpcklbw xmm0, xmm0
punpcklwd xmm0, xmm0
pshufd xmm1, xmm0, 0
全部是交叉赋值的,分别将1字节的input变为2字节、4字节、16字节
(试了OD和IDA都看不到XMM寄存器的值,最后用x32dbg才成功跟踪到)
下面两段循环实际上都是逐字节异或
因为第一段以XMM寄存器来操作,单位必须是16个字节,对于零头就无能为力了,于是使用第二段循环来逐字节操作
不知道是出题人故意设置的干扰还是编译器为了加速而优化出来的23333
那么下一个问题就是key啦
回头看参数a2对应的值,是0x401000+v6处的代码
观察加密后的HEX
(动调可知起始字节在0x89之后)
这里有大量的连续0x5A出现,在PE文件中通常会有大量的0x00作为填充,这里由于加密字节数太少所以没有用到上述的论据
不过连续三个0x5A通常一般是0x00000001之类的值加密得到的,因此还是只得一试的
之后还有0x7E、0x1E的出现频率比较高,但不连续,如果0x5A失败的话也可以试一试~
(实在不行也可以手动**,好几个交流的朋友都是一个一个试的,甚至还有一个小可爱是从255倒着试的23333333333333333333333333)
0x5A对应十进制的90,即NOP,这个数字还是比较有意义的233
输入以后便回显了flag(cin接受的是int类型,因此直接输入数字即可,而不用chr)
我们继续解密查看一下代码
解密IDC脚本
auto i;for(i=0;i<0x61;i++){PatchByte(0x401013+i, Byte(0x401013+i)^90);}
还是挺简单的,后面字节直接拷贝,前4个字节异或解密覆盖上去就完了
Interesting Pointer
前言
开始分析之前先表达一下对出题人和某春秋的意见
题目本身考点海星,可以算有意思的
但是没有考虑到多解,是出题人的失误
而某春秋死不认账,拿我提交的多解data强行跟正解data比较,找不同来一次一次限定范围就更没意思了
我知道正解才敢去质问你为什么长度一致的输入产生的flag还是不对,第一次提交多解讲要最短的输入,那理所当然最短的输入只有唯一的了。结果相同长度的输入还存在多解,请问我纠结浪费了几个小时谁来负责?
自己题目审核不严就算了,承认事故接受多解道个歉有这么难?处理态度一点都看不到大平台的体量
分析
一堆变量初始化直接放过,可以看到它读入data文件,所有内容放到了buff中
接着判断strlen和ftell给出的文件结尾是否相等,相等则结束
也就意味着文件中必须存在\x00来截断strlen
注意文件读取几乎晚于所有变量初始化,因此这里存在溢出点
处理过程如下
依次调用3个func,将返回值放在buff1、2、3中,要求buff[3]的值为0
3个Func的功能依次为exchange(buff[x], buff[y]), calc1(buff[x], buff[y]), calc2(buff[x], buff[y])
exchange固定 return 1
calc1具体过程为: return abs(x + y) - abs(x) - abs(y) + 2
calc2具体过程为: return abs(x) + abs(y) - abs(x + y) + 2
a-|a+1|+3
而三次函数的参数和返回值分别是
x = 3
y = 4
buff[1] = exchange(buff[x], buff[y])
buff[2] = calc1(buff[1], buff[2])
buff[3] = calc2(buff[2], buff[3])
这里我把x和y分离出来写的原因是,中间读入可以覆盖掉初始化,从而控制exchange的两个参数
首先如果不溢出的话,calc2想要返回0可以通过0x7fffffff和0x80000001来实现
由|x|+|y|>=|x+y|,即|x|+|y|-|x+y|+2>=2可知正常情况下无解
但是|x|+|y|-|x+y|+2==0x1 00000000却是有解的,令x、y互为相反数,则|x|==|y|==0x7fffffff
而calc1却不可能制造出这两个返回值,因此无解
calc1也很容易返回0, 有且仅有令两个操作数其中之一为-1即可
是吗?
令x, y都为正数, x+y=0x80000001则|x+y|=0x7fffffff,|x|+|y|-|x+y|正好就是2
返回buff[3]为0后,通过之前保存的buff[3]和buff[5]来得出flag
这里其实可以大概猜出来,关键的两个变量就是buff[3]和buff[5]了
然而比赛的时候我没注意这里23333导致从别的角度去考虑造成一堆多解
期望解
通过exchange交换calc1和calc2的位置,同时控制buff[3]为-1,则可满足条件
即构造data为 b"1"*(20+4*3) + b"\xff\xff\xff\xff" + b"\x07\x00\x00\x00" + b"\x08\x00\x00\x00"
生成flag的关键参数就是-1和8
非预期解
交换顺序非预期
这是最容易看到的多解,7和8两个数作为交换参数,当然8和7也是可行的,而生成flag提取的却只有最后一个参数
这个只有1种情况,试一下也可以接受
calc1的参数非预期
利用之前所说的calc1的溢出计算,使x和y都为正数的情况下和为0x80000001,则可同样使返回值为0
例如构造data为 b"1"*(20+4*3) + b"\xff\xff\xff\x7f" + b"\x07\x00\x00\x00" + b"\x08\x00\x00\x00"
通过交换过后的calc2来获得其中一个参数,由于exchange返回值只为1,从而可知calc2可获得的正数结果只有2
那么对应只要控制buff[3]为0x7fffffff即可
这样产生了2个多解
exchange的参数非预期
既然可以交换,凭什么非要交换calc1和calc2?
在buff前面有那么多可控字段,完全可以在其中填充上一个函数地址,然后通过exchange将其与calc2的指针变量交换,这个函数地址可以是calc1,还可以是随便什么返回0的函数
例如构造data为 b"\x00"*(20+4*3) + b"\xe8\x1f\x40\x00" + b"\x08\x00\x00\x00" + b"\x03\x00\x00\x00"
这里是利用strlen配合头部为’\0’从而返回0的示例
由于该程序没有ASLR,因此地址全部固定
这种情况下产生的多解就多了去了,一方面交换目标可以随意放置,即使在限制长度的情况下每个交换函数也有前面4个位置可以选择
而更关键的是此种情况下校验通过与否根本与第三个参数无关,因此生成的flag完全没有意义,硬要说的话每个buff[5]都有0x100000000个buff[3]可以组合
函数指针覆盖非预期
这个是最容易想到的,也是我最早提交的多解
反正都能溢出,凭什么还要通过exchange函数
例如构造data b"1"*(20+4*3) + b"\xffffffff" + b"\x00"*(2*4) + b"\x97\x13\x40\x00" + b"\x97\x13\x40\x00"
直接将calc2处的指针修改为calc1
具体情况与上面exchange的情况相同,只不过不用通过exchange多此一举,缺点就是长度会长一些
下一篇: 第一届巅峰极客