欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

180721 逆向-极客巅峰(Re)

程序员文章站 2022-03-09 21:05:39
...

Reverse

Simple Base-N

IDA打开,从pdb路径可以leek出现一些信息
180721 逆向-极客巅峰(Re)
可以看到有Base32的字样,估计算法里可能会出现相关信息

继续加载
简单重命名一***释上一眼就能看出来的结构
180721 逆向-极客巅峰(Re)

change函数里对Input做了变换,然后下面与一段字符串进行比较
注意这个地方break出去是继续跑下面的代码,并不是直接结束

180721 逆向-极客巅峰(Re)
从插件显示的花括号对应可以看到,break出去如果v7为true则会往下判断而不是直接到try a little bit harder那里return

因此上面的strcmp是个障眼法,实际上要在下面操作
分别查看sub_4012C0和sub_401310

做出一定整理后,4012C0如下所示
180721 逆向-极客巅峰(Re)
那个全局字符串变量估计就是base32的table了,没联想起来也不要紧,先放着就好
反正这里对全局变量进行了一些变换和修改,继续往下看

401310函数中的核心是另一个函数,长得比较乱,但是简单通过几个变量可以猜出来功能
180721 逆向-极客巅峰(Re)

&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变换函数,稍微理一理还是挺清晰的
180721 逆向-极客巅峰(Re)
针对大小写字母用不同的方法变换,要注意的是这里的%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变量的跟踪,可以发现都没有什么卵用
与输入无关,因此我们根本不需要去读,反正都是正向操作
180721 逆向-极客巅峰(Re)
只有箭头所指的部分才需要关注

于是查看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字节
180721 逆向-极客巅峰(Re)
(试了OD和IDA都看不到XMM寄存器的值,最后用x32dbg才成功跟踪到)
下面两段循环实际上都是逐字节异或
因为第一段以XMM寄存器来操作,单位必须是16个字节,对于零头就无能为力了,于是使用第二段循环来逐字节操作
不知道是出题人故意设置的干扰还是编译器为了加速而优化出来的23333
180721 逆向-极客巅峰(Re)

那么下一个问题就是key啦
回头看参数a2对应的值,是0x401000+v6处的代码
观察加密后的HEX
180721 逆向-极客巅峰(Re)

(动调可知起始字节在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);}

180721 逆向-极客巅峰(Re)
还是挺简单的,后面字节直接拷贝,前4个字节异或解密覆盖上去就完了

Interesting Pointer

前言

开始分析之前先表达一下对出题人和某春秋的意见
题目本身考点海星,可以算有意思的
但是没有考虑到多解,是出题人的失误

而某春秋死不认账,拿我提交的多解data强行跟正解data比较,找不同来一次一次限定范围就更没意思了
我知道正解才敢去质问你为什么长度一致的输入产生的flag还是不对,第一次提交多解讲要最短的输入,那理所当然最短的输入只有唯一的了。结果相同长度的输入还存在多解,请问我纠结浪费了几个小时谁来负责?
自己题目审核不严就算了,承认事故接受多解道个歉有这么难?处理态度一点都看不到大平台的体量

分析

一堆变量初始化直接放过,可以看到它读入data文件,所有内容放到了buff中
接着判断strlen和ftell给出的文件结尾是否相等,相等则结束
也就意味着文件中必须存在\x00来截断strlen

注意文件读取几乎晚于所有变量初始化,因此这里存在溢出点

处理过程如下
180721 逆向-极客巅峰(Re)
依次调用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多此一举,缺点就是长度会长一些