共三道题,前边几道题比较简单,就不写 writeup 了。
WxyVM
二话不说拖进 IDA,找到 main() 后 F5,顺便给作用明显的变量命个名:
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
char v4; // [sp+Bh] [bp-5h]@1
signed int i; // [sp+Ch] [bp-4h]@3
puts("[WxyVM 0.0.1]");
puts("input your flag:");
scanf("%s", &scanf_buffer_bytes);
v4 = 1;
sub_4005B6();
if ( strlen(&scanf_buffer_bytes) != 24 )
v4 = 0;
for ( i = 0; i <= 23; ++i )
{
if ( *(&scanf_buffer_bytes + i) != goal_dword[i] )
v4 = 0;
}
if ( v4 )
puts("correct");
else
puts("wrong");
return 0LL;
}
代码逻辑很清晰,先读字符串到 scanf_buffer_bytes
,然后调用 sub_4005B6()
,推测是进行加密,子函数跑完后,让加密后的数据跟 goal_dword
进行比对。读懂代码后直接跟进去子函数,同样,先对意图明显的变量进行命名:
__int64 func()
{
unsigned int v0; // ST04_4@3
__int64 result; // rax@3
signed int i; // [sp+0h] [bp-10h]@1
char v3; // [sp+8h] [bp-8h]@3
for ( i = 0; i <= 14999; i += 3 )
{
v0 = raw_data[(signed __int64)i];
v3 = raw_data[(signed __int64)(i + 2)];
result = v0;
switch ( v0 )
{
case 1u:
result = raw_data[(signed __int64)(i + 1)];
*(&scanf_buffer_bytes + result) += v3;
break;
case 2u:
result = raw_data[(signed __int64)(i + 1)];
*(&scanf_buffer_bytes + result) -= v3;
break;
case 3u:
result = raw_data[(signed __int64)(i + 1)];
*(&scanf_buffer_bytes + result) ^= v3;
break;
case 4u:
result = raw_data[(signed __int64)(i + 1)];
*(&scanf_buffer_bytes + result) *= v3;
break;
case 5u:
result = raw_data[(signed __int64)(i + 1)];
*(&scanf_buffer_bytes + result) ^= *(&scanf_buffer_bytes + raw_data[(signed __int64)(i + 2)]);
break;
default:
continue;
}
}
return result;
}
读一下代码,大意是从 raw_data
依次读取数据,3 Bytes 为一组。Byte 1 用来 switch,Byte 2 用来指定相对于 buffer
的偏移量,Byte 3 用以参与计算。
所以代码到这就很明显了,从 goal_dword
逆操作还原出 scanf_buffer_bytes
即可。不过代码里有两个坑:
- 从
goal_dword
取数据时需要每 4 位只保留最低位。 - 会溢出
#IDAPython
import sys
raw_data = 0x00000000006010C0
answer = 0x0000000000601060
buffer = 0x0000000000604B80
# 逆操作
op = {
1: lambda x, y: (x-y)%sys.maxint+1 if x-y<0 else x-y,
# 这里为了模拟溢出,比较粗暴
2: lambda x, y: (x+y)%maxint,
3: lambda x, y: x^y,
4: lambda x, y: x/y,
}
for i in range(24):
PatchByte(buffer+i, Byte(answer+4*i))
for i in range(14997, -1, -3):
v0 = Byte(raw_data + i)
v1 = Byte(raw_data + i + 1)
v2 = Byte(raw_data + i + 2)
if v0 > 0 and v0 < 5:
PatchByte(buffer+v1, op[v0](Byte(buffer+v1), v2))
elif v0 == 5:
PatchByte(buffer+v1, Byte(buffer+v1)^Byte(buffer+v2))
break
else:
continue
# find flag at buffer
maze
照常拖进 IDA 顺势 F5,读代码:
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
signed __int64 offset; // rbx@4
signed int chr; // eax@5
bool v5; // bp@5
bool v6; // al@8
const char *msg; // rdi@19
__int64 y; // [sp+0h] [bp-28h]@1
__int32 x; // 读汇编后觉得 &y+1 视为 &x 更好
x = 0L;
y = 0LL;
puts("Input flag:");
scanf("%s", &buffer, 0LL);
if ( strlen(&buffer) != 24 || strncmp(&buffer, "nctf{", 5uLL) || *(&byte_6010BF + 24) != 125 )
{
// assert len(buffer) == 24 && buffer.startswith("nctf{") && buffer.endswith("}")
wrong_and_exit:
puts("Wrong flag!");
exit(-1);
}
offset = 5LL; // 从 { 后面第一个字符开始,后边 ++offset 遍历 buffer
if ( strlen(&buffer) - 1 > 5 ) // 恒为真
{
while ( 1 )
{
chr = *(&buffer + offset);
v5 = 0;
if ( chr > 'N' )
{
chr = (unsigned __int8)chr;
if ( (unsigned __int8)chr == 'O' )
{
v6 = func_O((_DWORD *)&x); // v6 = x-- > 0;
goto assign_v6_to_v5_and_goto_label_15;
}
if ( chr == 'o' )
{
v6 = func_o((int *)&x); // v6 = x++ < 8;
goto assign_v6_to_v5_and_goto_label_15;
}
}
else
{
chr = (unsigned __int8)chr;
if ( (unsigned __int8)chr == '.' )
{
v6 = func_dot(&y); // v6 = y-- > 0;
goto assign_v6_to_v5_and_goto_label_15;
}
if ( chr == '0' )
{
v6 = func_0((int *)&y); // v6 = y++ < 8;
assign_v6_to_v5_and_goto_label_15:
v5 = v6;
goto LABEL_15;
}
}
// 分析到下边发现是走迷宫,O左 o右 .上 0下
LABEL_15:
if ( !(unsigned __int8)check(asc_601060, x, y) ) // SHIDWORD(y) == x, 检查是否撞到墙壁
// asc_601060: 8x8 array
// ******
// * * *
// *** * **
// ** * **
// * *# *
// ** *** *
// ** *
// ********
// return 1 if asc[x][y] == (' ' or '#') else 0
goto wrong_and_exit; // assert return == 1;
if ( ++offset >= strlen(&buffer) - 1 ) // 自增,如果下一个已经是 },则:
{
if ( v5 ) // 防止走出迷宫的范围
break;
wrong_and_exit_2:
msg = "Wrong flag!";
goto show_msg_and_exit;
}
}
}
if ( *(&asc_601060[8 * (signed int)y] + x) != '#' ) // 最后要以 # 为终点,否则 wrong
goto wrong_and_exit_2;
msg = "Congratulations!";
show_msg_and_exit:
puts(msg);
return 0LL;
}
// 综上,就是走迷宫,从 (0, 0) 走到 # 处即为 flag
WxyVM2
丢进 IDA,发现 “Sorry, this node is too big to display”,顿时觉得水深 [一脸黑线]。我不管!读汇编好费时的!F5 之后放一边,居然被我等来了 C 代码,哈哈哈,那就继续分析。
一看发现两万多行,emmm,先读下头尾。很简单,scanf
到 0x694100
处,一番操作后与 0x6940600
进行比对而已(依旧是那个 byte 与 dword 比对的坑,小心)。所以我就去头去尾,把代码丢 Sublime 里分析了。
看了一会,发现了猫腻:
scanf
放进去那个 buffer
的范围是 0x694100 ~ 0x694118
,最终比对的目标是 0x694060 ~ 0x6940C0
,而且后者的数据没变过。又发现中间那两万多行很多跟解题无关,就把对无关地址的操作全剔除了,剩下两千多行有效操作。
然后思路就很清晰了,反向操作即可。先写个 py 脚本逆一下操作顺序:
l = [i for i in open('operations.txt')]
f = open('operations-reversed.txt', 'w')
for i in reversed(l):
f.write(i)
f.close()
# 没有 close() 至少也 flush() 一下…刚开始这里忘了,卡了很久,很奇怪为啥数据少了一小部分…
然后在 Sublime 里把 --
换成 -= 1
,++
同理。再写个很丑的正则替换一下各个操作:(w+)_(w+) += (.*?);
to $1(0x$2) -= $3;
,这里是加法变减法,其他同理。
由于看起来很可能会有溢出,我就直接用 C 写了,省得像之前那样活生生地在 py 里模拟溢出…
#include <stdio.h>
#include <inttypes.h>
uint8_t goal[25] = {
0xC0, 0x85, 0xF9, 0x6C, 0xE2, 0x14, 0xBB, 0xE4, 0x0D,
0x59, 0x1C, 0x23, 0x88, 0x6E, 0x9B, 0xCA, 0xBA, 0x5C,
0x37, 0xFF, 0x48, 0xD8, 0x1F, 0xAB, 0xA5 };
#define byte(i) goal[i-0x694100]
int main() {
// 这里放上边得到的逆操作们
for (int i=0; i<25; ++i)
printf("%c", goal[i]);
return 0;
}
Over,至此南邮的训练平台逆向入门题 All Clear~
Here are two useful pages: