2018TCTF线上赛部分逆向Writeups

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看起来类似,但是似乎复杂了不少,由于第二天有课,就没有继续看了。此外,再一次感受到重命名的重要性!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容

  • 计算机系统漫游 代码从文本到可执行文件的过程(c语言示例):预处理阶段,处理 #inlcude , #defin...
    willdimagine阅读 3,581评论 0 5
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,623评论 18 399
  • 每走一步都像在跋涉。 呼吸也像跋涉,气流每穿过一次胸腔都带来灼热的刺痛。 蹲下来,歇口气。 想:这么好的蓝天白云阳...
    高小花0218阅读 263评论 0 0
  • 遇见你之前,我只是我,遇见你之后,余生都是你 视你如宝贝 宽容你的小毛病 体贴你的不周到 照顾你 溺爱你 像宠物...
    柠檬木头人阅读 256评论 0 1
  • 无意间的回眸,一袭碧绿 蝴蝶已轻轻落入,这未沾尘土的石子 有如,一初见你,...
    幻丝洁阅读 298评论 0 1