avatar

Catalog
KCTF题库:异想天开

前言

原本想着,在博客记录下做的每一道CTF题,懒癌,一直没有行动;但这题就不一样了,自认为还是有一定难度的,有一定启发性的,所以得抓紧记录下来,不然就忘记思路了。这题最初是上周五(2021.10.15)做的,当时没做出来,回家后又和萌萌哒研究了1小时,还是没头绪(这里有个插曲,萌萌哒发现很多时候我不会用工具,并给了一些指点)。就看了高博(他做出来了)给的算法入口点,然后周日简单写了一个wp,周一做了一个通用的注册机,今天(已经周四了。。。)抽点时间来完成这篇博客吧。

之所以这题花费这么多时间,其实是一直没有定位到算法处,早定位到,就早做出来了,算法本身并不难。而这个定位的过程,正是这一道题的难度所在。这里,就来解析一下这道题。

踩坑流程

找入口点

找入口点花了我差不多90min,就是这样,我花了整整90min都没有找到入口点,当然,最终花了一整天都没找到算法入口点。就像我在HiSuite.exe中找了几个月,都没找到验证码校验的地方一样。最终,中午前,高博告诉我,入口点为0x401840,他是通过CreateDialogParamA回溯到的(实际上也可以通过SetDlgItemTextA)。下午,我通过GetWindowTextA也回溯到了sub_401840(通过这个函数回溯,也是导致我踩到巨坑上的一个主要因素)。这里呢,简单概括一下回溯的过程:

  1. 该CrackMe程序的入口点都是0x400000,这样就不需要在IDA里面Rebase Program Segment,两者在主程序中的地址是一致的。下面开始回溯,在GetWindowTextA下断,点击确定,断下,栈中看到,由地址0x41DC22调用

  2. IDA中找到0x41DC22,所在函数为DDX_Text,通过交叉引用,发现其在sub_401440中被调用

  3. 进入,sub_401440,交叉引用,发现地址位于.rodata。遂进入OD,在0x401440处下断,重新执行程序,右下角栈中可以看到sub_401440的返回处为0x41A7A5

  4. 找到0x41A7A5,发现sub_401440是通过寄存器+偏移来寻址并调用的,此次调用发生在函数sub_41A750

  5. 再用同样的方法在OD中回溯,就可以找到是在函数sub_401840中调用了sub_401750

找算法入口

  1. 找到sub_401840后,先F5,打算分析算法(当然,算法并不在这)。先定位到黄色方框,这里我在经过OD分析后,已经将变量重命名了,显然这里的作用就是对输入的用户名和注册码做一个长度校验,若长度不符合,则会将v4的值设置为this+100处的值,然后跳转。一会来看this+100处存的是什么。再往下看,如果能够通过黄色方框的校验,就会来到LABEL_11,这部分最像算法的,就是红框方框里面的内容了。但显然不是,高博比我提前一个多小时定位到sub_401840,若这里就是获取flag的算法,他早就做出来了。

  2. 在OD中断到0x40185E,然后执行一下对sub_41A750的调用,观察堆栈变化,可以看到栈中有了输入的用户名和注册码;并且先前在利用GetWindowTextA进行回溯时也是从这里出来的。因此,可以推测sub_41A750通过调用GetWindowTextA获取到输入的用户名和注册码,再将其放入栈中的某个位置。

    而这个栈中的位置是什么呢?观察下图,先看OD,mov eax, [esi+0x80],可以获得到“CTFHUB”,正是输入的用户名,结合左边IDA中的伪代码,就可以推出esi即this。这样就可以得出下表中的结论:

    伪代码 对应的值
    this + 128 输入的用户名
    this + 124 输入的注册码
    this + 120 未注册
    this + 116 注册码不能为空
    this + 112 用户名不能为空
    this + 100 很遗憾!你输入的注册码不正确
    this + 96 恭喜:你已经注册成功。简单吧?
    this + 92 zouzhiyong-zouzhiyong

    有了这张表格,就能更容易看明白左图这张IDA的伪代码,在获取了值以后,需要先保证输入用户名的长度大于等于5,且输入注册码的长度大于等于19,这样才会执行的LABEL_11,即一个看上去有着flag算法逻辑的地方。

  3. 这里来看下LABEL_11的逻辑,很显然,如果存在flag的运算,那肯定就是黄色方框的部分了。再往后看,红色方框有一个校验。判断字符串Str1的值,与this+92(即字符串zouzhiyong-zouzhiyong)的值是否相等。如果相等,那v9的值会被设置为this+96,看似这样就能成功了。那再去看Str1的值哪来,如果说黄色方框是进行flag运算的部分,那么它是通过this+92这个字符串进行运算的。但这样问题也就来了,在对该字符串进行如下的一串运算后,再和自身进行比较,若一致,则能成功。显然,这里不可能成功。因为经过黄色方框中的运算,字符串不可能还会是原先的字符串了。我曾在这里卡壳了很久,认为通过计算出一个经过黄色方框中运算,能够得到字符串”zouzhiyong-zouzhiyong“的原始字符串,很显然,我怎么都无法获取到0x7E以上的字符,更何况,这里根本就没有用输入的注册码进行计算,而是用一个内置的字符串进行运算后再与自身比较,所以我被坑了。因此结论很明显了,那就是根本不能进入这个黄色方框的if语句,一旦进来了,那就已经无法通过校验了。

  4. 之后就一直在sub_401840中徘徊,进去看了该函数中的所有函数调用,都没有看着像算法实现的地方,始终无法找到计算flag的位置处,甚至一度怀疑是入口点找错了,根本不在sub_401840。后来看了高博给我的flag算法所在的函数,我从此处开始回溯,确实回溯到了sub_401840,也发现了问题所在。

