Easy_math Writeup
题目来源: DASCTF Sept X 浙江工业大学秋季挑战赛
题目链接: BUUCTF在线评测 (buuoj.cn)
运行程序, 发现只有一个"Input:". 输入非数字字符程序会崩溃, 连续输入5行数字能够得到错误提示"arr1 wrong!"
使用Ghidra打开文件, 同时用x32dbg开启动态调试. 方便起见, x32dbg附加到调试器后, 修改Ghidra的镜像基址为实际地址. (参考: Tutorial - How to rebase a module in Ghidra & IDA Pro | Guided Hacking )
在Ghidra中搜索字符串"Input:"(注意: 只有在Listing窗口才可以搜索字符串), 跳转到该字符串交叉引用的代码处, 此时可以在反汇编窗口中得到主函数的伪代码, 抽取出其中的关键逻辑如下:
// .....
do {
std::basic_istream<char,struct_std::char_traits<char>_>::operator>>
((basic_istream<char,struct_std::char_traits<char>_> *)cin_exref,local_38);
uVar6 = __RTC_CheckEsp(extraout_ECX_00,extraout_EDX_00);
local_1a0 = (int *)uVar6;
std::ios_base::operator_bool((ios_base *)((int)local_1a0 + *(int *)(*local_1a0 + 4)));
uVar7 = __RTC_CheckEsp(extraout_ECX_01,extraout_EDX_01);
uVar10 = (undefined)in_stack_fffffde8;
if ((uVar7 & 0xff) == 0) break;
thunk_FUN_005ddac0(local_38);
uVar6 = thunk_FUN_005dde80();
uVar10 = (undefined)in_stack_fffffde8;
} while ((int)uVar6 != 5);
uVar6 = thunk_FUN_005d58a0(1);
local_48 = *(uint *)uVar6;
local_44 = ((uint *)uVar6)[1];
uVar6 = thunk_FUN_005d58a0(2);
local_58 = *(uint *)uVar6;
local_54 = ((uint *)uVar6)[1];
uVar6 = thunk_FUN_005d58a0(3);
local_68 = *(uint *)uVar6;
local_64 = ((uint *)uVar6)[1];
uVar6 = thunk_FUN_005d58a0(4);
uVar1 = *(uint *)uVar6;
// local_78 = uVar1 + 0x939e98ab; // 这里是Ghidra自动生成的伪代码, 结合汇编码, 我进行
// local_74 = (((uint *)uVar6)[1] - 0x66) - (uint)(uVar1 < 0x6c616755); // 了修改
local_78 = *uVar1 - 0x6c616755; // 这里是修改过的代码
local_74 = uVar1[1] - 0x66; //
local_88 = thunk_FUN_005dcc90(local_78,local_74);
uVar6 = thunk_FUN_005dde80();
if ((int)uVar6 == 5) {
iVar4 = local_48 - (uint)local_88;
if ((iVar4 == 0x6369217d) &&
((local_44 - local_88._4_4_) - (uint)(local_48 < (uint)local_88) == 0x6153)) {
iVar4 = local_58 - (uint)local_88;
if ((iVar4 == 0x6531316f) &&
((local_54 - local_88._4_4_) - (uint)(local_58 < (uint)local_88) == 0x58)) {
iVar4 = local_68 - (uint)local_88;
if ((iVar4 == 0x31626f4e) &&
((local_64 - local_88._4_4_) - (uint)(local_68 < (uint)local_88) == 0x5f36)) {
lVar2 = local_88 +
CONCAT44(local_44 + local_54 + (uint)CARRY4(local_48,local_58) + local_64 +
(uint)CARRY4(local_48 + local_58,local_68),local_48 + local_58 + local_68
);
uVar3 = (undefined4)lVar2;
uVar10 = (undefined)lVar2;
if (lVar2 == 0xc121f9fcc23a) {
puts("You are right!");
//.....
可以看到, 在程序的第3行会读取用户输入并在后续由函数thunk_FUN_005ddac0
进行处理. 循环终止条件是变量uVar6
的值等于5. 结合程序运行时的表现情况, 猜测该变量应该是输入的行数. 进一步推断函数thunk_FUN_005ddac0
应该是存储每行输入.
继续分析, 第15行的函数thunk_FUN_005d58a0
的返回值是程序正常运行的关键. 观察它的调用方式, 结合变量uVar6
, 猜测该函数应该是对输入的数字按行进行处理.
第26行发现, 输入的第五行数字会经过单独的算法处理, 并会影响函数thunk_FUN_005dcc90
的返回值local_88
, 该变量对后续的程序逻辑也会产生影响.
静态分析发现挺复杂的, 遂决定直接用调试器调试. 首先分析下述代码:
uVar6 = thunk_FUN_005d58a0(1);
local_48 = *(uint *)uVar6;
local_44 = ((uint *)uVar6)[1];
在x32dbg中为语句MOV ECX,dword ptr [EAX]
设置断点并运行, 随便输入五行数字, 程序将会断在断点处. 逐语句执行, 确定函数thunk_FUN_005d58a0
确实会对输入的数字进行处理, 它会将输入的数字字符串转换成数字.
当输入123时, local_48
会被赋值为123, local_44
会赋值为0. 联想到程序为32位程序, 且后续代码中频繁出现"同样的运算有规律性的连续进行两次"的情况, 再结合后续出现的带借位减法指令sbb
. 猜测local_44
可能是存储的输入数字的高32位, local_48
存储低32位. 重新运行程序并输入一个大于0xffffffff的数字测试, 发现该猜测属实.
在Ghidra中重命名函数thunk_FUN_005d58a0
为parseLong
. 继续分析函数thunk_FUN_005dcc90
.
CALL thunk_FUN_005dcc90 # undefined8 thunk_FUN_005dcc90(ui
ADD ESP ,0x8
MOV dword ptr [EBP + local_88 ],EAX
MOV dword ptr [EBP + local_84 ],EDX
如上述汇编代码所示, 函数thunk_FUN_005dcc90
的返回值存储于寄存器EAX
和EDX
中. 根据前述分析, 猜测这一组返回值应当对应某个数字的高/低32位.
根据Ghidra给出的伪代码并结合汇编代码, 整理得到如下代码片段:
input5_low = input5[0] - 0x6c616755; // input5 是输入的第五个数, [0]表示取低32位
input5_hi = input5[1] - 0x66; // [1]表示取高32位
m_low = thunk_FUN_005dcc90(input5_low,input5_hi)[0];
m_hi = thunk_FUN_005dcc90(input5_low,input5_hi)[0];
len = get_input_length();
if ((int)len == 5) {
if ((input2_low - m_low == 0x6369217d) && ((input2_hi - m_hi) == 0x6153)) {
if ((input3_low - m_low == 0x6531316f) && ((input3_hi - m_hi) == 0x58)) {
if ((input4_low - m_low == 0x31626f4e) && ((input4_hi - m_hi) == 0x5f36)) {
//...... you are right.
在上述代码的第1,2行可以看到flag的字样(0x666c6167). 猜测输入5可能为字符串"flag{"的十进制值. 根据代码构造input5为: 0x66,6c61677b, 十进制: 439904987003, ASCII: flag{ .
x32dbg重新运行程序, 设置必要断点, 输入input5, 可以得到m_low == 0; m_hi == 0x10
.
因此根据上述伪代码, 可以得到正确的输入如下:
输入变量 | 高32位 | 低32位 | 输入值(10进制) | ASCII |
---|---|---|---|---|
input1 | 任意 | 任意 | 任意 | 任意 |
input2 | 0x6153 + 0x10 | 0x6369217d | 107079497490813 | acci!} |
input3 | 0x58 + 0x10 | 0x6531316f | 448374321519 | he11o |
input4 | 0x5f36 + 0x10 | 0x31626f4e | 104755080884046 | _F1boN |
input5 | 0x66 | 0x6c61677b | 439904987003 | flag{ |
正常运行程序, 输入上述值, 可验证正确.
得到flag{he11o_F1boNacci!}
对函数thunk_FUN_005dcc90
的分析如下所示:
正是因为对这个函数过于执着, 我比赛时间内根本没把这道题解出来...
在Ghidra中重命名函数thunk_FUN_005d58a0
为parseLong
. 继续分析函数thunk_FUN_005dcc90
.
CALL thunk_FUN_005dcc90 # undefined8 thunk_FUN_005dcc90(ui
ADD ESP ,0x8
MOV dword ptr [EBP + local_88 ],EAX
MOV dword ptr [EBP + local_84 ],EDX
如上述汇编代码所示, 函数thunk_FUN_005dcc90
的返回值存储于寄存器EAX
和EDX
中. 根据前述分析, 猜测这一组返回值应当对应某个数字的高/低32位. 跳转到函数thunk_FUN_005dcc90
的汇编码, 从ret
指令开始向上审计, 发现如下指令段实现了对上述两个寄存器赋值的操作:
MOV EAX ,dword ptr [EBP + local_20 ]
MOV EDX ,dword ptr [EBP + local_20 +0x4 ]
PUSH EDX
MOV ECX ,EBP
PUSH EAX
LEA EDX ,[DAT_005dce58 ] # = 01h
CALL @_RTC_CheckStackVars@8 # undefined @_RTC_CheckStackVars@8
POP EAX
POP EDX
可以看出, 变量local_20
即为返回值, 在Ghidra中重命名该变量为ret
.
该函数的伪代码主要部分如下所示:
//.....
if ((param_2 < 0) ||
((((param_2 < 1 && (param_1 < 2)) || (0 < param_2)) || ((-1 < param_2 && (200 < param_1)))))) {
ret = 0;
}
else {
thunk_FUN_005dcbb0(local_58,0x10);
thunk_FUN_005d7440();
thunk_FUN_005d5350(param_1);
local_8 = 0;
uVar10 = parseLong(0);
*(undefined4 *)uVar10 = 1;
((undefined4 *)uVar10)[1] = 0;
uVar10 = parseLong(1);
*(undefined4 *)uVar10 = 1;
((undefined4 *)uVar10)[1] = 0;
local_30 = 0;
local_40 = 2;
local_3c = 0;
while( true ) {
if ((param_2 < local_3c) || ((param_2 <= local_3c && (param_1 <= local_40)))) break;
uVar10 = parseLong(local_40 - 1);
uVar11 = parseLong(local_40 - 1);
puVar6 = (uint *)uVar11;
uVar1 = *(uint *)uVar10;
uVar2 = *puVar6;
uVar3 = *puVar6;
uVar4 = ((uint *)uVar10)[1];
uVar5 = puVar6[1];
uVar10 = parseLong(local_40);
*(int *)uVar10 = uVar1 + uVar3;
((int *)uVar10)[1] = uVar4 + uVar5 + (uint)CARRY4(uVar1,uVar2);
uVar10 = parseLong(local_40);
local_30 = *(undefined8 *)uVar10;
bVar9 = 0xfffffffe < local_40;
local_40 = local_40 + 1;
local_3c = local_3c + (uint)bVar9;
}
ret = local_30;
local_8 = 0xffffffff;
thunk_FUN_005d5da0();
}
/.......
To be continue....