逆向过程
直接切入主题,通过字符串引用查找到关键函数sub_401300
,如下:
加密算法基本原理就是源字符串3个字符通过算法转化成4个字符,有点类似base64算法的变形。加密算法还原:
const char *bytes = "pc$LEQz=`J&x}Z1H[_#WY?7(d+beh9Rg%U.omaADIn/^r2qj\"X8*SPiVT~,!@0lO";
/**
* 加密算法
*/
void encode(char * src)
{
size_t len = strlen(src);
size_t bufsize = 4 * (len / 3) + 4;
size_t padding = (3 * (len / 3 + 1) - len) % 3; // len % 3;
if (!padding) {
bufsize = 4 * (len / 3);
}
char *buf = (char *)malloc((bufsize+1) * sizeof(char));
char *pSrcStart = src + 2;
char *pBufEnd = bufsize + buf;
char *pBufStart = buf + 2;
*pBufEnd = 0; // 结尾反杠零
size_t i = ((bufsize - 3) >> 2) + 1; //循环计数
do {
char c1 = *(pSrcStart - 2);
pSrcStart += 3;
char c2 = *(pSrcStart - 4);
pBufStart += 4;
*(pBufStart - 6) = bytes[c1 >> 2]; // 标注(1)
int v9 = 0x10 * (c1 & 0x3) | (c2 >> 4); // 标注(2)
char c3 = *(pSrcStart - 3);
*(pBufStart - 5) = bytes[v9]; // 标注(3)
*(pBufStart - 4) = bytes[4 * (c2 & 0xF) | (c3 >> 6)]; // 标注(4)
*(pBufStart - 3) = bytes[c3 & 0x3F]; // 标注(5)
} while (i-- != 1);
if (padding == 1) {
*(pBufEnd - 1) = '=';
}
else if (padding == 2) {
*(pBufEnd - 1) = '=';
*(pBufEnd - 2) = '=';
}
printf("%s", buf);
free(buf);
}
为了保留原汁原味,上面的代码并没有优化,而是采用IDA分析后的结果直接还原代码。看懂了关键的加密算法,接下来分析一下如何写出解密算法。代码标注(1)(2)(3)(4)(5)
,可以看到用了&
这种不可逆的算法,只能穷举,由于字符0x00~0xFF范围很小,如果排除掉不可见字符,范围就更小了,所以穷举的办法可行,直接上代码:
int getindex(char ch)
{
const char *p = bytes;
while (*p != '\0') {
if (*p == ch) {
return (p - bytes);
}
p++;
}
return -1;
}
bool findch(char t0, char t1, char t2, char t3, char &c1, char &c2, char & c3)
{
bool result = false;
char temp = t0 << 2;
printf("findch2 start\n");
for (int k = 0; k <= 3; k++) {
char guess = temp + k;
for (int i = 1; i <= 0xFF; i++) {
//printf("findch2:i->%d\n", i);
if ((((guess & 3) << 4) | (i >> 4)) == t1) {
printf("findch2:%c\n", i);
for (int j = 1; j <= 0xFF; j++) {
if (((4 * (i & 0xF) | (j >> 6)) == t2) && ((j & 0x3F) == t3)) {
printf("findch1-3:%c%c%c\n", guess,i, j);
c1 = guess;
c2 = i;
c3 = j;
result = true;
//break;
}
}
}
}
}
return result;
}
/**
* 解密算法
*/
void decode(char *src)
{
size_t len = strlen(src);
size_t bufsize = len / 4 * 3;
char *buf = (char *)malloc((bufsize+1) * sizeof(char));
memset(buf, 0, bufsize + 1);
char *psrc = src;
char *pbuf = buf;
do {
//int idx = getindex(*psrc);
//char c1 = idx << 2; // 1
//16 * (c1 & 3) | (c2 >> 4);
char c1, c2, c3;
int t0 = getindex(*psrc);
int t1 = getindex(*(psrc + 1));
int t2 = getindex(*(psrc + 2));
int t3 = getindex(*(psrc + 3));
if (findch(t0, t1, t2, t3, c1, c2, c3)) {
printf("char1-3:%c%c%c\n", c1, c2, c3);
}
else {
printf("!!!!!!!!!!!!!!!!!!!????????");
}
*pbuf = c1;
*(pbuf+1) = c2;
*(pbuf+2) = c3;
psrc += 4;
pbuf += 3;
} while (*psrc != '\0');
printf("%s", buf);
free(buf);
}
到这里你可能会以为已经结束了,并不是如此,接着看关键函数sub_401300
后半部分:
为了方便取出v21
字符串,载入OD动态调试,在00401488处下断点:
得到9zUnhP0nhP0U(i+Ubi?g+AXU+P0Vbz?8+?0nhP0Sbz?g9=JP+W@=
,使用我们的decode
函数解密结果为:this_is_a_fake_flag_where_is_the_true?
,很明显这还不是真正的flag,被耍了哈哈。。说明还有其它地方判断,返回字符串窗口,仔细观察能看到DASCTF{%s}
被引用了两次:
sub_401300
已经跟过,这次进入sub_4010E0:
这个函数就很有意思了,故意触发int3
的异常,进入异常处理函数:
异常处理函数逻辑也很简单,取出真正的加密字符串与之前的输入后的加密字符串对比,相同就输出真正的flag。同样使用OD载入下断点,得到真正的加密字符串为:bDYP9Q0S}Vag}(_gZz~m(Vm"9?0V}7Xr(ih*9Q0Ybz?g+AXp+8E=
,解密后明文为:ju5t_t3y_1t_4nd_y0u_w1ll_g3t_The_fl@g!
。
最后输入验证:
延伸与思考
上面通过了很明显的字符串才查找到真正的加密字符串,并且调用也相当隐蔽,这里其实用到了_initterm和atexit函数结合的方式。_initterm是CRT的内部函数,可以利用下面代码给程序增加隐蔽的初始化代码,再利用atexit函数在程序退出时增加回调函数:
#include
using namespace std;
#define SECNAME ".CRT$XCV"
#pragma section(SECNAME, long, read)
void exit()
{
cout << "exit~" << endl;
}
void init()
{
cout << "init~" << endl;
atexit(exit);
}
typedef void(__cdecl * _PVFV)();
__declspec(allocate(SECNAME)) _PVFV dummy[] = { init, };
int main()
{
return 0;
}
另外一点,利用int3异常来打乱IDA的F5分析:
try {
__debugbreak();
}
catch(...)
{
cout << "int3 exception" << endl;
//代码...
}
这段代码在VS高版本中无法被catch到,上述代码要在debug模式下编译。
题目及代码
链接: https://pan.baidu.com/s/17hst6TLgjQnxjHhPBaJdFg?pwd=f3na 提取码: f3na
更多参考:https://docs.microsoft.com/zh-cn/previous-versions/visualstudio/visual-studio-2012/ff797097(v=vs.110)