首页
论坛
课程
招聘
[调试逆向] [原创]2021 CISCN gift题解(恢复符号+逆向算法)
2021-5-26 19:31 2120

[调试逆向] [原创]2021 CISCN gift题解(恢复符号+逆向算法)

2021-5-26 19:31
2120

打 CISCN 的时候正好碰上期中考试,预习高数去了qwq,最近才有时间整理一下这题的思路

恢复符号

初探报错

拖入 IDA ,在字符串窗口明显看到 golang 程序的特征
图片描述
本以为又是普通的 strip 掉符号表(go build -ldflags "-s -w"),直接 IDAGolangHelper 莽就完了,没想到他直接给我上了一课
图片描述
恢复失败了,顺着报错看看源码怎么说
图片描述
提示 beg 是 NoneType,beg 是他的上层 renameFunctions 这个函数传进去的 gopc_lntab ,我们加个 print 打印出结果,果然是 None
图片描述
图片描述

1
lookup = "FF FF FF FB 00 00" if is_be else "FB FF FF FF 00 00"

getGopcln 函数尝试查找 exe 中 .pclntab 段,并返回段的地址,如果没查找到,就调用 findGoPcln 搜索 lookup 常数,找到以后调用 check_is_gopclntab 对数据结构进行检查,通过以后返回 lookup 常数的地址

失效的原因

