0x01前言

这次偶然参加了这个CTF,没想到逆向全是exe,真让人头大。还有就是不得不吐槽的是,逆向你出一个算法题干嘛,又不是ACM 😦 。但还是有些题目很有质量,比如double code这种接近实战的逆向分析。也学到了很多windows方面逆向的一些知识吧。 还有要吐槽的,题目都能出错来,fake_game那道题我就说算出来flag怎么不对😓,暴打出题人就完事儿了。

0x02 题目

0x01 easy_re

使用exeinfope工具查看
可以看到有upx壳

直接用ida打开,ida也能识别

使用upx进行脱壳
命令:upx.exe -d re.exe
再将文件丢进ida中,就能看到清楚的函数了。

对逐个函数进行分析。

f3函数是对输入字符串进行reverse
总共32次,偶数次,就相当于没有变

然后是func函数,这个函数其实是base64加密,因为不太熟悉,一开始分析起来好困难😵
后面根据函数大概逻辑,三位变四位,大概能猜出来是base64.

void __cdecl func(char *x, char *y)
{
  unsigned __int8 *v3; // rax
  unsigned __int8 v4; // cl
  int v5; // ecx
  int v6; // eax
  int v7; // ecx
  int v8; // eax
  int v9; // eax
  int v10; // eax
  unsigned __int8 a4[4]; // [rsp+29h] [rbp-67h]
  unsigned __int8 a3[3]; // [rsp+2Dh] [rbp-63h]
  char b[65]; // [rsp+30h] [rbp-60h] BYREF
  int k; // [rsp+7Ch] [rbp-14h]
  int j; // [rsp+80h] [rbp-10h]
  int i; // [rsp+84h] [rbp-Ch]
  int len; // [rsp+88h] [rbp-8h]
  int index; // [rsp+8Ch] [rbp-4h]
  char *xa; // [rsp+A0h] [rbp+10h]

  xa = x;
  index = 0;
  len = strlen(x);
  i = 0;
  j = 0;
  k = 0;
  strcpy(b, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");
  while ( len-- )
  {
    v3 = (unsigned __int8 *)xa++;
    v4 = *v3;
    LODWORD(v3) = i++;
    a3[(int)v3] = v4;
    if ( i == 3 )
    {
      a4[0] = a3[0] >> 2;
      a4[1] = ((16 * a3[0]) & 0x30) + (a3[1] >> 4);
      a4[2] = ((4 * a3[1]) & 0x3C) + (a3[2] >> 6);
      a4[3] = a3[2] & 0x3F;
      for ( i = 0; i <= 3; ++i )
      {
        v5 = a4[i];
        v6 = index++;
        y[v6] = b[v5];
      }
      i = 0;
    }
  }
  if ( i )
  {
    for ( j = i; j <= 2; ++j )
      a3[j] = 0;
    a4[0] = a3[0] >> 2;
    a4[1] = ((16 * a3[0]) & 0x30) + (a3[1] >> 4);
    a4[2] = ((4 * a3[1]) & 0x3C) + (a3[2] >> 6);
    a4[3] = a3[2] & 0x3F;
    for ( j = 0; i >= j; ++j )
    {
      v7 = a4[j];
      v8 = index++;
      y[v8] = b[v7];
    }
    while ( 1 )
    {
      v9 = i++;
      if ( v9 > 2 )
        break;
      v10 = index++;
      y[v10] = 61;
    }
  }
  y[index] = 0;
}

将输入字符base64后与字符串a进行比较。

将字符串a进行base64解码得到flag
HDCTF{Y0u_h@v2_//@57er3d_7he_r3v3rs3}

0x02 easy_asm

这题的exe运行不了,就只能静态分析了,但是还好代码很少。
程序的大概流程图如下

有个地方没搞懂的是,对字符串进行操作时,都指向的asc_10000,而正确的字符串很明显是另一个字符串。

从汇编来看
0x52-0x47刚好等于11
就是字符串Not equal!$ 的长度。
所有对应的字符串指向,应该如下图所示。也符合程序的逻辑。

查看上半部分的汇编,将输入字符,对0x10异或后再对字符作比较。

根据之前的猜测,很明显0x59就是指向,后面的字符串了

根据IDA的提示知道$是停止符,就不管。

得到flag
HDCTF{Hust_a_e3sy_aSm}

0x03 double_code

从start函数一路找,这里的Code参数名也算是一个提示了,点进去继续看。

从这里的分配内存,写入内存,创建线程,可以知道是shellcode loader。

对照windows官方文档,writememory函数

第三个参数应该是buffer的指针。

但是这里,IDA把这个指针识别成了函数🤣,就是说帮我们把shellcode逆向了

strcpy这里就是把flag放进了v15里,然后长度%5取余,分情况做不一样的计算。
(具体的不用看太懂,因为ida识别的不是很好,要么就自己分析汇编😂)

编写脚本如下:

code = [0x48,0x67,0x45,0x51,0x42,0x7b,0x70,0x6a,0x30,0x68,0x6c,0x60,0x32,0x61,0x61,0x5f,0x42,0x70,0x61,0x5b,0x30,0x53,0x65,0x6c,0x60,0x65,0x7c,0x63,0x69,0x2d,0x5f,0x46,0x35,0x70,0x75,0x7d]

for i in range(len(code)):
    if(i%5 == 1):
        code[i] ^= 0x23
    if(i%5 == 2):
        code[i] -= 2
    if(i%5 == 3):
        code[i] += 3
    if(i%5 == 4):
        code[i] += 4
    if(i%5 == 5):
        code[i] += 25


for i in code:
    print(chr(i),end="")

得到flag
HDCTF{Sh3llC0de_and_0pcode_al1_e3sy}

这里我想问一下,i%5咋可能等于5啊,写个i%5==在这儿?是故意的还是不小心的👀

0x04 fake_game

看exe图标可以发现,是使用pyinstaller生成的exe.
使用工具pyinstxtractor-ng将exe转换为pyc文件。

使用python库uncompyle6进行反编译
pip3 install uncompyle6
uncompyle6.exe game.pyc > game.py

然后分析py代码,关键部分代码如下

xorr = [
         0] * 4
        ans = [0] * 55
        flag = [178868, 188, 56953, 2413, 178874, 131, 56957, 2313, 178867, 156, 
         56933, 2377, 178832, 202, 56899, 2314, 178830, 167, 56924, 
         2313, 178830, 167, 56938, 2383, 178822, 217, 56859, 2372]
        self.init_window()
        self.init_plant_points()
        self.init_map()
        self.init_zombies()
        while not GAMEOVER:
            MainGame.window.fill((255, 255, 255))
            MainGame.window.blit(self.draw_text('当前钱数$: {}'.format(MainGame.money), 26, (255,
                                                                                         0,
                                                                                         0)), (500,
                                                                                               40))
            MainGame.window.blit(self.draw_text('当前关数{},得分{},距离下关还差{}分'.format(MainGame.shaoguan, MainGame.score, MainGame.remnant_score), 26, (255,
                                                                                                                                                0,
                                                                                                                                                0)), (5,
                                                                                                                                                      40))
            self.load_help_text()
            xorr[0] = MainGame.money
            xorr[1] = MainGame.shaoguan
            xorr[2] = MainGame.score
            xorr[3] = MainGame.remnant_score
            if xorr[0] * 256 - xorr[1] / 2 + xorr[2] * 23 + xorr[3] / 2 == 47118166:
                if xorr[0] * 252 - xorr[1] * 366 + xorr[2] * 23 + xorr[3] / 2 - 1987 == 46309775:
                    if xorr[0] * 6 - xorr[1] * 88 + xorr[2] / 2 + xorr[3] / 2 - 11444 == 1069997:
                        if (xorr[0] - 652) * 2 - xorr[1] * 366 + xorr[2] * 233 + xorr[3] / 2 - 13333 == 13509025:
                            for i in range(len(flag)):
                                ans[i] = flag[i] ^ xorr[i % 4]
                            else:
                                with open('flag.txt', 'w') as (f):
                                    f.write(''.join([chr(a) for a in ans]))

发现需要金钱、分数啥的满足四个方程,再用这四个参数对flag列表进行异或。

编写脚本

from scipy.optimize import fsolve


def solve_function(unsolved_value):
    x, y, z, a= unsolved_value[0], unsolved_value[1], unsolved_value[2], unsolved_value[3]
    return [
        x * 256 - y / 2 + z * 23 + a / 2 - 47118166,
        x * 252 - y * 366 + z * 23 + a / 2 - 1987 - 46309775,
        x * 6 - y * 88 + z / 2 + a / 2 - 11444 - 1069997,
        (x - 652) * 2 - y * 366 + z * 233 + a / 2 - 13333 - 13509025,
    ]


solved = fsolve(solve_function, [0, 0, 0,0])
print(solved)

xorr = [0] * 4
ans = [0] * 55
flag = [178868, 188, 56953, 2413, 178874, 131, 56957, 2313, 178867, 156, 
56933, 2377, 178832, 202, 56899, 2314, 178830, 167, 56924, 
2313, 178830, 167, 56938, 2383, 178822, 217, 56859, 2372]


xorr[0] = int(solved[0])
xorr[1] = int(solved[1]+1)
xorr[2] = int(solved[2]+1)
xorr[3] = int(solved[3]+2)
                
print(xorr)
for i in range(len(flag)):
    ans[i] = flag[i] ^ xorr[i % 4]

print(''.join([chr(a) for a in ans]))

因为这题本身出的就有问题,所以需要根据HDCT这四个对应字符,调整一下xorr
最后得到flag
HDCTF{G0Od_pl2y3r_f0r_Pvz!!}

0x05 买了些什么呢

这道题是考算法的,01背包问题,
但是ida中反编译后的knapsack函数就相当于解法,所以还算有点逆向😅
直接拿来用就行

#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <vector>

int dp[500][500];
int __cdecl knapsack(int W, int* wt, int* val, int n)
{
    int j_0; // [esp+0h] [ebp-10h]
    int i_0; // [esp+4h] [ebp-Ch]
    int j; // [esp+8h] [ebp-8h]
    int i; // [esp+Ch] [ebp-4h]

    for (i = 0; i <= n; ++i)
    {
        for (j = 0; j <= W; ++j)
            dp[i][j] = 0;
    }
    for (i_0 = 1; i_0 <= n; ++i_0)
    {
        for (j_0 = 1; j_0 <= W; ++j_0)
        {
            if (j_0 < wt[i_0 - 1])
            {
                dp[i_0][j_0] = dp[i_0 - 1][j_0];
            }
            else
            {
                dp[i_0][j_0] = dp[i_0 - 1][j_0];
                if (dp[i_0 - 1][j_0 - wt[i_0 - 1]] + val[i_0 - 1] > dp[i_0][j_0])
                    dp[i_0][j_0] = val[i_0 - 1] + dp[i_0 - 1][j_0 - wt[i_0 - 1]];
            }
        }
    }
    return 0;

}
int main()
{
    int W = 50;
    int n = 40;
    int wt[0xA0];
    int val[0xA0];
    srand(1);
    for (int i = 0; i < n; i++)
    {
        wt[i] = rand() % 10 + 1;
        val[i] = rand() % 10 + 1;
        printf("|    %d    |   %d   |   %d   |\n", i + 1, wt[i], val[i]);
    }
    knapsack(W, wt, val, n);
    std::cout << "动态规划数组为: " << std::endl;
    for (int i = 0; i <= n; i++)
    {
        for (int j = 0; j <= W; j++)
        {
            std::cout << dp[i][j] << " ";
        }
        std::cout << std::endl;
    }
    std::vector<int> indices;
    int w = W;
    for (int i = n; i > 0 && w > 0; i--)
    {
        if (dp[i][w] != dp[i - 1][w])
        {
            indices.push_back(i - 1);
            w -= wt[i - 1];
        }
    }
    std::cout << "选择的物品的下标为: ";
    for (int i = indices.size() - 1; i >= 0; i--)
    {
        std::cout << indices[i] << " ";
    }
}

得到flag
HDCTF{0 4 6 10 11 13 16 18 21 22 24 26 31 33 34 36 39}

0x06 enc

这题主要考察一系列加密算法。
主函数逻辑如下

参考文章:https://www.anquanke.com/post/id/224198
对照可以发现sub_411523函数其实是tea加密算法。

但是这里ida把add 0x9E3779B9识别成减法了,虽然效果一样,但第一眼有点让人困惑🙃

直接使用tea的解密算法解密就行。

#include <stdio.h>
#include <stdint.h>  // 使用uint32_t数据类型需要包含此头文件
#include <string.h>
#include <iostream>
using namespace std;


void encrypt(uint32_t v[2], const uint32_t k[4]) {
    uint32_t v0 = v[0], v1 = v[1], sum = 0, i;           /* set up */
    uint32_t delta = 0x9E3779B9;                     /* a key schedule constant */
    uint32_t k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3];   /* cache key */
    for (i = 0; i < 32; i++) {                         /* basic cycle start */
        sum += delta;
        v0 += ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1);
        v1 += ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3);
    }                                              /* end cycle */
    v[0] = v0; v[1] = v1;
}

