L3HCTF 2021 RE 部分 Writeup

 

前言

和 Nebula 的队员们一起参加了这次的 L3HCTF ,最后排名第7,很不错的成绩。记录一下我做的 re 。

比赛官网:https://l3hctf.xctf.org.cn
比赛时间:2021-11-13 09:00:00 到 2021-11-15 09:00:00

 

reverse

IDAAAAAA

没给程序,给了一个ida的i64文件,直接拖入ida。

正常流程分析下来,是输入三个整数,判断5个等式,但是无法同时满足,应该是程序将判断流程隐藏了,但是程序中找不到修改代码段的行为。

看反汇编窗口,发现main函数有一个断点,打开断点列表(ida开始卡了),发现condition里有东西,复制condition到文本编辑器(文本编辑器开始卡了),是一个很大的idapython脚本。

ida下断点在0x40201F,当执行到这条cmp时就触发断点,执行上面的脚本,在另一个位置下断点,获得断点信息,并修改断点添加condition为idapython脚本,之后修改[rsp]rip的值使程序转到设置的断点,触发断点执行脚本,并根据输入的input[i]确定需要解密的indexkey,使用sub_401DB5(维吉尼亚)解密,并且修改[rsp]设置返回地址,并在返回地址设置断点,解密完成后返回时触发断点,继续执行脚本,解密出来的数据将会作为下一个断点的脚本,而这个脚本又会重复上面的过程。

这样可知,原程序并不能判断flag,真正控制执行流程的是idapython脚本。

尝试了很久,除了第一个断点的脚本,几乎每个脚本解密出来都是这样的形式:

NyPGpw = idaapi.get_byte(5127584 + N4QKUt)
NyPGpw -= ord('a')
if NyPGpw == 0:
    afvkwL = 667
    hsYnNw = b'vjHiPd4bBuf'
elif NyPGpw == 1:
    afvkwL = 667
    hsYnNw = b'vjHiPd4bBuf'
elif NyPGpw == 2:
    afvkwL = 667
    hsYnNw = b'vjHiPd4bBuf'
else:
    afvkwL = -1
if afvkwL < 0:
    # ...
else:
    # …
# ...

每个if/elif内部赋了两个值,第一个数值是需要解密的下一个脚本的序号index,第二个bytes是解密的密钥key;下面的if判断,如果序号为负,就直接结束;else块会解密出一个脚本,继续重复上述流程。

初始的断点脚本中间也有这样的if/elif结构,将if块的数据提取出来得:

[287, b'lqAT7pNI3BX']
[96, b'z3Uhis74aPq']
[8, b'9tjseMGBHR5']
[777, b'FhnvgMQjexH']
[496, b'SKnZ51f9WsE']
[822, b'gDJy104BSHW']
[914, b'PbRV4rSM7fd']
[550, b'WHPnoMTsbx3']
[273, b'mLx5hvlqufG']
[259, b'QvKgNmUFTnW']
[334, b'TCrHaitRfY1']
[966, b'm26IAvjq1zC']
[331, b'dQb2ufTZwLX']
[680, b'Y6Sr7znOeHL']
[374, b'hLFj1wl5A0U']
[717, b'H6W03R7TLFe']
[965, b'fphoJwDKsTv']
[952, b'CMF1Vk7NH4O']
[222, b'43PSbAlgLqj']

观察发现,每个解密的key长度都为11,而且解密出的第一行除了起始的变量名(变量名长度也都是6),后面是完全相同的,所以可以直接异或得到key:

keys = []
dec = 'NyPGpw = idaapi.get_byte(5127584 + N4QKUt)'[11: ]
for i in encs: # BIG bytes array, encrypted scripts, length 1000
    enc = i.decode()[11: ]
    t = ''
    for a, b in zip(enc, dec):
        t += chr(ord(a) ^ ord(b))
    if not t[11: ].startswith(t[: 11]):
        keys.append(None)
    else:
        keys.append(t[:11])

得到keys后,发现keys.count(None) == 1,再结合ida中最后flag的格式:

输入有最短,那么这应该是一个图问题,找最短路径,每个解密出来的脚本的if/elif块即为判断是否存在这条边,存在则进入下一节点。只有一个节点没有这个块,应该是图的终点。

于是编写脚本:

#!/usr/bin/python3

import networkx as nx
from hashlib import md5

