巅峰极客线上第一场ctf——RE
Input your lucky number
要求输入一个数字。
程序有ASLR,可以去掉便于分析。
F5
int __cdecl main(int argc, const char **argv, const char **envp)
{
signed int v3; // edx
char *v4; // eax
char *v5; // ecx
int v6; // edi
signed int v7; // esi
signed int v8; // esi
__m128i *v9; // edi
int v11; // [esp+0h] [ebp-3Ch]
int input; // [esp+14h] [ebp-28h]
char v13; // [esp+18h] [ebp-24h]
__int64 v14; // [esp+19h] [ebp-23h]
int v15; // [esp+21h] [ebp-1Bh]
int *v16; // [esp+2Ch] [ebp-10h]
int v17; // [esp+38h] [ebp-4h]
v16 = &v11;
printf(std::cout, "input your lucky number: ");
std::basic_istream<char,std::char_traits<char>>::operator>>(std::cin, &input);
v13 = 0;
v3 = 0;
v15 = 0;
v17 = 0;
v4 = (char *)&loc_401000 + 2;
v14 = 0i64;
v5 = (char *)&loc_401000 + 2;
while ( *(v5 - 2) != 0xC7u || *(v5 - 1) != 5 || *(int **)v5 != &dword_4043A8 || *((_DWORD *)v5 + 1) != 0x89898989 )
{
++v3; // 9
++v5;
if ( v3 >= 1000 )
return 0;
}
if ( v3 != -1 )
{
v6 = v3 + 10;
v7 = 0;
while ( *(v4 - 2) != 0xC7u || *(v4 - 1) != 5 || *(int **)v4 != &dword_4043A4 || *((_DWORD *)v4 + 1) != 0x98989898 )
{
++v7; // 0x74
++v4;
if ( v7 >= 1000 )
return 0;
}
if ( v7 != -1 )
{
v8 = v7 - v6; // 0x61
v9 = (__m128i *)((char *)&loc_401000 + v6);// 401013
sub_401100(v8, v9, input);
((void (__cdecl *)(const char *, char *))loc_401000)("seed_of_flag", &v13);
sub_401100(v8, v9, input);
}
}
return 0;
}
关键函数401100,传了0x61、0x401013、我们输入的值
反汇编时看到用到了XMM0、XMM1寄存器,用WinDBG调试。
我输的是189,16进制是0xBD。
从上面可以看出,401100函数的作用是把0x401013到0x401013+0x61 = 0x401074的每个字节,跟我们输入的这个数据进行异或。
PS:
从这儿看出是只取我们输入的数据的低8位的。之前一直在拿1024在测,一直在踩雷orz。
然后会返回401000执行程序。
一般来说,会有许多的0出现在文件中,401013到401074出现较多的0X5A,试着输一下0x5a = 90。
Simple Base-N
F5
int __cdecl main(int argc, const char **argv, const char **envp)
{
const char *v3; // ecx
char *v4; // eax
bool v5; // cf
unsigned __int8 v6; // dl
int v7; // eax
int v9; // eax
const char *v10; // edx
sub_401590(std::cout, "please input your flag:");
sub_4017D0(std::cin);
if ( (signed int)strlen(input) >= 10 )
{
change_i(input);
v3 = "guvf_vf_n_snxr_synt";
v4 = input;
while ( 1 ) // v7 = strcmp(input, "guvf_vf_n_snxr_synt");
{
v5 = (unsigned __int8)*v4 < *v3;
if ( *v4 != *v3 )
break;
if ( !*v4 )
goto LABEL_7;
v6 = v4[1];
v5 = v6 < v3[1];
if ( v6 != v3[1] )
break;
v4 += 2;
v3 += 2;
if ( !v6 )
{
LABEL_7:
v7 = 0;
goto LABEL_9;
}
}
v7 = -v5 | 1;
LABEL_9:
if ( !v7 )
{
sub_401590(std::cout, "try a little bit harder!\n");
return 0;
}
chang_table(v3);
base32(&input[1]);
v9 = strcmp(a0, "weNTDk5LZsNRHk6cVogqTZmFy2NRP7X4ZHLTBZwg");
if ( v9 )
v9 = -(v9 < 0) | 1;
v10 = "Congratulations!!!\n";
if ( v9 )
v10 = "soooooooooorry\n";
sub_401590(std::cout, v10);
system("pause");
}
return 0;
}
输进来的字符串首先会做一个如下的变化:
signed int __thiscall sub_401100(const char *this)
{
const char *v1; // edi
unsigned int i; // esi
char v3; // cl
v1 = this;
i = 0;
if ( strlen(this) )
{
do
{
v3 = v1[i];
if ( (unsigned __int8)(v3 - 97) <= 0x19u )
v1[i] = (v3 - 84) % 26 + 97;
if ( (unsigned __int8)(v3 - 65) <= 0x19u )
v1[i] = (v3 - 52) % 26 + 65;
++i;
}
while ( i < strlen(v1) );
}
return 1;
}
然后main函数中间的while(1)等价于strcmp(input, "guvf_vf_n_snxr_synt"); 不能让他们相等,不然就会直接return 0出去了。这里的while(1)是个迷惑作用,真正要分析的在下面。
chang_table(v3); OD调试时,发现一串字符串。跟base32的table很像,也是26个字母加6个数字。
base32(&input[1]); IDA进到这个函数里面有个sub_401170函数,sub_401170函数里面是base32基本特征。有&0x1F、填充等号“=”“==”“===”“====”
而且这个函数里的table跟chang_table里出来的那个字符串一个地址,所以这是一个改变了table表的base32。
最后再跟“weNTDk5LZsNRHk6cVogqTZmFy2NRP7X4ZHLTBZwg”比较,要相等。
综上,将“weNTDk5LZsNRHk6cVogqTZmFy2NRP7X4ZHLTBZwg”解变形base32再进行一个变换。
这里直接把python自带的base64里table改掉了。
再进行解base32
再进行变换
# -*- coding: utf-8 -*- s = b"L@h_Xa@J_o@f332_@Aq_e0g13" flag = '' for i in range(len(s)): ch = s[i] if ((chr(ch) >= 'a' and chr(ch) <= 'z')): ch = (ch - 84)%26 + 97 if ((chr(ch) >= 'A' and chr(ch) <= 'Z')): ch = (ch - 52)%26 + 65 flag += chr(ch) print(flag)
flag:Y@u_Kn@W_b@s332_@Nd_r0t13
Interesting Pointer
F5
int __cdecl main(int argc, const char **argv, const char **envp) { int result; // eax int v4; // ebx size_t len; // eax int v6; // ebx char data[20]; // [esp+1Ch] [ebp-48h] int v8; // [esp+30h] [ebp-34h] int v9; // [esp+34h] [ebp-30h] int v10; // [esp+38h] [ebp-2Ch] int v11; // [esp+3Ch] [ebp-28h] int v12; // [esp+40h] [ebp-24h] int v13; // [esp+44h] [ebp-20h] signed int (__cdecl *func_ptr)(int, int, int); // [esp+48h] [ebp-1Ch] int (__cdecl *v15)(int, int, int); // [esp+4Ch] [ebp-18h] int (__cdecl *v16)(int, int, int); // [esp+50h] [ebp-14h] int v17; // [esp+54h] [ebp-10h] int v18; // [esp+58h] [ebp-Ch] FILE *v19; // [esp+5Ch] [ebp-8h] __main(); func_ptr = func0; // v8+a 跟 v8+b 交换 v15 = func1; // abs(m+n) - abs(m) - abs(n) + 2 ==> v10 v16 = func2; // abs(x) + abs(y) - abs(x+y) + 2 ==> v11 v8 = 0; v9 = 1; v10 = 2; v11 = 3; v12 = 3; v13 = 4; v19 = fopen("data", "rb"); if ( !v19 ) return -1; fseek(v19, 0, 2); // 文件尾 v18 = ftell(v19); // 返回读写位置 fseek(v19, 0, 0); // 文件头 v17 = ftell(v19); // 返回读写位置 if ( v17 ) { puts("something wrong"); result = 0; } else { for ( i = 0; i < v18; ++i ) { v4 = i; data[v4] = fgetc(v19); // data } len = strlen(data); if ( len <= v18 ) { v18 = v11; i = 0; v17 = v13; while ( i <= 2 ) { v6 = i + 1; *(&v8 + v6) = (*(&func_ptr + i))((int)&v8, v12, v13);// // fun0 : 1 ==> v9 // fun1 : abs(m+n) - abs(m) - abs(n) + 2 ==> v10 // fun2 : abs(x) + abs(y) - abs(x+y) + 2 ==> v11 // v12 = ++i; v13 = i + 1; } if ( v11 ) { result = -1; } else { get_key(v18, v17); system("PAUSE"); result = 0; } } else { result = -1; } } return result; }
由上看出:要想进到get_key函数,要使v11的值为0。由它原本的程序走下来是不可能给v11赋到0的。
分析下具体的函数功能
func0(&v8,v12,v13):进行两个整型数据的交换,&v8 + v12 和 &v8 + v13 两个地址处的数据进行交换。且这个函数固定返回1.
func1(&v8,v12,v13):abs(m+n) - abs(m) - abs(n) + 2。m是地址为 &v8 + v12 处的整型数据,n是地址为 &v8 + v13 处的整型数据。
func2(&v8,v12,v13):abs(x) + abs(y) - abs(x+y) + 2。x是地址为 &v8 + v12 处的整型数据,y是地址为 &v8 + v13 处的整型数据。
for ( i = 0; i < v18; ++i ) { v4 = i; data[v4] = fgetc(v19); // data }
char data[20]; // [esp+1Ch] [ebp-48h] int v8; // [esp+30h] [ebp-34h] int v9; // [esp+34h] [ebp-30h] int v10; // [esp+38h] [ebp-2Ch] int v11; // [esp+3Ch] [ebp-28h] int v12; // [esp+40h] [ebp-24h] int v13; // [esp+44h] [ebp-20h] signed int (__cdecl *func_ptr)(int, int, int); // [esp+48h] [ebp-1Ch] int (__cdecl *v15)(int, int, int); // [esp+4Ch] [ebp-18h] int (__cdecl *v16)(int, int, int); // [esp+50h] [ebp-14h] int v17; // [esp+54h] [ebp-10h] int v18; // [esp+58h] [ebp-Ch] FILE *v19; // [esp+5Ch] [ebp-8h]
这里存在溢出。并没有限制data数组的大小,能覆盖掉后面的变量,因为这几个变量的初始化都在这个数组赋值之前。
绝对值不等式:|a| - |b| ≤ |a + b| ≤| a| + |b|
|a + b| ≤ |a| + |b| 取"="的条件是ab≥0
|a - b| ≤ |a| + |b| 取"="的条件是ab≤0
|a + b| ≥ |a| - |b| 取"="的条件是(a+b)b≤0
|a - b| ≥ |a| - |b| 取"="的条件是(a-b)b≥0
所以func2:abs(x) + abs(y) - abs(x+y) + 2 这个怎么算都i是>=2的,根据正常的流程,中间的那个while循环中,func2的返回值就是给v11,v11怎么也不可能为0,所以这个地方,即while第三次循环时*(&v8 + v6) = (*(&func_ptr + i))((int)&v8, v12, v13); (ps:第三次时v6 = 3,即 v11 = *(&func_ptr + 2)((int)&v8, v12, v13);) *(&func_ptr + 2) 即v16不能是func2。
通过溢出,这里有许多解法。
1、理想解:
利用溢出将v12和v13重新覆盖为7和8,这样exchange就会把v15(&8+7)和v16(&v8+8)两个变量里面的值交换。这样就是v9 = 1; v10 = func2(&v8, 1, 2); v11 = func1(&v8, 2, 3)
- 现在func2(&v8, 1, 2)就是把 x = *(&v8 +1); y = *(&v8 + 2) 即 x = v9 = 1,y = v10,来进行 abs(x) + abs(y) - abs(x+y) + 2 运算,这个结果是大于等于2的。这个运算结果再返回给v10。已经确定x为肯定为1,y如果取大于等于0的数,运算结果会总是2。取小于0的数,运算结果是4。
假设通过溢出覆盖v10的时候是个大于等于0的数,那么这里v10 = func2(&v8, 1 ,2); 返回给v10的值为2。
- 再往下func1(&v8, 2, 3)就是把 m = *(&v8 +2); n = *(&v8 + 3) 即 m = v10 = 2,m = v11,abs(m+n) - abs(m) - abs(n) + 2 运算,按着上面我们的假设,这里确定v10即m为2,且这里个函数的返回值是要赋给v11的,所以这里要使这个不等式运算结果为0。|2 + n| - 2 - |n| + 2 = 0 ==> 解得 n = -1。(但其实由于计算机的补码、符号位,这里还有一个解 0x7FFFFFFF (这是非预期解)。)即通过溢出覆盖后v11的值要为-1(0xFFFFFFFF)。
所以综上,这里就是通过data数组的溢出来进行覆盖,要确保v12跟v13为7和8(可以交换,也可以为8和7),还需确保v11为0x7FFFFFFF
get_key(-1,8)是他的预期解。所以v12跟v13为7跟8这样会产生非预期。
AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA
AA AA AA AA BB BB BB BB BB BB BB BB BB BB BB BB
FF FF FF FF 07 00 00 00 08 00 00 00
v12跟v13为7跟8的非预期:
2、非预期解
大佬们的神奇操作。
1)直接把v16原来是func2的地址直接换成了0x00401870:xor eax,eax; retn; 真的是妙。
这个就能产生很多的非预期的结果。
还有不交换,直接把v16的地址覆盖成func1。
2)还有就是求解零解。思路按着预期解的思路走。
关于求零解
这两个结果倒是一样。
信息安全菜为原罪、