void decrypt(uint32_t v[2], const uint32_t k[4]) {
    uint32_t v0 = v[0], v1 = v[1], sum = 0xC6EF3720, i;  /* set up; sum is 32*delta */
    uint32_t delta = 0x9E3779B9;                     /* a key schedule constant */
    uint32_t k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3];   /* cache key */
    for (i = 0; i < 32; i++) {                         /* basic cycle start */
        v1 -= ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3);
        v0 -= ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1);
        sum -= delta;
    }                                              /* end cycle */
    v[0] = v0; v[1] = v1;
}

int main() {
    uint32_t enc[2] = { 0x60FCDEF7,0x236DBEC };
    uint32_t key[] = { 0x12,0x34,0x56,0x78 };
    decrypt(enc, key);
    cout << enc[0];
    return 0;
}

解出v10 = 3
然后将它传入sub_4113DE函数,这个函数其实是一个smc函数
参考:https://www.anquanke.com/post/id/238645

查看代码段,可以很明显看到一段类似于shellcode的数据

smc函数对这段code做了xor处理。异或的数就是我们之前解出的3

编写idapython来还原

for i in range(5620):
 rightVal = idc.Byte(0x41D00C + i) ^ 0x3
 PatchByte(0x41D00C + i, rightVal)

修改后点击apply patch to input file

