打 CISCN 的时候正好碰上期中考试,预习高数去了qwq,最近才有时间整理一下这题的思路
拖入 IDA ,在字符串窗口明显看到 golang 程序的特征
本以为又是普通的 strip 掉符号表(go build -ldflags "-s -w"),直接 IDAGolangHelper 莽就完了,没想到他直接给我上了一课
恢复失败了,顺着报错看看源码怎么说
提示 beg 是 NoneType,beg 是他的上层 renameFunctions 这个函数传进去的 gopc_lntab ,我们加个 print 打印出结果,果然是 None
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 里搜索(别选到跳转这些跟地址有关系的指令),也可以找到函数与入口点的调用关系。
确定这个函数在上一个函数里的位置就行了
定位到 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 在前面链接里的文章有提到
整理得到公式:
写个小脚本
函数名终于出现了
双击运行,题目先输出一段 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 的逻辑
因为跑后一个数据的时候,之前已经算出的结果还会重复计算,所以最开始想拿 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,奇偶情况不改变。
根据这个可以写出状态转移方程。
最后别忘了把计算结果映射成字符串
python 脚本:
得到 flag: CISCN{4b445b3247c45344c54c44734445452c}
lookup
=
"FF FF FF FB 00 00"
if
is_be
else
"FB FF FF FF 00 00"
lookup
=
"FF FF FF FB 00 00"
if
is_be
else
"FB FF FF FF 00 00"
_rt0_amd64_linux(entry point)
-
>
_rt0_amd64
-
>
runtime_rt0_go
-
>
runtime_schedinit
-
>
runtime_modulesinit
_rt0_amd64_linux(entry point)
-
>
_rt0_amd64
-
>
runtime_rt0_go
-
>
runtime_schedinit
-
>
runtime_modulesinit
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
};
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
};
string_addr[i]
=
*
(moduledata
+
8
)
+
*
(func_struct
+
8
)
func_struct
=
*
(func_table
+
0x10
*
i
+
8
)
+
func_table
func_table
=
*
(moduledata
+
0x80
)
string_addr[i]
=
*
(moduledata
+
8
)
+
*
(func_struct
+
8
)
func_struct
=
*
(func_table
+
0x10
*
i
+
8
)
+
func_table
func_table
=
*
(moduledata
+
0x80
)
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
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
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
+
+
;
}
}
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!