看雪2023 KCTF年度赛 | 第12题·深入内核-设计思路及解析

WriteUp 7个月前 admin
103 0 0

看雪2023 KCTF年度赛 | 第12题·深入内核-设计思路及解析

这是一场人类与超智能AI的“生死”较量

请立刻集结,搭乘SpaceX,前往AI控制空间站

智慧博弈  谁能问鼎


看雪·2023 KCTF 年度赛于9月1日中午12点正式开赛!比赛基本延续往届模式,设置了难度值、火力值和精致度积分。由此来引导竞赛的难度和趣味度,使其更具挑战性和吸引力,同时也为参赛选手提供了更加公平、有趣的竞赛平台。

*注意:签到题持续开放,整个比赛期间均可提交答案获得积分


今日中午12:00第十二题《深入内核》已截止答题,该题仅有xxx支战队成功提交flag,一起来看下该题的设计思路和解析吧。



出题团队简介


出题战队:中午吃什么

战队成员:wx_孤城、瑞皇、hmfzy、charon561

看雪2023 KCTF年度赛 | 第12题·深入内核-设计思路及解析


设计思路


公开的用户名和序列号
User-Name : 4070382B95A4F0ED
Serial-Number : 47a62c6eb8a72031a27b89abc3d976f7

题目答案
User-Name : KCTF
Serial-Number : 35090e1336f1d1e872ba798256db1bfb

设计说明
一个Windows的CrakeMe,难度中上。设计了一个简单的脚本解释器
脚本明文经过加密然后bin2hex转成数组存入全局变量。


验证算法为
文本 = 用户名 + “KCTF2023”
文本 = 自定义异或加密(文本)
文本 = base64(文本)
文本 = 小写32位MD5(文本)
最后判断文本等于序列号则为成功

成功条件为
小写32位MD5(base64(自定义异或加密(用户名 + “KCTF2023”))) == 序列号

破解难点
有大量无效代码干扰,增加程序体积,有2个假验证算法干扰破解者,由于函数过大IDA F5会失败。
例如:if(xxx && 1 + 1 == 3) printf(“Success”)
例如:if(xxx && 1 + 1 == 3) printf(“Error”)

题目和源码均已上传附件,运行图如下:
看雪2023 KCTF年度赛 | 第12题·深入内核-设计思路及解析


赛题解析


解法一:分析程序原理

本题解析由看雪专家 GreatIchild 提供:

看雪2023 KCTF年度赛 | 第12题·深入内核-设计思路及解析


程序中的一些关键函数加了混淆,直接把 0x49A0E8 处 patch 为 double 类型的 100.0 ,再声明其类型为 const double 这样 ida F5 就能直接去除无关的逻辑。


题目实现了一个虚拟机,程序启动后会先解密得到虚拟机执行的命令,之后来到函数 0x460D0E 执行:

char __thiscall global_vtable_2_func_18(C *this){  C *v1; // ebx  int _pc; // esi  B *ins_start; // edi MAPDST  char *v6; // esi  _DWORD *v7; // eax  const char *v8; // ecx  unsigned int v9; // edx  unsigned int v10; // edi  _BYTE *v11; // ebx  char v12; // al  int v14; // [esp+8h] [ebp-6Ch]  char v16[88]; // [esp+1Ch] [ebp-58h] BYREF   v1 = this;  if ( v1->instructions.start == v1->instructions.finish )    return 0;  this->pc = 0;  ins_start = this->instructions.start;  if ( v1->instructions.finish - ins_start > 0 )  {    _pc = 0;    ins_start = v1->instructions.start;    do    {      if ( _pc == -1 )      {        v1->pc = 0;        _pc = 0;      }      if ( !(*((unsigned __int8 (__thiscall **)(C *, B *))v1->vtable + 15))(v1, &ins_start[_pc]) )      {        // show error message      }      _pc = v1->pc + 1;      v1->pc = _pc;      ins_start = v1->instructions.start;    }    while ( _pc < v1->instructions.finish - ins_start );  }  return 1;}

v1->vtable + 15 就是虚表中的第 15 项,地址为 0x4525EB 。


相关数据结构:

struct A{  int field_0;  std::string s0;  int i1;  int i2;  int i3;  std::string s4;  std::string s5;  std::string s6;}; struct std::vector$A${  A *start;  A *finish;  A *end_of_storage;}; struct B{  int is_label;  int field_4;  std::string s1;  std::string s2;  std::vector$A$ vector;}; struct std::vector$B${  B *start;  B *finish;  B *end_of_storage;}; struct RBTree // std::map<std::string, std::string>{  struct RBTree *left;  struct RBTree *parent;  struct RBTree *right;  char color;  char isnil;  char padding[2];  std::string key;  std::string value;}; struct C{  int **vtable;  RBTree *tree;  int node_count;  std::vector$B$ instructions;  int pc;  char valid_data;};

将解密之后的指令所在的内存 dump 出来,写脚本恢复原始指令:

def vm(ins):    opcode = int(ins[2])    operands = ins[3]    if opcode == 0x21344D4938CE0640:        print('system({0})'.format(repr(operands[0][0].decode())))    elif opcode == 0x3820EA1739C3E154:        print('map[{0}] = hex(vigenere(map[{0}], "恭喜发财".encode("gbk")))'.format(repr(operands[0][4])))    elif opcode == 0x4B0134D06B40680:        print('if (len(map[{0}]) {1} {2}) goto {3}'.format(repr(operands[0][4]), operands[1][5].decode(), operands[2][1], operands[3][6].decode()))    elif opcode == 0x5304FD305CA8C22A:        print('sleep({})'.format(operands[0][1]))    elif opcode == 0x5AC009C0F14B76E8:        print('map[{0}] = input()'.format(repr(operands[0][4])))    elif opcode == 0x6975C7A3C07CD226:        print('map[{0}] += {1}'.format(repr(operands[0][4]), repr(operands[1][0])))    elif opcode == 0x7929CBF0A1496FB0:        print('map[{0}] = base64encode(map[{0}])'.format(repr(operands[0][4])))    elif opcode == 0x885F75A1461ECEBB:        print('if (map[{0}] == map[{1}]) goto {2}'.format(repr(operands[0][4]), repr(operands[1][4]), operands[2][6].decode()))    elif opcode == 0x8DB9D83D80004137:        print('print({})'.format(repr(operands[0][0].replace(b'\n', b'n').decode('gbk'))))    elif opcode == 0xA43CBF9D015186F1:        if len(operands): code = operands[0][1]        else: code = 0        print('exit({0})'.format(code))    elif opcode == 0xAA9C8E70F01F8D61:        print('map[{0}] = map[{1}]'.format(repr(operands[0][4]), repr(operands[1][4])))    elif opcode == 0xDDEEFF2200112233:        print('nop')    elif opcode == 0xE56D33B21C50A892:        print('goto {0}'.format(operands[0][6].decode()))    elif opcode == 0xF4CC06C2E34200F0:        print('nop 0')    elif opcode == 0xF4CC06F2E3420459:        print('nop 1')    elif opcode == 0xFD1D1DFB19850CA1:        print('map[{0}] = md5(map[{0}])'.format(repr(operands[0][4])))    else:        assert False, hex(opcode) mem_offset = 0xea0000mem_size = 0x20000mem_dump = open('MEM_00EA0000_00020000.mem', 'rb').read() ins_start = 0xeb4b00ins_end = 0xeb62a4 def read_dword(addr):    assert mem_offset <= addr < mem_offset + mem_size    return int.from_bytes(mem_dump[addr - mem_offset: addr - mem_offset + 4], 'little') def read_data(addr, size):    assert mem_offset <= addr < mem_offset + mem_size - size    return mem_dump[addr - mem_offset: addr - mem_offset + size] def read_str(addr):    size = read_dword(addr + 0x10)    cap = read_dword(addr + 0x14)    if cap > 0x10: addr = read_dword(addr)    return read_data(addr, size) def read_A(addr):    return (read_str(addr + 4), read_dword(addr + 0x1c), read_dword(addr + 0x20), read_dword(addr + 0x24), read_str(addr + 0x28), read_str(addr + 0x40), read_str(addr + 0x58)) def read_B(addr):    B = (read_dword(addr), read_dword(addr + 4), read_str(addr + 0x20), [])    A_start = read_dword(addr + 0x38)    A_end = read_dword(addr + 0x3c)    for addr in range(A_start, A_end, 0x70):        B[-1].append(read_A(addr))    return B ins = []for addr in range(ins_start, ins_end, 0x44):    ins.append(read_B(addr)) for i in range(len(ins)):    t = ins[i]    if t[0]:        print(t[3][0][6].decode() + ':')    else:        # print(i, end=': ')        print('    ', end='')        vm(ins[i])

得到输出:

    sleep(25)    print('                                           n')    sleep(25)    print('                  _oo0oo_                  n')    sleep(25)    print('                 o8888888o                 n')    sleep(25)    print('                 88" . "88                 n')    sleep(25)    print('                 (| -_- |)                 n')    sleep(25)    print('                 0\  =  /0                 n')    sleep(25)    print("               ___/`---'\___               n")    sleep(25)    print("             .'\.\|     |/./'.             n")    sleep(25)    print('            / \.\||  :  ||/./ \            n')    sleep(25)    print('           / _||||| -:- |||||- \           n')    sleep(25)    print('          | | \.\.\  -  /././ | |          n')    sleep(25)    print("          | \_|  ''\---/''  |_/ |          n")    sleep(25)    print("          \  .-\__  '-'  ___/-. /          n")    sleep(25)    print("        ___'. .'  /--.--\  `. .'___        n")    sleep(25)    print('     ."" '<  `.___\_<|>_/___.' >' "".      n')    sleep(25)    print('    | | :  `- \`.;`\ _ /`;.`/ - ` : | |    n')    sleep(25)    print('    \  \ `_.   \_ __\ /__ _/   .-` /  /    n')    sleep(25)    print("=====`-.____`.___ \_____/___.-`___.-'===== n")    sleep(25)    print("                  `=---='                  n")    sleep(25)    print('                                           n')    sleep(25)    print('             佛祖保佑 永无BUG              n')    sleep(25)    print('            看雪 KCTF2023年度赛            n')    sleep(25)    print('            出题战队:中午吃什么            n')    sleep(25)    print('                                           n')    sleep(25)    print('                                           n')    nop    sleep(25)    print('请输入用户名:n--> ')    map[b'2569430338759937617'] = input()    nop    sleep(25)    print('请输入序列号:n--> ')    map[b'1509181994979340817'] = input()    nop    nop 0    nop 1    if (len(map[b'1509181994979340817']) != 32) goto 10547232137042693405    nop    map[b'18097274335226857185'] = map[b'2569430338759937617']    map[b'18097274335226857185'] += b'KCTF2023'    map[b'18097274335226857185'] = hex(vigenere(map[b'18097274335226857185'], "恭喜发财".encode("gbk")))    map[b'18097274335226857185'] = base64encode(map[b'18097274335226857185'])    map[b'18097274335226857185'] = md5(map[b'18097274335226857185'])    nop    if (map[b'18097274335226857185'] == map[b'1509181994979340817']) goto 15317636321340901566    goto 15315557398280875957    nop10547232137042693405:    sleep(25)    print('errornnn')    system('pause')    exit(0)    nop15317636321340901566:    sleep(25)    print('Success GoodJob!nnn')    system('pause')    exit(0)    nop15315557398280875957:    sleep(25)    print('errornnn')    system('pause')    exit(0)

求解:

#!/usr/bin/env python3 from base64 import b64encodefrom hashlib import md5 def vigenere(x, y):    return bytes(x[i] ^ y[i % len(y)] for i in range(len(x))) def solve(username):    return md5(b64encode(vigenere(username + b'KCTF2023', "恭喜发财".encode("gbk")).hex().upper().encode())).hexdigest()  '''User-Name : 4070382B95A4F0EDSerial-Number : 47a62c6eb8a72031a27b89abc3d976f7''' assert solve(b'4070382B95A4F0ED') == '47a62c6eb8a72031a27b89abc3d976f7'print(solve(b'KCTF')) # 35090e1336f1d1e872ba798256db1bfb



解法二:利用设计缺陷巧解

本题解析由看雪专家 奔跑的阿狸 提供:

看雪2023 KCTF年度赛 | 第12题·深入内核-设计思路及解析


1、直接FindCrypt一波,发现MD5 和 base64


2、然后hook 算法相关函数。sub_403CFE、sub_403BA0、sub_4011C2,并打印参数,会发现:OEQ5N0Y4ODI4NDlBODA4NDgwOTI4RTg2RjE5MkY3ODJGMkU0OUJGNDg1OTI4MEY1

043998a8  4f 45 51 35 4e 30 59 34 4f 44 49 34 4e 44 6c 42  OEQ5N0Y4ODI4NDlB043998b8  4f 44 41 34 4e 44 67 77 4f 54 49 34 52 54 67 32  ODA4NDgwOTI4RTg2043998c8  52 6a 45 35 4d 6b 59 33 4f 44 4a 47 4d 6b 55 30  RjE5MkY3ODJGMkU0043998d8  4f 55 4a 47 4e 44 67 31 4f 54 49 34 4d 45 59 31  OUJGNDg1OTI4MEY1

3、这段看着就像base64结果,直接将其md5以下,发现就是序列号


4、解base64:
8D97F882849A808480928E86F192F782F2E49BF4859280F5

5、然后继续寻找上层代码,期间又刷新下题目发现有人做出来了。忽然反应过来,直接拦截KCTF的base64不就行了。

6、于是乎,拦截到RjJFNDlCRjRGQ0UxRTY4MDhCOTdGRDgx,md5一下

35090e1336f1d1e872ba798256db1bfb


看雪2023 KCTF年度赛 | 第12题·深入内核-设计思路及解析
第十三题《共存之道》已开赛!

看雪2023 KCTF年度赛 | 第12题·深入内核-设计思路及解析


截至发文,本题已有2支战队成功提交flag,他们分别是:

看雪2023 KCTF年度赛 | 第12题·深入内核-设计思路及解析


目前攻击方排名前10如下:

看雪2023 KCTF年度赛 | 第12题·深入内核-设计思路及解析


在这个充满变数的赛场上,没有人能够预料到最终的结局。有时,优势的领先可能只是一时的,一瞬间的失误就足以颠覆一切。而那些一直默默努力、不断突破自我的人,往往会在最后关头迎头赶上,成为最耀眼的存在。

谁能保持领先优势?谁能迎头赶上?谁又能突出重围成为黑马?

看雪2023 KCTF年度赛 | 第12题·深入内核-设计思路及解析

看雪2023 KCTF年度赛 | 第12题·深入内核-设计思路及解析

球分享

看雪2023 KCTF年度赛 | 第12题·深入内核-设计思路及解析

球点赞

看雪2023 KCTF年度赛 | 第12题·深入内核-设计思路及解析

球在看


看雪2023 KCTF年度赛 | 第12题·深入内核-设计思路及解析

点击阅读原文进入比赛

原文始发于微信公众号(看雪学苑):看雪2023 KCTF年度赛 | 第12题·深入内核-设计思路及解析

版权声明:admin 发表于 2023年10月2日 下午6:00。
转载请注明:看雪2023 KCTF年度赛 | 第12题·深入内核-设计思路及解析 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...