美文网首页
2018TCTF线上赛部分逆向Writeups

2018TCTF线上赛部分逆向Writeups

作者: L1nkM3 | 来源:发表于2018-04-02 18:37 被阅读280次

    0x00 简介

    本文包含2018年TCTF线上赛三道逆向题的writeup,三道逆向题依次是g0g0g0、udp和babyvm


    0x01 g0g0g0

    题目并没有给出二进制文件,只给了go语言的strace.log,因此只能通过静态分析弄清楚程序的流程。
    nc到服务器上,通过验证后会打印"Input 3 numbers",因此可以通过搜索字符串定位到主函数中。
    log文件中标注了函数调用和返回的信息,其中fmt.Println和fmt.Scanf的调用产生了大量的log信息,可以通过以下脚本进行简化

    with open("./trace.log") as f:
        cont = f.read()
    f = open("./temp", "w+")
    lines = cont.split("\n")
    lines = lines[15585:]
    skip = False
    for line in lines:
        if "Entering fmt.Println" in line:
            skip = True
        elif "Entering fmt.Scanf" in line:
            skip = True
        elif "Leaving fmt.Println" in line:
            skip = False
            continue
        elif "Leaving fmt.Scanf" in line:
            skip = False
            continue
        if skip:
            continue
        f.write(line+'\n')
    f.close()
    

    得到简化后主函数log,可以分析主函数的流程。队友@echo整理出了主函数的伪代码表示

    print "Input 3 numbers"
    t12 = input()
    t17 = input()
    t22 = input()
    
    t24 = func6(t0) #sa
    t26 = func6(t1) #sb
    
    t29 = len(t24)
    
    if t29 == 0:
        exit(0)
    else:
        t43 = len(t26)
        if t43 == 0:
            exit(0)
        else:
            t41 = len(28)
            if t41 == 0:
                exit(0)
            else:
                t36 = [0]
                t39 = func1(t24,t36)
    
                if t39<=0:
                    exit(0)
                else:
                    t74 = [0]
                    t77 = func1(t26,t74)
    
                    if t77 <= 0:
                        exit(0)
                    else:
                        t69 = [0]
                        t72 = func1(t28,t71)
    
                        if t72 <= 0:
                            exit(0)
                        else:
                            t50 = func2(t24,t26)
                            t51 = func2(t24,t28)
                            t52 = func2(t26,t28)
    
                            t53 = func4(t50,t51)
                            t54 = func4(t53,t24)
                            t55 = func4(t50,t52)
                            t56 = func4(t55,t26)
                            t57 = func4(t51,t52)
                            t58 = func4(t57,t28)
    
                            t59 = func2(t56,t58)
                            t60 = func2(t54,t59)
    
                            t61 = [10]
                            t64 = func4(t51,t52)
                            t65 = func4(t50,t64)
                            t66 = func4(t61,t65)
                            t67 = func1(t60,t66)
    
                            if t67 == 0:
                                print 'Congratulations'
                            else:
                                print "Wrong! Try again!!"
    

    还是非常整洁的,接下来的重点就是分析func1, func2, func4和func6的功能
    我主要分析了func6,func2和func4,大致逻辑如下:

    #func6 : convert to number
    def func2(a, b):
        t4 = [0 for i in range(max(len(a), len(b)))]
        for i in range(t4):
            t6 = phi[0:0, 9:t22]
            if i < len(a):
                t13 = a[i]
            t14 = phi[2:0, 4:t13]
            if i < len(b):
                t18 = b[i]
            v19 = phi[5:0, 6:t18]
            t21 = t14 + t19 + t6
            t22 = t21/10
            if t21 >= 10:
                t24 = (t13 + t18) % 10
            t4[i] = phi[7:t21, 8:t24]
        return t4
    
    def func4(a, b):
        table = [0 for i in range(len(a) + len(b))]
        for i in range(len(a)):
            for j in range(len(b)):
                table[i+j] += a[i] * b[j]
        for i in range(len(table)-1):
            table[i+1] = table[i] / 10 + table[i+1]
            table[i] = table[i] % 10
        return table
    

    其中func6将读取的字符串中得到每一个字符转化为对应的数字存储在一个数组中。仔细观察func2和func4后发现:func2就是数组存储的整数加法!func4就是整数的乘法!反过头来看main.main中的func1,很显然这应该是减法,简单分析后确定如此。那么真相就浮出水面了:
    程序要求输入三个正整数n1,n2,n3需满足以下条件

    (n1+n2)*(n1+n3)*n1+(n1+n2)*(n2+n3)*n2+(n1+n3)*(n2+n3)*n3 == 10*(n1+n2)*(n1+n3)*(n2+n3)
    

    队友@zzh发现这不就是这道题把右边参数换成10吗,解法是椭圆曲线。


    椭圆曲线.jpg

    网上可以找到参数为10的方程的解,这一题就做完了。

    n1=221855981602380704196804518854316541759883857932028285581812549404634844243737502744011549757448453135493556098964216532950604590733853450272184987603430882682754171300742698179931849310347;
    
    n2=269103113846520710198086599018316928810831097261381335767926880507079911347095440987749703663156874995907158014866846058485318408629957749519665987782327830143454337518378955846463785600977;
    
    n3=4862378745380642626737318101484977637219057323564658907686653339599714454790559130946320953938197181210525554039710122136086190642013402927952831079021210585653078786813279351784906397934209.
    

    0x02 udp

    这一题是有关于linux进程间使用udp通信的
    先过一下程序逻辑

    首先看main函数监听6000端口后执行的这三个循环


    loop1

    在第一个循环中主进程fork了共4000个子进程,子进程对应的编号被存在子进程的全局变量index中,然后子进程调用图中标注的childUDP函数。父进程每fork一个子进程就会阻塞一次,收到子进程的udp包后才会继续执行

    loop2

    第二次循环主进程向每一个子进程发送udp包,内容为1,然后阻塞接收子进程的udp包

    loop3

    关键部分在于这第三个循环。主程序将一直与端口为0x1770(6000)的子进程进行通信,每次发送一个3,接收一个值,这个值只能是4或5,为4时v13自增1,为5时循环结束,此时v13中存放得到值就是flag值。直接运行程序是跑不出这个v13的值的,需要我们自己去算。

    最后一个要分析的,也是最关键的是childUDP函数。


    安装

    childUDP函数一开始进行了一个安装,监听端口后向主进程发送0来取消主进程的阻塞。注意全局变量table,这是解这题的关键,table为一个4000 * 4000的二维数组,每一个元素为一个int64值,table[i]为编号为i的子进程所拥有,table[i][j]表示子进程i对子进程j所剩的"权"(还是看后面分析吧2333)。

    函数剩余部分在IDA中不是很好看,所以我直接用伪代码描述

    # childUDP part2
    # loop为死循环
    loop {
        loop {
            # 调用recvfrom函数,将从parent接到的包存在info1中
            recv info1 from parent 
            if info1 > 2
                break
            # 借到主进程udp包,返回2取消主进程阻塞,相当于确认运行
            else
                send 2 to parent
        }
        if info1 != 3
            continue
    
        # 1号子进程是特殊的,无论谁向它发包,它都返回4
        if index == 1 {
            packet = 4
            goto LOOP_FINAL
        }
    
        packet = 5
        #依次与0到3999号子进程"握手"
        for i from 0 to 3999 {
            #对自己和对table中对应值为0的进程不进行交互
            if i == indedx or table[index][i] == 0
                continue
            send 3 to i
            loop {
                recv info2 from friend #含义同上
                if info2 != 3
                    break
                else
                    send 5 to friend
            }
            if info2 == 4 {
                --table[indedx][i]
                packet = 4
            }
            if packet == 4
                break
        }
    LOOP_FINAL:
        if packet == 4 and parent is not main process
            ++table[indedx][parent]
        send packet to parent
    }
    

    总结一下流程:

    1. 父进程创建4000个子进程并确保其成功安装(进入childUDP part2),此时所有子进程阻塞在part2的第一个recvfrom
    2. 父进程向0号子进程发送3,0号子进程开始工作,其parent为父进程,跳过自己(0号),向1号子进程发3,1号子进程收到后回复4,0号收到4后将table[0][1]-=1,向父进程发送4。父进程收到4后flag++,然后向0号发送3,0号进入新一轮outer loop
    3. 上一部一直执行直到table[0][1] == 0
    4. 0号子进程依次与2,3,4,...,3999号子进程握手,这些子进程被激活,分别与其它子进程握手,当握手到1号子进程时1号子进程返回4,这些子进程会向0号发送4,之后过程类似于2
    5. 只有当对所有子进程i(i!=1),table[i][1] == 0时0号子进程才能正常退出握手循环,并向父进程发送5,结束父进程循环。

    因此,flag的值为sum(table[i][1]) i=0,2,3,4,...,3999
    写脚本如下

    #idapython
    import idc
    ea = 0x6020E0
    table = [[0 for i in range(4000)] for j in range(4000)]
    for i in range(4000):
        for j in range(4000):
            addr = ea + 4000 * 8 * i + 8 * j
            table[i][j] = idc.Qword(addr)
    t = 0
    for i in range(4000):
        t += table[i][1]
    print hex(t)
    

    0x3 babyvm

    根据题面不难发现,本题为一个虚拟机,并且我们要通过这个虚拟机在查看服务器上的flag.txt文件。
    所以重点还是分析虚拟机的流程,由于分析的过程比较漫长而且写意总而言之就是分析加调试。这里直接给分析结果了。

    虚拟机是一个基于栈的指令体系,它有一些寄存器(临时变量)用于存放指令参数和一个栈结构,其实具体各部分怎么存的我没分析透彻,但是从抽象(猜)的角度上来看是这样的。
    栈的最大大小为0xFFFF,每一个元素为4字节大小,若元素最高位为1,则表示地址;次高位为1,则表示句柄;否则表示数,以小端法存储。虚拟机会自动处理和做出判断下面写指令集的时候忽略这一点。从栈的0号地址开始执行指令。
    整个虚拟机的指令体系如下(大部分用伪码描述,忽略异常退出情况,可能有误)

    0x00: =>退出
      exit
    0x01: =>跳转
      pop reg0
      jmp reg0
      push 2
    0x02: =>syscall
      pop reg0
      switch(reg0)
      0: => call CreateFile
        pop reg1  ;文件名地址
        从reg1处读取字符串filename。字符串格式为1个字节的长度+ascii字符
        pop reg2  ;CreateFile参数 dwDesireAccess
        pop reg3  ;CreateFile参数 dwFlagsAndAttributes
        handle = CreateFile(filename, dwDesireAccess, 1, 0, 4, dwFlagsAndAttributes, 0);
        push handle
      1: => call ReadFile
        pop reg1 ;文件句柄
        pop reg2 ;存放地址
        pop reg3 ;读取长度
        ReadFile(reg1, &mem[reg2], reg3, BytesReturned, 0);
      2: => call IoDeviceControl
         ;略,babyvm2应该要用到,但是本题没用到,我也没分析
      3: => call puts
        pop reg1 ;要打印的虚拟机内存地址
        call puts(&mem[reg2]);
    0x05: => 跳转
      pop reg0
      jmp reg0
    0x06: => 跳转
      pop reg0
      pop reg1
      jmp reg1
    0x07:
      push 0
    0x08:
      push 1
    0x8D, 0x8E: => push dword
      push *(Dword *)mem[ip+1] ;指令长度为5,后四字节为数据,0x8E保存的是地址
    0x4D, 0x4E: => push word
      push *(Word *)mem[ip+1] ;指令长度为2,后两字节为数据,0x4E保存的是地址
    0xCD, 0xCE: => push reg
      push reg0 ;0xCE保存的是地址
    0x0F: => pop
      pop reg0
    0x10: =>复制
      pop reg0
      push reg0
      push reg0
    0x11: =>取反
      pop reg0
      push ~reg0
    0x12 =>加法;0x13 => 减法;0x14 => 乘法;0x15 => 除法; 0x16 => 取余;0x18 => 按位与;0x19 => 按位或:
      pop reg0
      pop reg1
      reg0 = reg0 +(- * / % & |) reg1
      push reg0
    0x17:
      push 3
    

    根据分析出来的指令,就可以写bytecodes了


    指令
    8E FF 00 00 00    push data 0xff
    8D 40 00 00 80    push addr 0x40
    8E 01 00 00 00    push data 0x1
    8E 01 00 00 00    push data 0x1
    8D 28 00 00 80    push addr 0x28
    07                push 0
    02                syscall
    08                push 1
    02                syscall
    8D 40 00 00 80    push addr 0x40
    8E 03 00 00 00    push data 03
    02                syscall
    09 66 6C 61 67 2E 74 78 74 00   ;string flag.txt at 0x28
    

    0xFF 写在最后

    这次比赛得到这几道题还是比较善良的,不少队都做出来了。另外两道逆向一道是分析数据包,这个我不太擅长;另一道和udp看起来类似,但是似乎复杂了不少,由于第二天有课,就没有继续看了。此外,再一次感受到重命名的重要性!

    相关文章

      网友评论

          本文标题:2018TCTF线上赛部分逆向Writeups

          本文链接:https://www.haomeiwen.com/subject/ufnthftx.html