解法

  1. 真正的问题其实就在sub_401750中,由于我一开始找入口点是通过GetWindowTextA回溯的,也途径了sub_401750。因此就默认sub_401750是一个初始化this成员的函数,但实际上并不仅仅如此。在sub_401750中有一个不确定的点,就是call dword ptr [eax+0x84]这条调用指令,该调用采用了寄存器+偏移的方式寻址,这意味着,如果sub_401750被多次调用,每次执行到这里时,所调用的函数是不确定的。而恰好,这个sub_401750确实有被多次调用,且每次执行到这时调用的函数并不一样,而之前在利用GetWindowTextA进行回溯时,也仅仅从函数sub_401440回溯过来,即下图的第一种情况,也因此,忽略了。而在这里下断,会断下了3次(实际上是4次,但是第1次是在执行sub_401840之前)。这样也就有额外的两个函数需要分析,即sub_401E80sub_401AB0

  2. 先来看sub_401E80,这里就直接把该函数的各个调用的伪代码贴一下,显然,该函数及其子函数是没有flag算法实现的

  3. 接着看sub_401AB0,很明显sub_419413的部分并没有flag的算法实现,但是要注意,这里也调用了GetWindowTextA,该API也被多次调用,但是我在回溯时,也只回溯到了第一次调用的时候,因此最终回溯到了sub_401840而不是sub_419413

    再看sub_401AE0,这里面内容很多,实际上flag的算法就在这个函数里面。这里我就不详细分析了,总体难度不大。它分为两步,首先是将字符串“zouzhiyong-zouzhiyong”与输入的字符串(本题为”CTFHUB“)进行一个运算,得到一个新的字符串(本题得到新字符串为“yggz-iiiz-uggo-uzoy-zyih”,该字符串可以直接在内存中获得,因而有些人的wp非常简单,但不通用),这个得到字符串的过程还是有点意思的,它有一个ascii求值的循环,前3次求和用的字符串分别是CTFHUB、TTFHU,FTFH,自己找规律哈哈。得到字符串之后,接下来会调用sub_401C80实现一个简单的转换,就可以得到最终的flag了,sub_401C80中的运算过程更简单,就不分析了。

  4. 最后附上写好的一个writeup,唯一限制主要是只能支持长度为6位的用户名,不过感觉差不多了,也不想进一步完善了

    python
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    s1 = 'CTFHUB'
    s2 = 'zouzhiyong-zouzhiyong'
    s3 = []

    str_pos = 0
    while str_pos < 5:
    # 每次循环后,会从s2中获取到4个字符,它们之间需用'-'隔开
    if str_pos != 0:
    s3.append('-')

    # 这一步计算出此轮循环对s2进行变换时,所需的s1的部分字符串
    temp_s = s1[str_pos] + s1[1:6-str_pos]

    # 利用前一步得到s1算出其ascii值的和,用于对s2进行变换,并
    # 将变换后s2存储到s3中
    sum_s = 0
    for x in temp_s:
    sum_s = sum_s + ord(x)

    while sum_s < 10000:
    sum_s = sum_s * 3

    sum_s = sum_s // 3

    while sum_s != 0:
    i = sum_s % 10
    sum_s = (sum_s - sum_s%10) // 10
    s3.append(s2[i])

    str_pos = str_pos + 1

    s3 = ''.join(s3)


    # 利用s3计算出最后的flag
    res = []

    for x in s3:
    if ord(x) == 45:
    res.append(x)
    else:
    temp = ord(x) - ord(x)%5 - 20 - 12 - ord(x)%2
    res.append(chr(temp))

    print(''.join(res))

总结

  1. 猜测可能调用的API并下断,来回溯到函数的入口点是一个值得采取的手段。直接通过winMain去跟,可能很难跟到。用API进行回溯更容易找到某些逻辑的执行处,而通过对堆中数据下内存断点进行回溯则容易死在框架里。
  2. 多利用工具,例如IDA伪代码中的Tab键,或者Synchronize with -> IDA-View,可以将伪代码和汇编联系起来(感谢萌萌哒的指导)。以及最近认识到的OD中的插件OllyDump(Dump内存),SharpOD(过Vmp自带的反调试)
  3. 进行回溯时,需要了解某个函数的所有交叉调用关系,这点在IDA中用X快捷键可以看的更清晰。而在OD中,从某个API的回溯,往往只能看到某一次调用的过程,从而容易忽略该API在其它地方可能也被调用。
Author: cataLoc
Link: http://cata1oc.github.io/2021/10/22/KCTF%E9%A2%98%E5%BA%93-%E5%BC%82%E6%83%B3%E5%A4%A9%E5%BC%80/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