menu 牢记自己是菜
CUMT2020新生赛--出题,WP
283 浏览 | 2020-11-28 | 阅读时间: 约 9 分钟 | 分类: 计算机 | 标签:
请注意,本文编写于 61 天前,最后修改于 56 天前,其中某些信息可能已经过时。

0x1 前言

这里是新生赛出题的思路,题解与复现专栏。主要是写一下自己的想法,与学到的一些东西,和复现一下PWN题。本次比赛我主要负责了一道密码学与三道逆向题目。学弟们太强了,差点把我底裤给我撕了,差一点点就被打穿了,但是我觉得被打穿只是时间问题,要是最后一道题目再稍微的早一点点放出,我应该现在已经在裸奔了。本文将分为三个部分,第一部分题目思路+WP。第二部分出题时学习到的东西,第三部分PWN的复现(持续更新)。师傅们可以选择食用。


0x2 题目思路即题解

pseudo-random number

一道来自空指针公开赛的原题,具体传送门在这里。我对原本脚本进行了一定的魔改,但是并没有更改主要逻辑。删除了原先脚本的VSS加密逻辑,将其中的知识点直接进行了抽象。具体的考点就如同题目描述的一样,伪随机数的爆破,利用二维码的白边,获得被泄露出的随机数生成序列还原本来的种子生成序列。最终完成对图像的解密,具体的WP解密脚本如下:

from PIL import Image
from randcrack import RandCrack
import random
share = Image.open('Secret.png')
width = share.size[0]//2
res = Image.new('L', (width, width))
print(width)#width=444由于加密使得图片扩容,即一个像素生成2*2=4个像素
bits = ''#由于share2的构成的逻辑是当原图片是255像素
for idx in range(width*width-624*32, width*width):#取倒数624*32个数据
    i,j = idx//width, idx % width#i,j分别代表行和列
    #print(i,end=" ")
    #print(j)
    if share.getpixel((2*j, 2*i)) == 255:#取最小的一个像素点代表此处的方阵
        bits += '0'
    else:
        bits += '1'#bits即为初始化的字符串
rc = RandCrack()
for i in range(len(bits), 0, -32):
    rc.submit(int(bits[i-32:i], 2))#每次将32位放入初始化的rc一共放入624组,达到初始化最少数据
flipped_coins = [int(bit) for bit in bin(rc.predict_getrandbits(width*width-624*32))[2:].zfill(width*width-624*32)] + list(map(int, bits))#通过初始化数据与脚本猜测剩余随机数
data = []
for idx in range(width*width):
    i, j = idx//width, idx % width
    if share.getpixel((2*j, 2*i)) == 255:
        data.append(0 if flipped_coins[idx] else 255)
    else:
        data.append(255 if flipped_coins[idx] else 0)
    #if flipped_coins[idx]:
     #   data.append(255 if share.getpixel((2*j, 2*i)) == 255 else 0 )
    #else:
     #   data.append(0 if share.getpixel((2*j, 2*i)) == 255 else 255 )
res.putdata(data)
res.save('ans.png')

还是希望大家能学会这种爆破的方法。

要识庐山真面目,只需身在此山中

这道题目我是没想到解答数会比python字节码少的,一开始沟通的,我是想把这道题目放在签到题目中的。本题的主要想传达的一种逆向的思想,就是“七分靠猜,三分靠看”,不要被一些恶心的东西所迷惑。
这道题目很抱歉,出现了一小点点小问题,是我在看师傅们WP时候发现的,由于最后还原数组没有注意,导致少了一个字符,第一位flag字符没有卡住,出现了不是C也可以通过check的问题,我很抱歉,已经对题目文件进行了修改。
本题的主要逻辑如下:

  1. 首先进行了一次普通的异或
  2. 进行了第一次处理F1(X)
  3. 进行了第二次处理F2(X)
  4. 最后进行一次异或,其中第0位被连续使用了两次

这里是两次函数,这两个函数实际上是互为反函数的,一个数经过原先两个数处理后其实是不变的。所以整个解密逻辑只需要过两次异或即可。
这道题目的灵感是来自我高数的复习的时候,一开始我是想在其中加入反动调的,但是被你子洋师傅制止了。快感谢子洋师傅,这里可以使用动态调试观察输入与输出的结果,从而得到两次函数加密后没有被改变的结论。
当然,由于这个程序并没有对flag进行严格的密码学处理,导致各个flag位是独立的,也可以选择强行模拟程序通过过爆破的手段对flag进行爆破。
无论是哪种方法,都体现出来猜的思想,有些时候我们就需要大胆的去猜,毕竟猜错也不会掉一块肉。所以我觉得这道题目应该会比较的简单,可能放在入门区会有更多的师傅们做出来。

