学校OJ的一道简单的CTF reverse题
本帖最后由 默之然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里鼠标滚轮,手动到达包含这个地址的函数
int __cdecl sub_415280(int a1, int a2)
{
size_t v2; // eax@5
char v4; // @1
char v5; // @1
char Buf; // @11
unsigned int i; // @4
FILE *File; // @9
char v9; // @1
unsigned int v10; // @1
int savedregs; // @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,可能是因为两者所占用的内存空间是一样的),
还是得具体问题具体分析这个问题坑了我好久的时间……
char *a2;
char* str = a2 + 4;
for (int i = 0; i < strlen(str); ++i)
str += i;
嫌a2+4太烦,就把a2+4替换成str了,不影响,
下面那个strcmp也是比对的a2+4
然后着手编写解密代码,这个倒是挺简单的
#include <stdio.h>
#include <cstring>
int main()
{
const char* str = "fmcj2y~{";
for (int i = 0; i < strlen(str); ++i)
printf("%c", str - i);
return 0;
}
解出flag.txt
解出后就动态调试验证一下,传入参数为flagflag.txt
(为什么前面多了一个“flag”,因为上面是a2+4,随便在”flag.txt”前面加了4个字符),
发现还是过不去,于是就尝试只传入flag.txt,然后就过了?!
【黑人问号】原来IDA不可全信,我还是太天真了……
终于来到最后一关了。
反编译出的代码告诉我,
此程序会读取当前目录下名为“fmcj2y~{”的文件,
读取其中的数据,使用此数据进行某种操作,
然后再对这种操作返回出来的某种变量进行验证,最后输出flag。
注意文件中的数据一定是32个字节,不然就exit
也就是说,实际上“fmcj2y~{”文件存放的,就是真正的flag,
所以我必须通过这两个函数,将flag解出来
对文件读取的数据进行操作的伪代码
int __cdecl sub_414E50(char *Str, int a2)
{
size_t v2; // eax@2
char v4; // @1
int v5; // @17
int i; // @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 >= 48 && Str <= 57 )
{
*(_DWORD *)(a2 + 4 * (i / 2)) = Str - 48;
}
else
{
if ( Str < 97 || Str > 102 )
{
sub_41137A("Input Error!\n");
exit(0);
}
*(_DWORD *)(a2 + 4 * (i / 2)) = Str - 87;
}
*(_DWORD *)(a2 + 4 * (i / 2)) *= 16;
if ( Str >= 48 && Str <= 57 )
{
v5 = Str - 48;
}
else
{
if ( Str < 97 || Str > 102 )
{
sub_41137A("Input Error!\n");
exit(0);
}
v5 = Str - 87;
}
*(_DWORD *)(a2 + 4 * (i / 2)) += v5;
}
return sub_411212();
}
对返回的字符串进行验证的伪代码
int __cdecl sub_411D90(int a1)
{
char v2; // @1
int i; // @1
memset(&v2, 0xCCu, 0xCCu);
sub_411208(dword_41C008);
for ( i = 0; i < 16 && *(_DWORD *)(a1 + 4 * i) + 1 == dword_41A078; ++i )
;
return sub_411212();
}
这样的两份伪代码单独看也看不出什么东西,
但是一结合就发现,两者搭配的十分默契。
对操作后的字符串每个字符值加一,并与内存中的值一一进行比较,
如果32个字符全对,则通过验证
不要认为验证部分for循环那里是个分号,就以为这个没事不用管,
楼主当初也是这么想的,
点击return处的sub_411212(),无法查看反编译的伪代码,
进反汇编窗口也看不出什么东西,
想了想唯一有用的也就这个了
再编写解密代码之前,
我们还得去内存中把dword_41A078 ~dword_41A078的值找出来
解密代码
voiddecrypt()
{
int Str;
int secretCode = { 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 = '0'; Str <= 'f'; Str++)
{
for (Str = '0'; Str <= 'f'; Str++)
{
//本质上仍然是加密代码,只是使用加密代码来碰撞生成 加密前的明文
//同时为了代码更好的理解,将判断条件内的数字全部替换为ASCII码
int tmpNum;
if (Str >= '0' && Str <= '9')
tmpNum = Str - '0';
else
{
//将exit位置的代码全部替换为continue
if (Str < 'a' || Str > 'f')
continue;
tmpNum = Str - 'a' + 10;
}
tmpNum *= 16;
if (Str >= '0' && Str <= '9')
tmpNum += Str - '0';
else
{
if (Str < 'a' || Str > 'f')
continue;
tmpNum += Str - 'a' + 10;
}
//将验证部分编写在这里
if (tmpNum + 1 == secretCode)
printf("%d --> %c%c\n", i / 2, Str, Str);
}
}
}
}
解密结果
最后构建文件
在控制台中运行,即可得到flag:flag{4fc5f0e3e2e199a0a6ddd945aa2dfeda}
——————————————————
自己挖的大坑
当初我以为这个程序的流程是:
1. 输入flag
2. 程序进行解密,将真正的flag解密出来
3. 如果输入的flag与解密出来的flag相同,则输出flag
在我刚准备reverse这个程序时,
吾爱的win7虚拟机我还没下载好,
没配置好环境,没办法脱壳,
(不知道为什么我在win10脱壳失败,可能是我太菜了QAQ)
就想着,不脱壳,直接执行到核心代码,
解出flag,花最少的时间,得更多的分数
于是,我就神挡杀神佛挡杀佛
哪里不行改哪里
把不能满足条件的jnz改成jz
把自己觉得没用的strcmpnop掉
然后屡战屡败,还一直觉得应该是我修改姿势不对,
下次一定可以
于是浪费了一天时间
直到52pojie的win7虚拟机下载好,
脱壳、丢进IDA后才发现,
原来自己一直是在以头抢地尔
这道题目真正改变了我的想法,
原来一山更有一山高,学到了
—————————————————
这是我根据对伪代码的理解,写出来的程序源代码
#include <stdio.h>
#include <Windows.h>
int secretNum = { 0x50, 0xC6, 0xF1, 0xE4, 0xE3, 0xE2, 0x9A, 0xA1 };
void dealWithBuf(char* buf, char* returnedData)
{
secretNum = 167;
secretNum = 222;
secretNum = 218;
secretNum = 70;
secretNum = 171;
secretNum = 46;
secretNum = 255;
secretNum = 219;
int v5;
for (int i = 0; i < strlen(buf); i += 2)
{
if (buf >= '0' && buf <= '9')
{
returnedData = buf - '0';
}
else
{
if (buf < 97 || buf > 102)
{
printf("Input Error!\n");
exit(0);
}
returnedData = buf - 87;
}
returnedData *= 16;
if (buf >= 48 && buf <= 57)
{
v5 = buf - 48;
}
else
{
if (buf < 97 || buf > 102)
{
printf("Input Error!\n");
exit(0);
}
v5 = buf - 87;
}
returnedData += v5;
}
}
bool checkBuf(char* returnedData)
{
int i;
for (i = 0; i < 16 && returnedData + 1 == secretNum; ++i)
;
return i == 16;
}
int main(int argc, char* argv[])
{
char v9;
char Buf;
FILE* File;
printf("Input Your Flag:\n");
scanf("%19s", v9);
if (argc != 2)
{
printf("Input error!\n");
exit(1);
}
printf("%s\n", argv);
for (int i = 0; i < strlen(argv); ++i)
argv += i;
if (!strcmp("fmcj2y~{", argv))
{
File = fopen(argv, "r");
if (File)
{
fgets(Buf, 40, File);
if (strlen(Buf) != 32 || strlen(Buf) % 2 == 1)
exit(1);
char returnedStr;
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程序的过程以及一点总结。
这次发帖也应该是我第一次在吾爱发主题帖
如果有什么不足之处,还请多多包涵,欢迎指出存在的问题
楼主很厉害,一起学习:lol 楼主是个逆向好苗子,希望再接再厉,加油! 厉害呀,楼主 6766666666666666666 加油楼主 作为大二的我对于这些还是一知半解 下载论坛附件下载论坛附件 LZ瞎弄.弄一天成这样.也不容易.支持一下.
页:
[1]
2