根据sayhi
师傅的思路总结。需要依赖于动态调试。实验用的源码贴在最后面。
实验源程序:http://file.eonew.cn/ctf/re/crackme。
目录
题目类型
可以用于非算法类的逆向题目,对于逐个字符比较的题目由为简单,可以忽略ollvm
的混淆作用,只需要看核心代码即可。
思路
对于我们的输入下硬件断点。然后查看程序的运行情况:
- 查看我们的输入是否有变化
- 注意我们的输入是否被转移
- 对于断点前后的代码要仔细研究
实例
实验的环境是 IDA7.0 。
这个是我自己用obfuscator-llvm
编译的一个简单的程序,但是经过混淆之后的代码根本就不堪入目。下面是main函数的程序流图。

IDA反汇编出的main
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // er13
bool v4; // r14
signed int v5; // eax
int v6; // ebp
const char *v7; // rdi
bool v8; // zf
signed int v9; // ecx
char v11; // [rsp+6h] [rbp-182h]
char v12; // [rsp+7h] [rbp-181h]
int v13; // [rsp+8h] [rbp-180h]
int v14; // [rsp+Ch] [rbp-17Ch]
__int64 v15; // [rsp+18h] [rbp-170h]
char *v16; // [rsp+28h] [rbp-160h]
__int128 v17; // [rsp+30h] [rbp-158h]
__int64 v18; // [rsp+40h] [rbp-148h]
char s[255]; // [rsp+50h] [rbp-138h]
char v20; // [rsp+14Fh] [rbp-39h]
v17 = xmmword_400920;
v18 = 0LL;
fgets(s, 255, stdin);
v20 = 0;
v5 = -509329727;
v6 = 0;
while ( 1 )
{
LABEL_25:
while ( v5 > -509329728 )
{
switch ( v5 )
{
case -509329727:
v13 = v6;
v5 = -1064694185;
if ( v6 < 255 )
v5 = 425795528;
v4 = 0;
if ( v5 > 287732016 )
{
LABEL_5:
while ( v5 > 1288322757 )
{
if ( v5 > 1505978791 )
{
if ( v5 != 1505978792 )
{
if ( v5 != 1985928755 )
goto LABEL_4;
v3 = v14 + 1;
v5 = -507379925;
goto LABEL_25;
}
v5 = -1367224388;
if ( !*v16 )
v5 = 287732017;
if ( v5 <= 287732016 )
goto LABEL_25;
}
else
{
if ( v5 != 1288322758 )
{
if ( v5 != 1321704548 )
goto LABEL_4;
v6 = v13 + 1;
v5 = -509329727;
goto LABEL_25;
}
v5 = -1367224388;
if ( !*((_BYTE *)&v17 + v15) )
v5 = 1505978792;
if ( v5 <= 287732016 )
goto LABEL_25;
}
}
switch ( v5 )
{
case 287732017:
v7 = "Successed";
goto LABEL_2;
case 425795528:
v4 = s[v13] != 10;
v5 = -1064694185;
break;
case 453611148:
s[v13] = 0;
v5 = -507379925;
v3 = 0;
break;
default:
goto LABEL_4;
}
}
break;
case -507379925:
v14 = v3;
v15 = v3;
v16 = &s[v3];
v11 = s[v3];
v8 = v11 == 0;
v5 = 1288322758;
v9 = -916046165;
goto LABEL_44;
case 45310531:
v8 = v14 + v12 + 1 == v11;
v5 = 1985928755;
v9 = 1288322758;
goto LABEL_44;
default:
LABEL_4:
if ( v5 > 287732016 )
goto LABEL_5;
break;
}
}
if ( v5 > -954026375 )
break;
if ( v5 == -1367224388 )
{
v7 = "Failed";
LABEL_2:
puts(v7);
v5 = -954026374;
}
else
{
if ( v5 != -1064694185 )
goto LABEL_4;
v8 = v4 == 0;
v5 = 453611148;
v9 = 1321704548;
LABEL_44:
if ( !v8 )
v5 = v9;
if ( v5 > 287732016 )
goto LABEL_5;
}
}
if ( v5 == -916046165 )
{
v12 = *((_BYTE *)&v17 + v15);
v8 = v12 == 0;
v5 = 1288322758;
v9 = 45310531;
goto LABEL_44;
}
if ( v5 != -954026374 )
goto LABEL_4;
return 0;
}
从上面可以看出,要恢复出原来的流程图是非常困难的,这时我们不妨使用一下上面说的那种思路。
第一步:下断点
执行的时候我们先输入字符串12
进行实验。
.text:00000000004005D6 mov rdx, cs:stdin@@GLIBC_2_2_5 ; stream
.text:00000000004005DD mov esi, 0FFh ; n
.text:00000000004005E2 mov rdi, rbx ; s
.text:00000000004005E5 call _fgets
.text:00000000004005EA mov [rsp+188h+var_39], 0
首先在0x4005EA
下断点,也就是调用fgets
之后的第一条指令,来保证我们的输入现在还没有经过任何操作,然后找到我们的输入的内存,下硬件断点
,并加入的watch list
中,直接按F9
执行到硬件断点处。
第二步:下额外断点
在硬件断点旁继续下断点,比如在执行完上面的步骤后,程序会在0x4006F9
停下来:
.text:00000000004006EF loc_4006EF: ; CODE XREF: main+EC↑j
.text:00000000004006EF movsxd rax, [rsp+188h+var_180]
.text:00000000004006F4 cmp [rsp+rax+188h+s], 0Ah
.text:00000000004006F9 setnz r14b
.text:00000000004006FD mov eax, 0C08A0E57h
.text:0000000000400702 cmp eax, 11267130h
.text:0000000000400707 jg loc_40063B
.text:000000000040070D jmp short loc_400730
.text:000000000040070F ; ---------------------------------------------------------------------------
所以我们在0x4006EF
下断点来观察这段代码的具体功能。
继续按F9
执行
.text:0000000000400802 loc_400802: ; CODE XREF: main+1EC↑j
.text:0000000000400802 mov [rsp+188h+var_17C], r13d
.text:0000000000400807 movsxd rax, [rsp+188h+var_17C]
.text:000000000040080C mov [rsp+188h+var_170], rax
.text:0000000000400811 mov rax, [rsp+188h+var_170]
.text:0000000000400816 add rax, rbx
.text:0000000000400819 mov [rsp+188h+var_160], rax
.text:000000000040081E mov rax, [rsp+188h+var_160]
.text:0000000000400823 movzx eax, byte ptr [rax]
.text:0000000000400826 mov [rsp+188h+var_182], al
.text:000000000040082A cmp [rsp+188h+var_182], 0
.text:000000000040082F mov eax, 4CCA3EC6h
.text:0000000000400834 mov ecx, 0C9663EABh
.text:0000000000400839 jmp short loc_400858
.text:000000000040083B ; ---------------------------------------------------------------------------
程序在0x400826
处停下,所以在0x400802
下断点来观察这段代码的具体功能。同时还观察到下面的代码:
.text:0000000000400823 movzx eax, byte ptr [rax]
.text:0000000000400826 mov [rsp+188h+var_182], al
这里程序将我们的输入的字符串的1
转移到了另一个内存地址,可以怀疑是转移判断
,所以在转移到的地方再下一个硬件断点,这里假设断点的名字是break2
,并加入的watch list
中。
继续按F9执行,发现在.text:000000000040082A cmp [rsp+188h+var_182], 0
停下,进行了0判断,应该是判断是否为字符串尾部。
继续按F9执行
.text:000000000040078D movsx eax, [rsp+188h+var_181]
.text:0000000000400792 mov ecx, [rsp+188h+var_17C]
.text:0000000000400796 lea eax, [rcx+rax+1]
.text:000000000040079A movsx ecx, [rsp+188h+var_182]
.text:000000000040079F cmp eax, ecx
.text:00000000004007A1 mov eax, 765EDE33h
.text:00000000004007A6 mov ecx, 4CCA3EC6h
.text:00000000004007AB jmp loc_400858
又在0x40079A
触发硬件断点,所以继续在0x40078D
下断点,这里假设断点的名字是break3
,可以看到程序将break2
的值赋值给了ecx。并在下面有一个判断:
.text:000000000040079F cmp eax, ecx
这是eax的低字节是字符y
,而ecx的低字节就是我们输入的第一个字符1
。所以这应该是一个转移判断。
结束
继续运行,发现程序进入了系统库中,标志着程序的结束。
分析
对上面的几个断点进行分析。不难看出其重点是break3
断点。这里我们只需要下断点,在执行下面语句的时候
.text:000000000040079F cmp eax, ecx
每次都把ecx的值改成和eax一样,并记录eax的值,那么就能得到我们的flag。
注意:在得到flag的步骤那一步,我们输入的字符串要足够长,否则会提前中止判断。
源码
// compiled: clang -mllvm -fla crackme.c -O3 -o crackme
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main()
{
int i;
// you_get_the_flag
char str[24] = "xmr[b_mWk^ZSY^RW";
char buf[0x100];
fgets(buf, 0x100 - 1, stdin);
buf[0x100 - 1] = '\0';
i = 0;
while (i < 0x100 - 1 && buf[i] != '\n')
{
i++;
}
buf[i] = '\0';
for (i = 0; buf[i]; i++)
{
if (str[i])
{
if (str[i] + i + 1 != buf[i])
{
break;
}
}
else
{
break;
}
}
if (str[i] == 0 && buf[i] == 0)
{
puts("Successed");
}
else
{
puts("Failed");
}
return 0;
}
总结
对于CTF题目来说,掌握的思路越多,那么解出题目的可能性更大。