所以问题就是,这个 exe 里,他没有找到 gopclntab ,经过疯狂百度,找到了安全客上一位师傅写的文章(https://www.anquanke.com/post/id/215419#h2-4),解释了 pclntab 的结构

pclntab 开头 4-Bytes 是从 Go1.2 至今不变的 Magic Number: 0xFFFFFFFB ;
第 5、6个字节为 0x00,暂无实际用途;
第 7 个字节代表 instruction size quantum, 1 为 x86, 4 为 ARM;
第 8 个字节为地址的大小,32bit 的为 4,64 bit 的为 8,至此的前 8 个字节可以看作是 pclntab 的 Header;
第 9 个字节开始是 function table 的起始位置,第一个 uintptr 元素为函数(pc, Program Counter) 的个数;
第 2 个 uintptr 元素为第 1 个函数(pc0) 的地址,第 3 个 uintptr 元素为第 1 个函数结构定义相对于 pclntab 的偏移,后面的函数信息就以此类推;
直到 function table 结束,下面就是 Source file table。Source file table 以 4 字节(int32)为单位,前 4 个字节代表 Source File 的数量,后面每一个 int32 都代表一个 Source File Path String 相对于 pclntab 的偏移;
uintptr 代表一个指针类型,在 32bit 二进制文件中,等价于 uint32,在 64bit 二进制文件中,等价于 uint64 。

 

所以上面查找的 lookup 常数就是 Magic Number 了,没找到的原因可能是 Magic Number被更换,或者 pclntab 的结构变了,可以用对照的方式找到这个出问题的 exe 中的 pclntab

 

我们再自己编译一个 strip 的 golang 程序,查找 0xFFFFFFFB
图片描述
这里没开 PIE ,所以 .gopclntab 段的第一个结果就是我们要的 pclntab
我们找他的交叉引用,能找到包含指向 pclntab 指针的一个上层数据结构 moduledata ,再对 moduledata 交叉引用,有很多结果,我们取第二项
图片描述
然后在出问题的 exe 中找到对应的函数,方法有很多,截取一段特征码去另一个 exe 里搜索(别选到跳转这些跟地址有关系的指令),也可以找到函数与入口点的调用关系。

1
2
3
4
5
_rt0_amd64_linux(entry point)->
_rt0_amd64 ->
runtime_rt0_go ->
runtime_schedinit ->
runtime_modulesinit

确定这个函数在上一个函数里的位置就行了
图片描述
图片描述
定位到 moduledata 和 pclntab
图片描述


Magic Number 变成了 0xFFFFFFFA,而且结构的内容也变了,难道符号表被删掉无法恢复了吗

手动计算结构,恢复符号

GO 语言中的很多机制都需要符号等信息(比如Stack Trace),所以函数名等信息应该还保存在 exe 中。可以从需要函数名的函数入手(比如Caller),分析函数名和地址的关系。
图片描述
这是验证 moduledata 的一个函数,将 moduledata 传入 runtime_moduledataverify1
图片描述

 

函数表 (func_table) 是这个样子的,两个元素为一组,即 (func_addr, func_struct_offset)
图片描述
经调试可得知取函数名的操作在 runtime_moduledataverify1 的 runtime_funcname 中,runtime_moduledataverify1 中有对比 Magic Number 的指令 cmp ebx, 0FFFFFFFBh ,所以可以查找常数 0xFFFFFFFA 在题目文件中定位这个函数。
图片描述
函数表在 moduledata 的偏移变成了 80h
图片描述
Function Struct 在前面链接里的文章有提到

1
2
3
4
5
6
7
8
9
10
11
12
struct Func
{
    uintptr      entry;     // start pc
    int32        name;      // name (offset to C string)
    int32        args;      // size of arguments passed to function
    int32        frame;     // size of function frame, including saved caller PC
    int32        pcsp;      // pcsp table (offset to pcvalue table)
    int32        pcfile;    // pcfile table (offset to pcvalue table)
    int32        pcln;      // pcln table (offset to pcvalue table)
    int32        nfuncdata; // number of entries in funcdata list
    int32        npcdata;   // number of entries in pcdata list
};

图片描述
图片描述
整理得到公式:

1
2
3
string_addr[i] = *(moduledata+8)+ *(func_struct+8)
func_struct = *(func_table+0x10*i+8)+func_table
func_table = *(moduledata+0x80)

写个小脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
moduledata = 0x55DA80
func_table = Qword(moduledata+0x80)
func_table_size = Qword(Qword(moduledata)+0x8) * 0x10
string_table = Qword(moduledata+0x8)
 
start_addr = func_table+0x8
end_addr = start_addr + func_table_size
while(start_addr<end_addr):
    func_struct = func_table+Qword(start_addr)
    func_entry = Qword(func_struct)
    string_addr = string_table + Dword(func_struct+8)
    print 'renaming '+hex(func_entry)+' to '+GetString(string_addr)
    del_items(func_entry, ida_bytes.DELIT_SIMPLE,1)
    add_func(func_entry)
    set_name(func_entry,GetString(string_addr),SN_NOCHECK)
    start_addr += 0x10

图片描述
函数名终于出现了

逆向算法

程序逻辑

双击运行,题目先输出一段 flag ,然后下一个字符的输出变得越来越慢,而且 CPU 占用很高(看来不是 Sleep 导致的),看来是要我们优化算法。
图片描述
查看 main_main 函数
图片描述
解密字符串,每一位异或 0x66 后再输出
图片描述
这里 IDA 的反编译结果是有问题的(因为 golang 使用栈传参,IDA 不能正确识别函数的参数,可以参考汇编)
每轮执行完 main_wtf 后,都会根据他的运算结果输出一个字符,追溯 rcx 那几个寄存器,可得到 main_wtf 的参数,main_wtf(0,i,slice_addr,slice_size,slice_size)
图片描述
反编译 main_wtf
图片描述
原来是递归,怪不得那么慢,再看看 main_goooo()
图片描述
到这已经可以整理出输出 flag 的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
QWORD qword_5B20E8;
int main_goooo(QWORD* array){
    QWORD v2[5] = {0,0,0,0,0};
    for(QWORD i=0;i<size;i++)
        v2[array[i]]^=1;
    return v2[1]==0 && v2[3]==0;
}
void main_wtf(QWORD depth,QWORD replace,QWORD* array,QWORD size,QWORD size2){
    array[depth] = replace;
    if(depth==size-1){
        if(main_goooo(array,size)){
            qword_5B20E8 = (qword_5B20E8+1)%17;
        }
    }else{
        for(QWORD i=1;i<=4;i++)
            main_wtf(depth+1,i,array,size,size2);
    }
}
int main(){
    QWORD qword_55C080[] = {1, 3, 6, 9, 0xA,0x0B,0x0C,0x0D,0x0E,0x0F,0x10,0x11,0x12,0x14,0x19,0x1E,0x28,0x42,0x66,0x0A0,0x936,0x3D21,0x149A7,0x243AC,0x0CB5BE,0x47DC61,0x16C0F46,0x262C432,0x4ACE299,0x10FBC92A,0x329ECDFD,0x370D7470};
    int n = 0;
    while(n<32){
        QWORD *slice_addr = (QWORD*)malloc(qword_55C080[i]*sizeof(QWORD));
        qword_5B20E8 = 0;
        for(QWORD i=1;i<=4;i++)
            main_wtf(0,i,slice_addr,qword_55C080[i],qword_55C080[i]);
        printf("%c",table[qword_5B20E8^0x66]);
        n++;
    }
}

求解

因为跑后一个数据的时候,之前已经算出的结果还会重复计算,所以最开始想拿 unicorn 做一个记忆化,在用到已经算出来的结果时直接返回,但是后面的数据实在太大(比如 0x370D7470),递归这么多层肯定是不行的,还是分析一下吧。
读几遍代码发现,其实这个代码就是创建了一个 n 位数,然后使用递归把每一位填上1~4的数字,最后判断其中1和3的数量是不是偶数,可以用动态规划求解。
因为只有1~3的数量影响结果,所以每一个数字的情况可以分为四种:
偶数个1 偶数个3
偶数个1 奇数个3
奇数个1 偶数个3
奇数个1 奇数个3
而数字每增加一位,如果增加的是 1 或者 3,就把原来奇数个1 或者奇数个3 的情况变成偶数个1 或者偶数个 3,如果增加的是 2 或 4,奇偶情况不改变。
根据这个可以写出状态转移方程。

1
2
3
4
1 3 = (偶1 3+ (奇1 3+ (偶1 3*2
1 3 = (偶1 3+ (奇1 3+ (偶1 3*2
1 3 = (偶1 3+ (奇1 3+ (奇1 3*2
1 3 = (偶1 3+ (奇1 3+ (奇1 3*2

最后别忘了把计算结果映射成字符串
图片描述
python 脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import binascii
f = [2,1,1,0]
table = binascii.unhexlify('5F53055504525E54')[::-1]
table += binascii.unhexlify('570003025156540750')[::-1]
flag = ''
qword_55C080 = [1, 3, 6, 9, 0xA,0x0B,0x0C,0x0D,0x0E,0x0F,0x10,0x11,0x12,0x14,0x19,0x1E,0x28,0x42,0x66,0x0A0,0x936,0x3D21,0x149A7,0x243AC,0x0CB5BE,0x47DC61,0x16C0F46,0x262C432,0x4ACE299,0x10FBC92A,0x329ECDFD,0x370D7470]
k = 0
for i in range(0x370D7470):
    if(i == qword_55C080[k]-1): #因为 f 的初始值是计算一次的结果,所以要减一
        flag+= chr(table[f[0]]^0x66)
        k+=1
    tmp0 = (2*f[0]+f[1]+f[2])%17
    tmp1 = (2*f[1]+f[0]+f[3])%17
    tmp2 = tmp1
    tmp3 = (2*f[3]+f[1]+f[2])%17
    f = [tmp0,tmp1,tmp2,tmp3]
 
print(flag)

得到 flag: CISCN{4b445b3247c45344c54c44734445452c}


[看雪官方培训] Unicorn Trace还原Ollvm算法!《安卓高级研修班》2021年秋季班火热招生!!

上传的附件:
收藏
点赞2
打赏
分享
最新回复 (2)
雪    币: 9515
活跃值: 活跃值 (629)
能力值: ( LV8,RANK:132 )
在线值:
发帖
回帖
粉丝
CrazymanArmy 活跃值 2 2021-5-26 19:59
2
0
v0id爷tql
雪    币: 384
活跃值: 活跃值 (275)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
N1ptune 活跃值 2021-5-26 22:21
3
0
tql,膜
游客
登录 | 注册 方可回帖
返回