encs = [
    # BIG bytes array, encrypted scripts, length 1000
]

def veg_xor_crypt(src, key):
    dec = b''
    for i in range(len(src)):
        dec += chr(src[i] ^ key[i % len(key)]).encode()
    return dec.decode()

def parse_graph(script):
    i_name = 'N4QKUt'
    var_name = script[: script.index(' ')]
    lines = script.splitlines()
    if lines[0] == var_name + ' = idaapi.get_byte(5127584 + ' + i_name + ')':
        if lines[1] == var_name + ' -= ord(\'a\')':
            if lines[2] == 'if ' + var_name + ' == 0:':
                # enc = [[int(lines[3][lines[3].index('=') + 2: ]), lines[4][lines[4].index('=') + 2:][2: -1].encode()]]
                enc = [int(lines[3][lines[3].index('=') + 2: ])]
                i = 5
                while lines[i].startswith('elif ' + var_name + ' == '):
                    assert(int(lines[i][lines[i].index('==') + 3: -1]) == (i - 2) // 3)
                    enc.append(int(lines[i + 1][lines[i + 1].index('=') + 2: ]))
                    i += 3
                assert(lines[i] == 'else:')
                return enc
    print("parse error: ")
    print(script)
    return None

keys = []
edges = []
dec = 'NyPGpw = idaapi.get_byte(5127584 + N4QKUt)'[11: ]
for i in encs:
    enc = i.decode()[11: ]
    t = ''
    for a, b in zip(enc, dec):
        t += chr(ord(a) ^ ord(b))
    if not t[11: ].startswith(t[: 11]):
        keys.append(None)
        edges.append([])
    else:
        keys.append(t[:11])
        edges.append(parse_graph(veg_xor_crypt(i, keys[-1].encode())))

assert(keys[8] == '9tjseMGBHR5')
assert(keys.count(None) == 1) # destination
dst_node = keys.index(None)
src_node = -1
src_edges = [287, 96, 8, 777, 496, 822, 914, 550, 273, 259, 334, 966, 331, 680, 374, 717, 965, 952, 222]

G = nx.DiGraph()
G.add_node(-1)
for i in range(len(encs)):
    G.add_node(i)


for i in src_edges:
    G.add_edge(-1, i)

for i in range(len(edges)):
    for j in edges[i]:
        G.add_edge(i, j)

path = nx.shortest_path(G, source = src_node, target = dst_node)
print(path)
s = chr(ord('a') + src_edges.index(path[1]))

for i in range(2, len(path)):
    s += chr(ord('a') + edges[path[i - 1]].index(path[i]))

print(s)
print('L3HCTF{%s}' % md5(s.encode()).hexdigest())

执行结果:

[-1, 331, 578, 255, 875, 765, 687, 209, 119, 963, 939, 443, 250, 366, 65, 504, 920, 849, 720, 893, 728, 580, 114, 665, 72, 51, 241, 519, 473, 970, 984, 557, 90, 793, 487, 67, 428, 236, 263, 24, 39, 104, 505, 491, 95, 223, 486, 798, 873, 872, 64, 229, 37, 274, 329, 601, 372, 750, 446, 3, 332, 698, 277, 740, 816, 845, 570, 828, 21, 36, 839, 770, 343, 451, 151, 994, 937, 760, 644, 9, 614, 302, 454, 153, 840, 76, 424, 352, 950, 238, 613, 497, 898, 858, 415, 205, 393, 927, 522, 705, 426]
mcaebacedaabfacacabgagbbaaeacabcbacebagaaabcdbgbdbcbdacgabfbbebababbbbbcaabdababafaccacdagdaababaaaa
L3HCTF{6584ed9fd9497981117f22a6c572caee}

double-joy

主函数里初始化了两个vm,添加到一个deque中,然后执行第一个,当返回值为1时将刚执行的vm再加到deque末尾,返回值为0时不再添加,并重复上面的过程。所以是两个vm交替执行。

sub_1D90即为vm执行程序,将switch结构恢复出来,写idc脚本反汇编:

static disasm(addr) {
    auto ins;
    auto pc = 0;
    auto tmp0, tmp1, tmp2, tmp3;
    Message("Disasm 0x%x:\n", addr);
    while (1) {
        /*
            origin stack            after operation
        top:
            num1        ===>    top:
            num2                    num1 op num2
            ...                     ...
        /**/
        ins = Byte(addr + pc);
        if (ins == 0) {
            Message("0x%04X:    add\n", pc);
            pc++;
        } else if (ins == 1) {
            Message("0x%04X:    sub\n", pc);
            pc++;
        } else if (ins == 2) {
            Message("0x%04X:    mul\n", pc);
            pc++;
        } else if (ins == 3) {
            Message("0x%04X:    div\n", pc);
            pc++;
        } else if (ins == 4) {
            Message("0x%04X:    mod\n", pc);
            pc++;
        } else if (ins == 5) {
            Message("0x%04X:    and\n", pc);
            pc++;
        } else if (ins == 6) {
            Message("0x%04X:    or\n", pc);
            pc++;
        } else if (ins == 7) {
            Message("0x%04X:    xor\n", pc);
            pc++;
        } else if (ins == 8) {
            Message("0x%04X:    store\n", pc);
            pc++;
        } else if (ins == 9) {
            Message("0x%04X:    load\n", pc);
            pc++;
        } else if (ins == 10) {
            Message("0x%04X:    neq\n", pc);
            pc++;
        } else if (ins == 11) {
            Message("0x%04X:    lt\n", pc);
            pc++;
        } else if (ins == 12) {
            Message("0x%04X:    exch\n", pc);
            pc++;
        } else if (ins == 13) {
            Message("0x%04X:    pop\n", pc);
            pc++;
        } else if (ins == 14) {
            Message("0x%04X:    push 0x%x\n", pc, Dword(addr + pc + 1));
            pc = pc + 5;
        } else if (ins == 15) {
            Message("0x%04X:    jmp 0x%04X\n", pc, (pc + 5 + Dword(addr + pc + 1)) & 0xffffffff);
            pc = pc + 5;
        } else if (ins == 16) {
            Message("0x%04X:    jnz 0x%04X\n", pc, (pc + 5 + Dword(addr + pc + 1)) & 0xffffffff);
            pc = pc + 5;
        } else if (ins == 17) {
            Message("0x%04X:    int %d\n", pc, Dword(addr + pc + 1));
            pc = pc + 5;
        } else if (ins == 18) {
            Message("0x%04X:    ret %d\n", pc, Dword(addr + pc + 1));
            pc = pc + 5;
            if (Dword(addr + pc + 1) == 0) return ;
        }
    }
}

static main() {
    disasm(0x5280);
    disasm(0x5020);
    Message("All done!\n");
}

然后读汇编,恢复出C程序:

#define CONTINUE 1
#define FINISH 0

int func1(int* buffer) {
    // unsigned int buffer[10]; // stack[0] ~ stack[9]
    int delta; // stack[0xa]
    int sum; // stack[0xb]
    int v0; // stack[0xc]
    int v1; // stack[0xd]
    int k[4]; // stack[0xe] ~ stack[0x11]
    // stack[0x12] = 0; // ??
    int i; // stack[0x13]
    int j; // stack[0x14]

    // unsigned int stack[21];

    delta = 0x75bcd15;
    sum = 0x3ade68b1;

    for (i = 0; i < 10; i++) {
        buffer[i] ^= ((i + 1) * 0x01010101);
    }

    k[0x0] = 0x494c;
    k[0x1] = 0x6f76;
    k[0x2] = 0x6520;
    k[0x3] = 0x4355;

    for (i = 0; i < 10; i += 2) {
        for (j = 0; j < 20; j++) {
            v0 = buffer[i];
            v1 = buffer[i + 1];
            v0 += (((v1 * 16) ^ (v1 / 32) + v1) ^ (sum + k[sum & 3]));
            sum += delta;
            v1 += (((v0 * 16) ^ (v0 / 32)) + v0) ^ (sum + k[(sum / 0x800) & 3]);
            buffer[i] = v0;
            buffer[i + 1] = v1;
            return CONTINUE;
        }
    }
    return FINISH;
}


int func2(int* buffer) {
    // int buffer[10]; // stack[0] ~ stack[9]
    int delta; // stack[0xa]
    int sum; // stack[0xb]
    int v0; // stack[0xc]
    int v1; // stack[0xd]
    int k[4]; // stack[0xe] ~ stack[0x11]
    // stack[0x12] = 0; // ??
    int i; // stack[0x13]
    int j; // stack[0x14]


    delta = 0x154cbf7;
    sum = 0x5eeddead;

    for (i = 0; i < 10; i++) {
        buffer[i] ^= ((i + 1) * 0x01010101);
    }

    for (i = 0; i < 10; i += 2) {
        for (j = 0; j < 20; j++) {
            v0 = buffer[i];
            v1 = buffer[i + 1];
            sum += delta;
            v0 += (v1 * 16 + k[0]) ^ (v1 + sum) ^ (v1 / 32 + k[1]);
            v1 += (v0 * 16 + k[2]) ^ (v0 + sum) ^ (v0 / 32 + k[3]);
            buffer[i] = v0;
            buffer[i + 1] = v1;
            return CONTINUE;
        }
    }
    return FINISH;
}

发现是xtea和tea,编写逆向程序:

#include <stdio.h>
int main() {
    int buffer[11] = {
        0xAEE0FAE8, 0xFC3E4101, 0x167CAD92, 0x51EA6CBE,
        0x242A0100, 0x01511A1B, 0x514D6694, 0x2F5FBFEB,
        0x46D36398, 0x79EEE3F0, 0
    };

    int delta_1 = 0x75bcd15, delta_2 = 0x154cbf7;
    int sum_1 = 0x3ade68b1, sum_2 = 0x5eeddead;
    int v0;
    int v1;
    int k_1[4] = {0x494c, 0x6f76, 0x6520, 0x4355};
    int k_2[4] = {0x5354, 0x4f4d, 0x2074, 0x6561};
    int i;
    int j;

    // init
    for (i = 0; i < 10; i += 2) {
        for (j = 0; j < 20; j++) {
            sum_1 += delta_1;
            sum_2 += delta_2;
        }
    }

    // func1 and func2
    for (i = 8; i >= 2; i -= 2) {
        for (j = 0; j < 20; j++) {
            v0 = buffer[i];
            v1 = buffer[i + 1];
            v1 -= (v0 * 16 + k_2[2]) ^ (v0 + sum_2) ^ (v0 / 32 + k_2[3]);
            v0 -= (v1 * 16 + k_2[0]) ^ (v1 + sum_2) ^ (v1 / 32 + k_2[1]);
            sum_2 -= delta_2;
            buffer[i] = v0;
            buffer[i + 1] = v1;

            v0 = buffer[i];
            v1 = buffer[i + 1];
            v1 -= (((v0 * 16) ^ (v0 / 32)) + v0) ^ (sum_1 + k_1[(sum_1 / 0x800) & 3]);
            sum_1 -= delta_1;
            v0 -= (((v1 * 16) ^ (v1 / 32)) + v1) ^ (sum_1 + k_1[sum_1 & 3]);
            buffer[i] = v0;
            buffer[i + 1] = v1;
        }
    }

    i = 0;
    // func1 and func2
    for (j = 1; j < 20; j++) {
        v0 = buffer[i];
        v1 = buffer[i + 1];
        v1 -= (v0 * 16 + k_2[2]) ^ (v0 + sum_2) ^ (v0 / 32 + k_2[3]);
        v0 -= (v1 * 16 + k_2[0]) ^ (v1 + sum_2) ^ (v1 / 32 + k_2[1]);
        sum_2 -= delta_2;
        buffer[i] = v0;
        buffer[i + 1] = v1;

        v0 = buffer[i];
        v1 = buffer[i + 1];
        v1 -= (((v0 * 16) ^ (v0 / 32)) + v0) ^ (sum_1 + k_1[(sum_1 / 0x800) & 3]);
        sum_1 -= delta_1;
        v0 -= (((v1 * 16) ^ (v1 / 32)) + v1) ^ (sum_1 + k_1[sum_1 & 3]);
        buffer[i] = v0;
        buffer[i + 1] = v1;
    }

    // func2
    v0 = buffer[0];
    v1 = buffer[1];
    v1 -= (v0 * 16 + k_2[2]) ^ (v0 + sum_2) ^ (v0 / 32 + k_2[3]);
    v0 -= (v1 * 16 + k_2[0]) ^ (v1 + sum_2) ^ (v1 / 32 + k_2[1]);
    sum_2 -= delta_2;
    buffer[0] = v0;
    buffer[1] = v1;

    for (i = 0; i < 10; i++) {
        buffer[i] ^= ((i + 1) * 0x01010101);
    }


    // func1
    v0 = buffer[0];
    v1 = buffer[1];
    v1 -= (((v0 * 16) ^ (v0 / 32)) + v0) ^ (sum_1 + k_1[(sum_1 / 0x800) & 3]);
    sum_1 -= delta_1;
    v0 -= (((v1 * 16) ^ (v1 / 32)) + v1) ^ (sum_1 + k_1[sum_1 & 3]);
    buffer[0] = v0;
    buffer[1] = v1;

    for (i = 0; i < 10; i++) {
        buffer[i] ^= ((i + 1) * 0x01010101);
    }

    puts((char*) buffer);
}

得到flag:L3HCTF{D0uBle_vM_W1th_dOubIe_TEA}

Load

拖入ida,sub_401000计算了一些值,与输入无关,然后输入被映射到名为l3hsecMapViewOfFile上,进入sub_401290,做了一些操作后判断一个值是否为0x4550,转为char[]为PE\x00\x00,正是exe文件的一个头,而sub_4017F0中将byte_404020数组解密,总共长度为10752,很大,很可疑,于是x86dbg动态调试,下断点在上面的比较位置,内存转到byte_404020,正是熟悉的MZ...PE...头,于是dump出来,在010中将MZ前面的所有字节去掉,拖入ida,判断逻辑有了。

先通过MapViewOfFile重新得到输入的flag,将flag{…}中间的字节提取,每两个16进制转为一个新的字节值(长度26 / 2 = 13),保存到一个长为9和另一个长为4的数组中,然后进入函数sub_401370变换,没完全分析这个函数,只认真看了调用的sub_4012A0函数,在草稿纸上画一下维度为2时的结果就可以知道这是det,求矩阵行列式的函数,sub_401370没仔细看了,跟delta相关的操作应该就是求逆了。

用软件求出目标矩阵的逆矩阵,再写脚本获得flag:

#!/usr/bin/python
part1 = [-8, 18, -9, 6, -13, 6, -1, 2, -1]
part2 = [13, -3, -30, 7]
flag = 'flag{'
for i in part1:
    flag += '%02x' % (i & 0xff)
for i in part2:
    flag += '%02x' % (i & 0xff)
print(flag + '}')

luuuuua

真机下安装apk并运行,点按钮后会闪退。

题目说是lua脚本,在assets中查找,两个文件,一个logo.jpg,另一个test.luatest.lua中的判断只有base64,解码后提交不正确,显然test.lua跟本题无关。

logo.jpg拖入010,尾部有一些多余的数据,这可能是加密的脚本。jadx中搜索logo.jpg,除了onCreate中设置ImageView外,lua的解析部分也有一处:

调用了getExternalFilesDir函数,但是运行程序后到/sdcard/Android/data/com.l3hsec.luuuuua/files目录下查看并没有文件,猜测这就是运行时闪退的原因,尝试将文件复制到此目录,再运行,正常了。

jadx跟入LdoFile,调用了native函数_LdoFile,ida中打开armeabi-v7a下的 libluajava.so,找到jni_LdoFile,进入sub_9BA4,看到如下代码:

010中可以看到,jpg的有效大小刚好是0x3AFA1,那么后面的部分就是加密的lua脚本。

继续分析,sub_9D80读入了一个字节,判断该字节的值;sub_9DFC中执行解密:

解密方式就是异或0x3C,尝试发现第一个字节不用异或,只有后面的字节需要,这样得到文件头部:\x1bLuaSluac脚本。尝试luadec反编译,失败,应该是改变了字节码的对应关系。

为了恢复出改变后的字节码与原字节码的对应关系,将一个编译好的lua可执行文件拖入ida(带符号信息,下载编译lua参考 https://www.lua.org/download.html#building ,改成5.3版本即可),找到luaV_execute函数;同时将apk中x86-64libluajava.so拖入ida,通过字符串'for' limit must be a number的交叉引用定位到luaV_execute函数,再在两个函数的switch块找对应的opcode。

这里找对应关系有一些小技巧,比如我先找出两个switch中的没有if判断的case块,各有5个,再比较这几个的代码,对应关系就容易找到;还有相同字符串常量出现的case块有对应关系;还有根据带符号的程序恢复出另一个程序的一些函数名,再在luaV_execute函数中找调用了相同函数的case块等等。

找了一些对应关系后就可以发现规律,再在解密出来的luac脚本中找到几个函数的位置,使用010 script修复opcode:

char table[47];
int i;
for (i = 0; i < 16; i++) table[i] = i + 13;
table[16] = 0;
for (i = 17; i < 34; i++) table[i] = i + 12;
table[34] = 0x3f;
for (i = 35; i < 47; i++) table[i] = i - 34;

int filesize = FileSize();
int poses[20] = {
    0x0037, 0x02C1, 0x0392, 0x04E4,
    0x0928, 0x0A7C, 0x10C0, 0x19EF,
    0x1B15, 0x1CF8, 0x1DF8, 0x1F3E,
    0x2200, 0x2419, 0x288A
};
int pos = 0x37;
int size;
int j;
char ins;
char type;

for (i = 0; i < 15; i++) {
    pos = poses[i];
    size = ReadInt(pos);
    pos += 4;
    Printf("Start patching codes at 0x%x, size: %d, stack size: %d.\n", pos, size, ReadByte(pos - 5));
    for (j = 0; j < size; j++) {
        ins = ReadByte(pos + 4 * j);
        WriteByte(pos + 4 * j, table[ins & 0x3f] | (ins & 0xC0));
    }
}

修复后就能使用luadec反编译了:

~/luadec/luadec/luadec  ./test.luac > test.lua

得到lua脚本:

-- Decompiled using luadec 2.2 rev: 895d923 for Lua 5.3 from https://github.com/viruscamp/luadec
-- Command line: ./sth.luac 

-- params : ...
-- function num : 0 , upvalues : _ENV
local base64 = {}
if _G.bit32 then
  local extract = (_G.bit32).extract
end
if not extract then
  if _G.bit then
    local shl, shr, band = (_G.bit).lshift, (_G.bit).rshift, (_G.bit).band
    do
      extract = function(v, from, width)
  -- function num : 0_0 , upvalues : band, shr, shl
  return band(shr(v, from), shl(1, width) - 1)
end

    end
  else
    do
      if _G._VERSION == "Lua 5.1" then
        extract = function(v, from, width)
  -- function num : 0_1
  local w = 0
  local flag = 2 ^ from
  for i = 0, width - 1 do
    local flag2 = flag + flag
    if flag <= v % flag2 then
      w = w + 2 ^ i
    end
    flag = flag2
  end
  return w
end

      else
        extract = (load("return function( v, from, width )\n\t\t\treturn ( v >> from ) & ((1 << width) - 1)\n\t\tend"))()
      end
      base64.makeencoder = function(s62, s63, spad)
  -- function num : 0_2 , upvalues : _ENV
  local encoder = {}
  for b64code,char in pairs({"B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", s62 or "+", s63 or "/", spad or "="; [0] = "A"}) do
    encoder[b64code] = char:byte()
  end
  return encoder
end

      base64.makedecoder = function(s62, s63, spad)
  -- function num : 0_3 , upvalues : _ENV, base64
  local decoder = {}
  for b64code,charcode in pairs((base64.makeencoder)(s62, s63, spad)) do
    decoder[charcode] = b64code
  end
  return decoder
end

      local DEFAULT_ENCODER = (base64.makeencoder)()
      local DEFAULT_DECODER = (base64.makedecoder)()
      local char, concat = string.char, table.concat
      base64.encode = function(str, encoder, usecaching)
  -- function num : 0_4 , upvalues : DEFAULT_ENCODER, char, extract, concat
  if not encoder then
    encoder = DEFAULT_ENCODER
  end
  local t, k, n = {}, 1, #str
  local lastn = n % 3
  local cache = {}
  for i = 1, n - lastn, 3 do
    local a, b, c = str:byte(i, i + 2)
    local v = a * 65536 + b * 256 + c
    local s = nil
    if usecaching then
      s = cache[v]
      if not s then
        s = char(encoder[extract(v, 18, 6)], encoder[extract(v, 12, 6)], encoder[extract(v, 6, 6)], encoder[extract(v, 0, 6)])
        cache[v] = s
      end
    else
      s = char(encoder[extract(v, 18, 6)], encoder[extract(v, 12, 6)], encoder[extract(v, 6, 6)], encoder[extract(v, 0, 6)])
    end
    t[k] = s
    k = k + 1
  end
  if lastn == 2 then
    local a, b = str:byte(n - 1, n)
    local v = a * 65536 + b * 256
    t[k] = char(encoder[extract(v, 18, 6)], encoder[extract(v, 12, 6)], encoder[extract(v, 6, 6)], encoder[64])
  else
    do
      do
        if lastn == 1 then
          local v = str:byte(n) * 65536
          t[k] = char(encoder[extract(v, 18, 6)], encoder[extract(v, 12, 6)], encoder[64], encoder[64])
        end
        return concat(t)
      end
    end
  end
end

      base64.decode = function(b64, decoder, usecaching)
  -- function num : 0_5 , upvalues : DEFAULT_DECODER, _ENV, char, extract, concat
  if not decoder then
    decoder = DEFAULT_DECODER
  end
  local pattern = "[^%w%+%/%=]"
  do
    if decoder then
      local s62, s63 = nil, nil
      for charcode,b64code in pairs(decoder) do
        if b64code == 62 then
          s62 = charcode
        else
          if b64code == 63 then
            s63 = charcode
          end
        end
      end
      pattern = ("[^%%w%%%s%%%s%%=]"):format(char(s62), char(s63))
    end
    b64 = b64:gsub(pattern, "")
    if usecaching then
      local cache = {}
    end
    local t, k = {}, 1
    local n = #b64
    local padding = (b64:sub(-2) == "==" and 2) or (b64:sub(-1) == "=" and 1) or 0
    for i = 1, padding > 0 and n - 4 or n, 4 do
      local a, b, c, d = b64:byte(i, i + 3)
      local s = nil
      if usecaching then
        local v0 = a * 16777216 + b * 65536 + c * 256 + d
        s = cache[v0]
        if not s then
          local v = decoder[a] * 262144 + decoder[b] * 4096 + decoder[c] * 64 + decoder[d]
          s = char(extract(v, 16, 8), extract(v, 8, 8), extract(v, 0, 8))
          cache[v0] = s
        end
      else
        do
          do
            do
              local v = decoder[a] * 262144 + decoder[b] * 4096 + decoder[c] * 64 + decoder[d]
              s = char(extract(v, 16, 8), extract(v, 8, 8), extract(v, 0, 8))
              t[k] = s
              k = k + 1
              -- DECOMPILER ERROR at PC143: LeaveBlock: unexpected jumping out DO_STMT

              -- DECOMPILER ERROR at PC143: LeaveBlock: unexpected jumping out DO_STMT

              -- DECOMPILER ERROR at PC143: LeaveBlock: unexpected jumping out IF_ELSE_STMT

              -- DECOMPILER ERROR at PC143: LeaveBlock: unexpected jumping out IF_STMT

            end
          end
        end
      end
    end
    if padding == 1 then
      local a, b, c = b64:byte(n - 3, n - 1)
      local v = decoder[a] * 262144 + decoder[b] * 4096 + decoder[c] * 64
      t[k] = char(extract(v, 16, 8), extract(v, 8, 8))
    else
      do
        if padding == 2 then
          local a, b = b64:byte(n - 3, n - 2)
          local v = decoder[a] * 262144 + decoder[b] * 4096
          t[k] = char(extract(v, 16, 8))
        end
        do
          return concat(t)
        end
      end
    end
  end
end

      local strf = string.format
      local byte, char = string.byte, string.char
      local spack, sunpack = string.pack, string.unpack
      local app, concat = table.insert, table.concat
      local stohex = function(s, ln, sep)
  -- function num : 0_6 , upvalues : strf, byte, concat
  if #s == 0 then
    return ""
  end
  if not ln then
    return s:gsub(".", function(c)
    -- function num : 0_6_0 , upvalues : strf, byte
    return strf("%02x", byte(c))
  end
)
  end
  if not sep then
    sep = ""
  end
  local t = {}
  for i = 1, #s - 1 do
    t[#t + 1] = strf("%02x%s", s:byte(i), i % ln == 0 and "\n" or sep)
  end
  t[#t + 1] = strf("%02x", s:byte(#s))
  return concat(t)
end

      local hextos = function(hs, unsafe)
  -- function num : 0_7 , upvalues : _ENV, char
  local tonumber = tonumber
  if not unsafe then
    hs = (string.gsub)(hs, "%s+", "")
    if (string.find)(hs, "[^0-9A-Za-z]") or #hs % 2 ~= 0 then
      error("invalid hex string")
    end
  end
  return hs:gsub("(%x%x)", function(c)
    -- function num : 0_7_0 , upvalues : char, tonumber
    return char(tonumber(c, 16))
  end
)
end

      local stx = stohex
      local xts = hextos
      local ROUNDS = 64
      local keysetup = function(key)
  -- function num : 0_8 , upvalues : _ENV, sunpack, ROUNDS
  assert(#key == 16)
  local kt = {0, 0, 0, 0}
  kt[1] = sunpack(">I4I4I4I4", key)
  local skt0 = {}
  local skt1 = {}
  local sum, delta = 0, 2654435769
  for i = 1, ROUNDS do
    skt0[i] = sum + kt[(sum & 3) + 1]
    sum = sum + delta & 4294967295
    skt1[i] = (sum) + kt[((sum) >> 11 & 3) + 1]
  end
  do return {skt0 = skt0, skt1 = skt1} end
  -- DECOMPILER ERROR: 1 unprocessed JMP targets
end

      local encrypt_u64 = function(st, bu)
  -- function num : 0_9 , upvalues : ROUNDS
  local skt0, skt1 = st.skt0, st.skt1
  local v0, v1 = bu >> 32, bu & 4294967295
  local sum, delta = 0, 2654435769
  for i = 1, ROUNDS do
    v0 = v0 + ((v1 << 4 ~ v1 >> 5) + v1 ~ skt0[i]) & 4294967295
    v1 = v1 + (((v0) << 4 ~ (v0) >> 5) + (v0) ~ skt1[i]) & 4294967295
  end
  bu = (v0) << 32 | v1
  return bu
end

      local enc = function(key, iv, itxt)
  -- function num : 0_10 , upvalues : _ENV, sunpack, keysetup, encrypt_u64, spack, app, concat
  assert(#key == 16, "bad key length")
  assert(#iv == 8, "bad IV length")
  if #itxt == 0 then
    return ""
  end
  local ivu = sunpack("<I8", iv)
  local ot = {}
  local rbn = #itxt
  local ksu, ibu, ob = nil, nil, nil
  local st = keysetup(key)
  for i = 1, #itxt, 8 do
    ksu = encrypt_u64(st, ivu ~ i)
    if rbn < 8 then
      local buffer = (string.sub)(itxt, i) .. (string.rep)("\000", 8 - rbn)
      ibu = sunpack("<I8", buffer)
      ob = (string.sub)(spack("<I8", ibu ~ ksu), 1, rbn)
    else
      ibu = sunpack("<I8", itxt, i)
      ob = spack("<I8", ibu ~ ksu)
      rbn = rbn - 8
    end
    app(ot, ob)
  end
  do return concat(ot) end
  -- DECOMPILER ERROR: 5 unprocessed JMP targets
end

      check_login = function(username, password)
  -- function num : 0_11 , upvalues : base64, enc
  local encoded = (base64.encode)(username)
  if encoded ~= "TDNIX1NlYw==" then
    return false
  end
  username = username .. "!@#$%^&*("
  local x = (base64.encode)(enc(username, "1qazxsw2", password))
  if x == "LKq2dSc30DKJo99bsFgTkQM9dor1gLl2rejdnkw2MBpOud+38vFkCCF13qY=" then
    return true
  end
  return false
end

    end
  end
end

读代码发现enc函数只是固定值异或明文,所以将密文加密一次即可解密。将enc前的local去掉,最后添加一行:

print(enc("L3H_Sec!@#$%^&*(", "1qazxsw2", ",\xaa\xb6u'7\xd02\x89\xa3\xdf[\xb0X\x13\x91\x03=v\x8a\xf5\x80\xb9v\xad\xe8\xdd\x9eL60\x1aN\xb9\xdf\xb7\xf2\xf1d\x08!u\xde\xa6"))

运行后输出乱码,读代码时就觉得有个地方很奇怪,是keysetup中的这行:

kt[1] = sunpack(">I4I4I4I4", key)

应该是luadec反编译出错了,将其改为:

kt[1], kt[2], kt[3], kt[4] = sunpack(">I4I4I4I4", key)

运行脚本,得到flag:
L3HCTF{20807a82-fcd7-4947-841e-db4dfe95be3e}

 

总结

题目总体较难,中途不止一次被卡住,还要多学习。

(完)