记事本逆向

这道题目应该会比较的恶心,但是解题数居然会比上一题多是我没想到的。本题的想法主要是来自上次比赛周学长的题目,上次比赛认真复现的同学们会知道,当时的题目逻辑是一颗树,并且汇编语言不是x86架构的,导致IDA并不能反汇编。所以本次采取了python字节码的方式模拟了一棵树。鉴于大多数同学并没有接触过算法,所以我并没有选择排列树等比较复杂的树,而是根据flag的顺序构造出了一颗左枝非常非常长的盗版树最后采取了中序遍历的方式输出了整棵树,简化了整个程序的逻辑降低难度。主要目的是考察一下大家对一种自己不熟悉的语言的一种快速掌握能力。这里给出python的源码,大家可以对着进行一下复现。

def main1():
    class Node:
        def __init__(self, data):
            self.left = None  #左节点
            self.right = None #右节点
            self.data = data  #值
        def PrintTree(self):
            if self.left:
                self.left.PrintTree()
            print(chr(self.data),end="")
            if self.right:
                self.right.PrintTree()
        def insert(self,data):
            if self.data:
                if self.left is None:
                    self.left = Node(data)
                elif self.right is None:
                    self.right = Node(data)
                else:
                    self.left.insert(data)
        def inorderTraversal(self, root):
            res = []
            if root:
                res = self.inorderTraversal(root.left)
                res.append(root.data)
                res = res + self.inorderTraversal(root.right)
            return res
    str1=input("PLZ input flag:")
    #str1="cumtctf{1e70a305fb378202c26dd6dafa14db17}"
    flag=[ord(i) for i in str1]
    root = Node(flag[0]) #创建节点
    for i in range(1,len(flag)):
        root.insert(flag[i])
    F=root.inorderTraversal(root)
    flag="7b}41ada16fdd2d262c70b8533f00ea{7t1tfuccm"
    YES=1
    for i, element in enumerate(F):
        if ord(flag[i])!=element:
            YES=0
            print("wrong")
            break
    if YES:
        print("wow~you are right!")

大三老狗的童年生活

本题是裤衩题,开玩笑的呀,本题是一个游戏。很多时候我也会再想逆向手到底能干啥,制作游戏外挂就是一个方向。所以我找到了一个很老的很老的游戏,又不能让大家直接使用网上的现成的外挂直接出结果,所以我为大家规定了特定的触发flag条件,主要就是希望大家能真正的体会到身为一个逆向手可以掌控程序的快乐。本题属于一个沙盒题目,没有固定的解法,这里给出几个我想到的解法,当然不限于这些解法。主要的目的就是希望大家玩的开心,享受RE的快乐。具体题目的实现我会放在第二部分,有兴趣的同学可以看一下,毕竟这个知识我也是现学现卖,如有不足的地方,请多包涵。
解法1:

  • 根据游戏名字直接寻找到本游戏的程序(指的是没有被我魔改过的程序)
  • 使用Hexcmp进行比较,直接找到两个文件不同的代码段,通过对代码的分析,判断shellcode作用,触发条件。
  • 寻找相应的地址通过OD完成对标志位的修改,弹出flag。

解法2:

  • 使用制作外挂神奇CE修改器,监控内存,找到小球记录的内存地址
  • 对其经行更改也好,进行内存断点确认shellcode也行,任选其一

解法3:

  • 享受游戏乐趣,发现所有的道具都是死亡并没有分裂道具
  • 寻找随机道具选择,通过修改程序使得所有道具都分裂球,调整道具爆率。
  • 直到使得自己的球数成为32即可。
    当然你要是猜到我是使用注入shellcode的方法修改程序的,你也可以使用子洋师傅的方法,直接定位特殊指令,定位shellcode,观察寄存器得到相应的flag。

0x3 大三老狗的童年生活题目制作:shellcode的编写与注入

程序分析

首先这道题目在出题方面上最难得就是他没有源码,所有的操作与修改必须在汇编层面上实现(自己给自己整了一道逆向题目)。所一整个程序我必须十分的熟悉。首先需要确定自己的出题思路,由于整个程序有很多关,而且每关有很多小关,要是以通关为flag判断条件一定会存在一些不必要的麻烦,比如在小关切换时就触发flag弹出条件,使整道题目丧失他的意义(非预期)。所以在斟酌后我选择了以球的数目为判断通关条件,flag形式由shellcode注入的弹窗实现。
由于整个程序读写权限分明,我必须选择有执行权限的段区植入shellcode,所以找到一块代码空间就成为了下一个目标。由于整个程序不一定全部会被使用,所以一定存在可以利用的代码段。最终我选择了这里。