重新打开,就可以看到正常的sub_411302函数了。

这个函数用rc4加密输入的字符串,与另一个字符串作比较。

直接使用官方wp的脚本

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cmath>
#include<map>
#include<vector>
#include<queue>
#include<stack>
#include<set>
#include<string>
#include<cstring>
#include<list>
#include<stdlib.h>
using namespace std;
typedef int status;
typedef int selemtype;



/*初始化函数*/
void rc4_init(unsigned char* s, unsigned char* key, unsigned long Len)
{
    int i = 0, j = 0;
    char k[256] = { 0 };
    unsigned char tmp = 0;
    for (i = 0; i < 256; i++)
    {
        s[i] = i;
        k[i] = key[i % Len];
    }
    for (i = 0; i < 256; i++)
    {
        j = (j + s[i] + k[i]) % 256;
        tmp = s[i];
        s[i] = s[j];//交换s[i]和s[j]
        s[j] = tmp;
    }
}

/*加解密*/
void rc4_crypt(unsigned char* s, unsigned char* Data, unsigned long Len)
{
    int i = 0, j = 0, t = 0;
    unsigned long k = 0;
    unsigned char tmp;
    for (k = 0; k < Len; k++)
    {
        i = (i + 1) % 256;
        j = (j + s[i]) % 256;
        tmp = s[i];
        s[i] = s[j];//交换s[x]和s[y]
        s[j] = tmp;
        t = (s[i] + s[j]) % 256;
        Data[k] ^= s[t];
    }
}

