本帖最后由 默之然i 于 2019-12-7 21:23 编辑
楼主的学校信安协会搞了个OJ出来
作为信安大一新生的萌新,我对关于计算机方面的知识都非常感兴趣
于是就尝试了一下
在那些题目中,在我看来最有趣的就是这个逆向(更有趣的不会做T_T):
这是我第一次做reverse,第一次,所以遇到了很多坑
————————————————————————
题目描述
程序打开界面
先查壳,区段数据什么的都不懂,就只看是什么壳
一看——UPX压缩壳
这个简单,之前ximo在教程里脱壳过pushad对应popad,或者esp定律法,再不行就单步跟踪
楼主图省事,就直接从上往下拉,过一个个jmp以及call
再一个jump就到OEP了
然后LordPE dump,再ImportREC走起
输入OEP偏移地址,然后AutoSearch
出来了个Yes,很开心,然后就FixDump。
OK后尝试直接执行程序,程序崩了……
仿佛想起了什么,于是把Size改大,果然……
多了两个yes
那之前的错误就是没把这两个dll一起import了
之后就show Invalid——右键——delete thunks,
把那些Invaid都删掉,再FixDump就完事了,程序也正常运行。
(原谅楼主连一个简单的压缩壳都搞不定,
楼主距离上一次看ximo教程已经一年半了,忘了很多,
这次写的这么详细也希望其他一起学习的小伙伴们能避坑)
——————————————————
接下来就日常拖到IDA里,再一个反编译
几乎都是这种类型的反编译代码查找字符串也没结果,有点奇怪。
那就只好再把程序拖到OD里,定位关键代码
拖入,右键——中文搜索引擎——智能搜索,找到了这个
这几个字符串被调用的距离都挺近,
随便记住一个地址,然后到IDA里鼠标滚轮,手动到达包含这个地址的函数
[C] 纯文本查看 复制代码 int __cdecl sub_415280(int a1, int a2)
{
size_t v2; // eax@5
char v4; // [sp+0h] [bp-134h]@1
char v5; // [sp+Ch] [bp-128h]@1
char Buf; // [sp+D0h] [bp-64h]@11
unsigned int i; // [sp+100h] [bp-34h]@4
FILE *File; // [sp+10Ch] [bp-28h]@9
char v9; // [sp+118h] [bp-1Ch]@1
unsigned int v10; // [sp+130h] [bp-4h]@1
int savedregs; // [sp+134h] [bp+0h]@1
memset(&v5, 0xCCu, 0x128u);
v10 = (unsigned int)&savedregs ^ __security_cookie;
sub_411208(dword_41C008);
((void (__cdecl *)(const char *, char))sub_41137A)("Input Your Flag:\n", v4);
sub_41137F("%19s", (unsigned int)&v9);
if ( a1 != 2 )
{
((void (__cdecl *)(const char *, char))sub_41137A)("Input error!\n", v4);
exit(1);
}
sub_41137A("%s\n", *(_DWORD *)(a2 + 4));
for ( i = 0; ; ++i )
{
v2 = j_strlen(*(const char **)(a2 + 4));
if ( i >= v2 )
break;
*(_BYTE *)(*(_DWORD *)(a2 + 4) + i) += i;
}
if ( !j_strcmp("fmcj2y~{", *(const char **)(a2 + 4)) )
{
fopen(*(const char **)(a2 + 4), "r");
File = (FILE *)sub_411212();
if ( File )
{
fgets(&Buf, 40, File);
sub_411212();
if ( j_strlen(&Buf) != 32 || j_strlen(&Buf) % 2 == 1 )
exit(1);
sub_4113B1(&Buf, (int)dword_41A4E0);
if ( sub_4113B6(dword_41A4E0) )
sub_41137A("flag{%s}", &Buf);
else
sub_41137A("Input Error!\n");
}
else
{
sub_41137A("Input Error!\n");
}
}
else
{
sub_41137A("Input Error!\n");
}
sub_411235(&savedregs, dword_4154C4);
sub_4111E0();
return sub_411212();
}
Nice! 核心代码找到了,接下来就是对它进行分析了
仔细观察一下,发现这个V9变量就是程序提示输入flag的变量,这个变量就是个幌子
下文没有任何一个地方用到这个V9,被坑了。
而是要满足这个a1==0才能不被Exit
a1变量是由上一个函数调用的,于是我就逐级查看上一层函数,发现……
“argv””argc”似曾相识,好像就是C/C++ main函数的传入参数
再联想一下这道reverse的题目“Argument”
于是在调试选项里加入了一个参数 “flag”(随便加的字符串)
单步走,发现成功过了第一个if,并输出了传入的参数
接下来就是对传入参数进行简单的加密,然后在下面的if里对加密后的字符串进行比对
这样看的太难受了,就等价替换了一下
坑:IDA提示的类型并不一定真就是那个类型
(例如一个char*,IDA就误以为是int,可能是因为两者所占用的内存空间是一样的),
还是得具体问题具体分析这个问题坑了我好久的时间……
[C] 纯文本查看 复制代码 char *a2;
char* str = a2 + 4;
for (int i = 0; i < strlen(str); ++i)
str[i] += i;
嫌a2+4太烦,就把a2+4替换成str了,不影响,
下面那个strcmp也是比对的a2+4
然后着手编写解密代码,这个倒是挺简单的
[C] 纯文本查看 复制代码 #include <stdio.h>
#include <cstring>
int main()
{
const char* str = "fmcj2y~{";
for (int i = 0; i < strlen(str); ++i)
printf("%c", str[i] - i);
return 0;
}
解出flag.txt
解出后就动态调试验证一下,传入参数为flagflag.txt
(为什么前面多了一个“flag”,因为上面是a2+4,随便在”flag.txt”前面加了4个字符),
发现还是过不去,于是就尝试只传入flag.txt,然后就过了?!
【黑人问号】原来IDA不可全信,我还是太天真了……
终于来到最后一关了。
反编译出的代码告诉我,
此程序会读取当前目录下名为“fmcj2y~{”的文件,
读取其中的数据,使用此数据进行某种操作,
然后再对这种操作返回出来的某种变量进行验证,最后输出flag。
注意文件中的数据一定是32个字节,不然就exit
也就是说,实际上“fmcj2y~{”文件存放的,就是真正的flag,
所以我必须通过这两个函数,将flag解出来
对文件读取的数据进行操作的伪代码
[C] 纯文本查看 复制代码 int __cdecl sub_414E50(char *Str, int a2)
{
size_t v2; // eax@2
char v4; // [sp+Ch] [bp-D8h]@1
int v5; // [sp+D0h] [bp-14h]@17
int i; // [sp+DCh] [bp-8h]@1
memset(&v4, 0xCCu, 0xD8u);
sub_411208(dword_41C008);
*(&dword_41A078 + 8) = 167;
*(&dword_41A078 + 9) = 222;
*(&dword_41A078 + 10) = 218;
*(&dword_41A078 + 11) = 70;
*(&dword_41A078 + 12) = 171;
*(&dword_41A078 + 13) = 46;
*(&dword_41A078 + 14) = 255;
*(&dword_41A078 + 15) = 219;
for ( i = 0; ; i += 2 )
{
v2 = j_strlen(Str);
if ( i >= v2 )
break;
if ( Str[i] >= 48 && Str[i] <= 57 )
{
*(_DWORD *)(a2 + 4 * (i / 2)) = Str[i] - 48;
}
else
{
if ( Str[i] < 97 || Str[i] > 102 )
{
sub_41137A("Input Error!\n");
exit(0);
}
*(_DWORD *)(a2 + 4 * (i / 2)) = Str[i] - 87;
}
*(_DWORD *)(a2 + 4 * (i / 2)) *= 16;
if ( Str[i + 1] >= 48 && Str[i + 1] <= 57 )
{
v5 = Str[i + 1] - 48;
}
else
{
if ( Str[i + 1] < 97 || Str[i + 1] > 102 )
{
sub_41137A("Input Error!\n");
exit(0);
}
v5 = Str[i + 1] - 87;
}
*(_DWORD *)(a2 + 4 * (i / 2)) += v5;
}
return sub_411212();
}
对返回的字符串进行验证的伪代码
[C] 纯文本查看 复制代码 int __cdecl sub_411D90(int a1)
{
char v2; // [sp+Ch] [bp-CCh]@1
int i; // [sp+D0h] [bp-8h]@1
memset(&v2, 0xCCu, 0xCCu);
sub_411208(dword_41C008);
for ( i = 0; i < 16 && *(_DWORD *)(a1 + 4 * i) + 1 == dword_41A078[i]; ++i )
;
return sub_411212();
}
这样的两份伪代码单独看也看不出什么东西,
但是一结合就发现,两者搭配的十分默契。
对操作后的字符串每个字符值加一,并与内存中的值一一进行比较,
如果32个字符全对,则通过验证
不要认为验证部分for循环那里是个分号,就以为这个没事不用管,
楼主当初也是这么想的,
点击return处的sub_411212(),无法查看反编译的伪代码,
进反汇编窗口也看不出什么东西,
想了想唯一有用的也就这个了
再编写解密代码之前,
我们还得去内存中把dword_41A078[0] ~dword_41A078[7]的值找出来
解密代码
[C] 纯文本查看 复制代码 void decrypt()
{
int Str[32];
int secretCode[16] = { 0x50, 0xC6, 0xF1, 0xE4, 0xE3, 0xE2, 0x9A, 0xA1,
167, 222, 218, 70, 171, 46, 255, 219 };
for (int i = 0; i < 32; i += 2)
{
//加了两个for循环遍历穷举
for (Str[i] = '0'; Str[i] <= 'f'; Str[i]++)
{
for (Str[i + 1] = '0'; Str[i + 1] <= 'f'; Str[i + 1]++)
{
//本质上仍然是加密代码,只是使用加密代码来碰撞生成 加密前的明文
//同时为了代码更好的理解,将判断条件内的数字全部替换为ASCII码
int tmpNum;
if (Str[i] >= '0' && Str[i] <= '9')
tmpNum = Str[i] - '0';
else
{
//将exit位置的代码全部替换为continue
if (Str[i] < 'a' || Str[i] > 'f')
continue;
tmpNum = Str[i] - 'a' + 10;
}
tmpNum *= 16;
if (Str[i + 1] >= '0' && Str[i + 1] <= '9')
tmpNum += Str[i + 1] - '0';
else
{
if (Str[i + 1] < 'a' || Str[i + 1] > 'f')
continue;
tmpNum += Str[i + 1] - 'a' + 10;
}
//将验证部分编写在这里
if (tmpNum + 1 == secretCode[i / 2])
printf("%d --> %c%c\n", i / 2, Str[i], Str[i + 1]);
}
}
}
}
解密结果
最后构建文件
在控制台中运行,即可得到flag:flag{4fc5f0e3e2e199a0a6ddd945aa2dfeda}
——————————————————
自己挖的大坑
当初我以为这个程序的流程是:
1. 输入flag
2. 程序进行解密,将真正的flag解密出来
3. 如果输入的flag与解密出来的flag相同,则输出flag
在我刚准备reverse这个程序时,
吾爱的win7虚拟机我还没下载好,
没配置好环境,没办法脱壳,
(不知道为什么我在win10脱壳失败,可能是我太菜了QAQ)
就想着,不脱壳,直接执行到核心代码,
解出flag,花最少的时间,得更多的分数
于是,我就神挡杀神佛挡杀佛
哪里不行改哪里
把不能满足条件的jnz改成jz
把自己觉得没用的strcmp nop掉
然后屡战屡败,还一直觉得应该是我修改姿势不对,
下次一定可以
于是浪费了一天时间
直到52pojie的win7虚拟机下载好,
脱壳、丢进IDA后才发现,
原来自己一直是在以头抢地尔
这道题目真正改变了我的想法,
原来一山更有一山高,学到了
—————————————————
这是我根据对伪代码的理解,写出来的程序源代码
[C] 纯文本查看 复制代码 #include <stdio.h>
#include <Windows.h>
int secretNum[16] = { 0x50, 0xC6, 0xF1, 0xE4, 0xE3, 0xE2, 0x9A, 0xA1 };
void dealWithBuf(char* buf, char* returnedData)
{
secretNum[8] = 167;
secretNum[9] = 222;
secretNum[10] = 218;
secretNum[11] = 70;
secretNum[12] = 171;
secretNum[13] = 46;
secretNum[14] = 255;
secretNum[15] = 219;
int v5;
for (int i = 0; i < strlen(buf); i += 2)
{
if (buf[i] >= '0' && buf[i] <= '9')
{
returnedData[4 * (i / 2)] = buf[i] - '0';
}
else
{
if (buf[i] < 97 || buf[i] > 102)
{
printf("Input Error!\n");
exit(0);
}
returnedData[4 * (i / 2)] = buf[i] - 87;
}
returnedData[4 * (i / 2)] *= 16;
if (buf[i + 1] >= 48 && buf[i + 1] <= 57)
{
v5 = buf[i + 1] - 48;
}
else
{
if (buf[i + 1] < 97 || buf[i + 1] > 102)
{
printf("Input Error!\n");
exit(0);
}
v5 = buf[i + 1] - 87;
}
returnedData[4 * (i / 2)] += v5;
}
}
bool checkBuf(char* returnedData)
{
int i;
for (i = 0; i < 16 && returnedData[4 * i] + 1 == secretNum[i]; ++i)
;
return i == 16;
}
int main(int argc, char* argv[])
{
char v9[20];
char Buf[40];
FILE* File;
printf("Input Your Flag:\n");
scanf("%19s", v9);
if (argc != 2)
{
printf("Input error!\n");
exit(1);
}
printf("%s\n", argv[0]);
for (int i = 0; i < strlen(argv[0]); ++i)
argv[0][i] += i;
if (!strcmp("fmcj2y~{", argv[0]))
{
File = fopen(argv[0], "r");
if (File)
{
fgets(Buf, 40, File);
if (strlen(Buf) != 32 || strlen(Buf) % 2 == 1)
exit(1);
char returnedStr[40];
dealWithBuf(Buf, returnedStr);
if (checkBuf(returnedStr))
printf("flag{%s}", &Buf);
else
printf("Input Error!\n");
}
else
printf("Input Error!\n");
}
else
printf("Input Error!\n");
return 0;
}
搞定!
——————————————————
以上内容是我对我这第一次reverse程序的过程以及一点总结。
这次发帖也应该是我第一次在吾爱发主题帖
如果有什么不足之处,还请多多包涵,欢迎指出存在的问题
|