这里是道具选择的一个case,一共有20个case跳转,假若我将所有道具的图标绑定在一个图片上,其余的空间就变成了无用代码,我们可以进行利用。本题的shellcode就被注入到了这个地方,同时在主程序中所有的图表都产生了变化---死亡图标,无形中又减少了直接靠技术通关的可能性。

shellcode编写

由于每次程序启动的时候地址都是不确定的,所以MassageBox的函数地址也是不能被确定了,所以我们除了需要给MassageBox传参,还需要确定它的函数地址。这里我们需要使用到一个动态链接库----动态定位kernel32.dll
不同版本的操作系统,kernel32.dll的基地址也是不同的。Windows没有linux那样方便的中断机制来调用系统函数,所以只能通过基址+偏移地址来确定函数的位置。大概流程是通过FS段选择器找到TEB,通过TEB找到PEB,然后获取kernel和ntdll的地址。
具体可参考
在确定kernel32.dll后,我们就可以是使用他对windows的API进行查找了。Massageboxs在user32.dll中,而我们并不确定这个动态库是否被程序加载,所以我们需要使用LoadLibrary对user32.dll进行加载。
在加载相应的库之后,我们需要确定我们需要函数的API地址,我们可以使用查找的方式遍历库来实现。但由于每个函数名字都很长,对比起来需要占很大的空间,导致我们的shellcode变得十分的长,所以我们需要使用hash索引的方式,简化比较。我们可以先通过程序计算哈希值。

#include <stdio.h>
#include <windows.h>

DWORD GetHash(char *fun_name)
{
    DWORD digest = 0;
    while (*fun_name)
    {
        digest = ((digest << 25) | (digest >> 7));
        digest += *fun_name;
        fun_name++;
    }
    return digest;
}

int main()
{
    DWORD hash;
    hash = GetHash("LoadLibraryExA");
    printf("0x%.8x\n", hash);
    getchar();
    return 0;
}

当然这里的算法不是固定的,只要能保证这里面算出来的值和你shellcode中对应的值相等即可,我们把他提取出来,我们接完成了准备工作。
对于shellcode的编写我们可以使用C中的asm,直接编写汇编代码,随后在OD中提取相应的字节码,直接注入到程序内即可。注意函数调用的规则等,不要让程序崩溃。
这里是shellcode编写代码:

#include <stdio.h>
#include <windows.h>