int main()
{
    unsigned char s[256] = { 0 }, s2[256] = { 0 };//S-box
    char key[256] = { "you_are_master" };
    unsigned char pData[512] = {
         0xf,0x94,0xae,0xf2,0xc0,0x57,0xc2,0xe0,0x9a,0x45,
         0x37,0x50,0xf5,0xa0,0x5e,0xcb,0x2c,0x16,0x28,0x29,
         0xfe,0xff,0x33,0x46,0xe,0x57,0x82,0x22,0x52,0x26,
         0x2b,0x6e,0xe4,0x82,0x24
    };
    unsigned long len = 35;
    int i;

    printf("pData=%s\n", pData);
    printf("key=%s,length=%d\n\n", key, strlen(key));
    rc4_init(s, (unsigned char*)key, strlen(key));//已经完成了初始化
    printf("完成对S[i]的初始化,如下:\n\n");
    for (i = 0; i < 256; i++)
    {
        printf("%02X", s[i]);
        if (i && (i + 1) % 16 == 0)putchar('\n');
    }
    printf("\n\n");
    for (i = 0; i < 256; i++)//用s2[i]暂时保留经过初始化的s[i],很重要的!!!
    {
        s2[i] = s[i];
    }

    rc4_crypt(s, (unsigned char*)pData, len);//加密

    printf("现在解密:\n\n");
    //rc4_init(s,(unsignedchar*)key,strlen(key));//初始化密钥
    //rc4_crypt(s2, (unsigned char*)pData, len);//解密
    printf("pData=%s\n\n", pData);
    return 0;
}

得到flag
HDCTF{y0u_ar3_rc4_t3a_smc_m4ster!!}

直接用cyberchef也行

最后还有一道谜语题,就不复现了🤣

0x03 总结

总的来说还是学到了很多加密方式的逆向,熟悉了之后以后就能快速识别了。
也学到了很多windows相关的shellcode的逆向方法。还是有收获的🤠