int main()
{
    __asm {
        // ===将索要调用的函数hash值入栈保存
        CLD                  // 清空标志位DF
        push 0x1E380A6A      // 压入MessageBoxA-->user32.dll
        push 0x4FD18963      // 压入ExitProcess-->kernel32.dll
        push 0x0C917432      // 压入LoadLibraryA-->kernel32.dll
        mov esi, esp          // 指向堆栈中存放LoadLibraryA的地址
        lea edi, [esi - 0xc]     // 后面会利用edi的值来调用不同的函数

        // ===开辟内存空间,这里是堆栈空间
        xor ebx, ebx
        mov bh, 0x04       // ebx为0x400
        sub esp, ebx       // 开辟0x400大小的空间

        // ===将user32.dll入栈
        mov bx, 0x3233
        push ebx           // 压入字符'32'
        push 0x72657375    // 压入字符 'user'
        push esp
        xor edx, edx        // edx=0

        // ===查找kernel32.dll的基地址
        mov    ebx, fs: [edx + 0x30]    // [TEB+0x30] -> PEB
        mov    ecx, [ebx + 0xC]        // [PEB+0xC] -> PEB_LDR_DATA
        mov ecx, [ecx + 0x1C]         // [PEB_LDR_DATA+0x1C] -> InInitializationOrderModuleList
        mov    ecx, [ecx]            // 进入链表第一个就是ntdll.dll
        mov    ebp, [ecx + 0x8]        //ebp = kernel32.dll 的基地址

        // === hash 的查找相关
        find_lib_functions :
        lodsd                    // eax=[ds*10H+esi],读出来是LoadLibraryA的Hash
        cmp eax, 0x1E380A6A       // 与MessageBoxA的Hash进行比较
        jne find_functions       // 如果不相等则继续查找
        xchg eax, ebp
        call[edi - 0x8]
        xchg eax, ebp

        // ===在PE文件中查找相应的API函数
        find_functions :
        pushad
            mov eax, [ebp + 0x3C]       // 指向PE头
            mov ecx, [ebp + eax + 0x78]     // 导出表的指针
            add ecx, ebp                 // ecx=0x78C00000+0x262c
            mov ebx, [ecx + 0x20]       // 导出函数的名字列表
            add ebx, ebp              // ebx=0x78C00000+0x353C
            xor edi, edi              // 清空edi中的内容,用作索引

            // ===循环读取导出表函数
            next_function_loop :
        inc edi                   // edi作为索引,自动递增
            mov esi, [ebx + edi * 4]       // 从列表数组中读取
            add esi, ebp               // esi保存的是函数名称所在的地址
            cdq

            // ===hash值的运算过程
            hash_loop :
        movsx eax, byte ptr[esi]    // 每次读取一个字节放入eax
            cmp al, ah                  // eax和0做比较,即结束符
            jz compare_hash            // hash计算完毕跳转
            ror edx, 7
            add edx, eax
            inc esi
            jmp hash_loop
            // ===hash值的比较函数
            compare_hash :
        cmp    edx, [esp + 0x1C]
            jnz    next_function_loop      // 比较不成功则查找下一个函数
            mov ebx, [ecx + 0x24]          // ebx=序数表的相对偏移量
            add ebx, ebp                 // ebx=序数表的绝对地址
            mov di, [ebx + 2 * edi]          // di=匹配函数的序数
            mov ebx, [ecx + 0x1C]          // ebx=地址表的相对偏移量
            add ebx, ebp                 // ebx=地址表的绝对地址
            add ebp, [ebx + 4 * edi]         // 添加到EBP(模块地址库)
            xchg eax, ebp                // 将func addr移到eax中    
            pop    edi                     // edi是pushad中最后一个堆栈
            stosd
            push edi
            popad

            cmp    eax, 0x1e380a6a          // 与MessageBox的hash值比较
            jne    find_lib_functions

            // ===下方的代码,就是我们的弹窗
            function_call :
               xor ebx, ebx       // 清空eb寄存器
                   push ebx
                   mov eax,0x151BAA25
                   xor eax,0x68699916
                   push eax
                   mov eax,0x4E7E413A
                   xor eax,0x20172654
                   push eax
                   mov eax,0x13481527
                   xor eax,0x20172654
                   push eax
                   mov eax,0xFA330061
                   xor eax,0x88567652
                   push eax
                   mov eax,0x1A36A911
                   xor eax,0x68699920
                   push eax
                   mov eax,0x595AF15B
                   xor eax,0x68699920
                   push eax
                   mov eax,0x51DF754
                   xor eax,0x63699420
                   push eax
                   mov eax,0xF90BE1B3
                   xor eax,0x956694D0
                   push eax

            mov    eax, esp

            push ebx         // push 0
            push eax         // push "flag"
            push eax         // push "flag"
            push ebx         // push 0
            call[edi - 0x04]  // call MessageBoxA

            push ebx         // push 0
            call[edi - 0x08]  // call ExitProcess
    }
    return 0;
}

最后将shellcode注入。由于注入的flag不能是明文,不然很可能直接被搜索字符串找到,我给每个字符串进行了把不同的异或。

shellcode的调用

这部分就十分的简单,我们可以使用CE,确定小球的内存地址,直接读取内存地址,比较跳转即可。

0x4 PWN题目复现与学习

  • PWN2简单的canary绕过
  • PWN3栈溢出
  • PWN4整数溢出

0x5 写在后面

很荣幸可以参见这次的出题活动,每道题我还是很认真的思考过的。如果同学们这次玩的还比较开心,喜欢这种风格的话,我们不出意外应该明年上半学期的校赛会再次见面,届时我会单独负责RE板块,给大家带来我精心润色的题目(当然老张要是坚持办比赛的话),到之后的题难度一定会上升,毕竟有子洋师傅嘛!如果对此次这几道题有很大意见的师傅们,不用担心,因为12月份的校赛你不会再见到我啦!当然我还是希望师傅们给我提供宝贵的建议,第一次出题,如有不妥,请多多包涵。

发表评论

email
web

全部评论 (共 8 条评论)

    2020-11-30 18:15
    还好有ljzjsc师傅压轴,不然我裤衩都要被学弟打没了,ljzjsc yyds
      2020-12-01 21:41
      @黑你全家∠( ᐛ 」∠)_ 已经在爬了,再爬了
    2020-11-30 15:19
    劝你昊自炜汁
      2020-12-01 21:42
      @x1ngg3 害怕
    Ld1ng
    2020-11-30 15:17
    永远愿做炜哥的小迷弟!
      2020-12-01 21:42
      @Ld1ng贴贴PWN爷爷 ୧(๑•̀⌄•́๑)૭
    2020-11-28 22:49
    炜昊师傅tql。
    我永远单推炜昊师傅 (☆ω☆)
      2020-11-29 19:57
      @iyzyi 我最会爬了