[WUSTCTF2020]level3

进IDA,发现应该是base64

2024-10-31T02:54:23.png

去看看base64_encode函数看看,发现没有异常,观察base64_table发现数据段中的也没有异常

2024-10-31T02:55:32.png

那就看看还有哪里改了这个base64_table,因为直接解base64出来有乱码(

2024-10-31T02:56:15.png

发现一个可疑的O_OLookAtYou函数,进去一看果然修改了base64_table

2024-10-31T02:56:51.png

还原即可得到结果

exp.py

standard_base64_table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
custom_base64_table = list(standard_base64_table)

for i in range(0, 10):
    temp = custom_base64_table[i]
    custom_base64_table[i] = custom_base64_table[19 - i]
    custom_base64_table[19 - i] = temp

custom_base64_table = ''.join(custom_base64_table)
decode_map = str.maketrans(custom_base64_table, standard_base64_table)

encoded_str = "d2G0ZjLwHjS7DmOzZAY0X2lzX3CoZV9zdNOydO9vZl9yZXZlcnGlfD=="
standard_encoded_str = encoded_str.translate(decode_map)

import base64

print(base64.b64decode(standard_encoded_str))

Youngter-drive

到手发现有UPX壳,脱壳后进IDA

2024-10-31T08:34:43.png

进入第一个函数观察

2024-10-31T08:39:03.png

发现是读取用户输入,接着回到主函数,发现主函数通过两个CreateThread创建了两个子线程

其中第一个线程是执行StartAddress函数,执行sub_41112C,然后等待另一个线程

2024-10-31T08:47:02.png

2024-10-31T08:48:06.png

而另一个线程sub_411B10则是dword_418008 - 1

2024-10-31T08:50:48.png

最后当dword_418008-1时,执行sub_411190,比较flag是否正确

2024-10-31T08:52:43.png

所以整体思路应该是,对off_418004还原,因为dword_418008初始值为29,先启动的是线程StartAddress,所以推测应该是对所有奇数位动手,因此写出还原exp.py(注意,观察sub_411190只验证了0-28位,所以最后我们还原出来还要自己补一位)

exp.py

off_418004 = "TOiZiZtOrYaToUwPnToBsOaOapsyS"
off_418000 = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm"

for i in range(29):
    if i % 2:
        temp = off_418000.index(off_418004[i])
        if 'A' <= off_418004[i] <= 'Z':
            print(chr(temp + 96), end='')
        else:
            print(chr(temp + 38), end='')
    else:
        print(off_418004[i], end='')

但是flag只有29位,所以最后一位要猜。。。
所以完整flag是flag{ThisisthreadofwindowshahaIsESE}(为什么猜大写E,第一尝试了几个,但是Buuoj提交不对,第二其它师傅的WP写的是E)

[FlareOn4]IgniteMe

进IDA分析大致流程后,找到核心的比较函数

2024-10-31T12:20:59.png

比较的对象分别是byte_403000byte_403180,其中byte_403000是目标提取出来,然后分析byte_403180如何构成
其中v4=sub_401000();

2024-10-31T13:08:32.png

所以刚开始v4=4。回到sub_401050,发现应该是对输入,从最后向前异或,然后进行比较,所以写出脚本还原

exp.py

byte_403000 = [0xd, 0x26, 0x49, 0x45, 0x2a, 0x17, 0x78, 0x44, 0x2b, 0x6c, 0x5d, 0x5e, 0x45, 0x12, 0x2f, 0x17, 0x2b, 0x44, 0x6f, 0x6e, 0x56, 0x9, 0x5f, 0x45, 0x47, 0x73, 0x26, 0xa, 0xd, 0x13, 0x17, 0x48, 0x42, 0x1, 0x40, 0x4d, 0xc, 0x2, 0x69]
byte_403000[-1] ^= 4

for i in range(len(byte_403000) - 2, -1, -1):
    byte_403000[i] ^= byte_403000[i + 1]

print(''.join([chr(i) for i in byte_403000]))
# flag{R_y0u_H0t_3n0ugH_t0_1gn1t3@flare-on.com}

相册

小技巧.jpg

遇到安卓的题,先看native部分,大概率会用到(x

进jadx-gui,找到他自己写的部分,发现没有东西(x
2024-10-31T14:29:44.png

所以选择直接搜索与email有关的内容

2024-10-31T14:31:43.png

2024-10-31T14:32:04.png

找到了A2中声明的方法,然后根据方法,去查找用例

2024-10-31T14:32:38.png

2024-10-31T14:33:01.png

发现收件信息来自C2,进C2查看

2024-10-31T14:33:51.png

发现user要去native方法里找,把libcore.so拖进IDA,找到相关内容

2024-10-31T14:34:51.png

解base64,得到flag

[WUSTCTF2020]Cr0ssfun

进IDA,看到输入v4然后check(v4),如果返回为真,就表示flag正确

2024-10-31T14:36:51.png

check分析

2024-10-31T14:38:15.png

2024-10-31T14:39:48.png

2024-10-31T14:40:46.png

2024-10-31T14:41:46.png

2024-10-31T14:42:40.png

2024-10-31T14:43:44.png

2024-10-31T14:44:40.png

exp.py

a1 = {
    10: "p",
    13: "@",
    3: "f",
    26: "r",
    20: "e",
    7: "0",
    16: "_",
    11: "p",
    23: "e",
    30: "u",
    0: "w",
    6: "2",
    22: "s",
    31: "n",
    12: "_",
    15: "d",
    8: "{",
    18: "3",
    28: "_",
    21: "r",
    2: "t",
    9: "c",
    32: "}",
    19: "v",
    5: "0",
    14: "n",
    4: "2",
    17: "r",
    29: "f",
    24: "_",
    1: "c",
    25: "@",
    27: "e",
}

print("".join([a1[i] for i in range(33)]))

# flag{cpp_@nd_r3verse_@re_fun}

[UTCTF2020]basic-re

没啥好说的,IDA见

2024-10-31T14:53:43.png

特殊的 BASE64

标题暗示太明显了,换了table的base64,直接去string里找,然后还原

2024-10-31T14:55:44.png

exp.py

custom_base64_table = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0987654321/+"
standard_base64_table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
decoded_map = str.maketrans(custom_base64_table, standard_base64_table)

encoded_string = "mTyqm7wjODkrNLcWl0eqO8K8gc1BPk1GNLgUpI=="
decoded_string = encoded_string.translate(decoded_map)

import base64
print(base64.b64decode(decoded_string).decode())
# flag{Special_Base64_By_Lich}

[GWCTF 2019]xxor

无壳,进IDA查看主函数

2024-10-31T15:00:21.png

先看比较条件sub_400770

2024-10-31T15:07:57.png

__int64 __fastcall sub_400770(_DWORD *a1)
{
  if ( a1[2] - a1[3] == 0x84A236FFLL
    && a1[3] + a1[4] == 0xFA6CB703LL
    && a1[2] - a1[4] == 0x42D731A8LL
    && *a1 == 0xDF48EF7E
    && a1[5] == 0x84F30420
    && a1[1] == 0x20CAACF4 )
  {
    puts("good!");
    return 1LL;
  }
  else
  {
    puts("Wrong!");
    return 0LL;
  }
}
// 解出来(没必要用什么Z3,手动计算器都能解)
// a0 = 0xDF48EF7E
// a1 = 0x20CAACF4
// a2 = 0xE0F30FD5
// a3 = 0x5C50D8D6
// a4 = 0x9E1BDE2D
// a5 = 0x84F30420

然后观察循环,发现每组只有一个数进行了处理,进入sub_400686函数分析

2024-11-02T10:21:39.png

天真了,以为只对一组数处理,结果还是每组都变了,根据函数还原,得到flag

exp.py


使用python写exp有两个需要注意的点:

  1. 需要模拟unsigned int的范围
  2. 输出需要自己拼接

unk_601060 = [2, 2, 3, 4]

a1 = [0xDF48EF7E, 0x20CAACF4, 0xE0F30FD5, 0x5C50D8D6, 0x9E1BDE2D, 0x84F30420]


def simulate_unsigned_int(value):
 return value & 0xFFFFFFFF


for i in range(3):
 v5 = 1166789954 * 64
 v3 = simulate_unsigned_int(a1[i * 2])
 v4 = simulate_unsigned_int(a1[i * 2 + 1])
 for _ in range(64):
     v4 = simulate_unsigned_int(v4 - ((v3 + v5 + 20) ^ ((v3 << 6) + unk_601060[2]) ^ ((v3 >> 9) + unk_601060[3]) ^ 0x10))
     v3 = simulate_unsigned_int(v3 - ((v4 + v5 + 11) ^ ((v4 << 6) + unk_601060[0]) ^ ((v4 >> 9) + unk_601060[1]) ^ 0x20))
     v5 -= 1166789954
 a1[i * 2] = v3
 a1[i * 2 + 1] = v4


for i in range(6):
 print(chr((a1[i] >> 16) & 0xFF), end='')
 print(chr((a1[i] >> 8) & 0xFF), end='')
 print(chr(a1[i] & 0xFF), end='')

[FlareOn6]Overlong

没有加壳,直接进IDA,发现逻辑很简单外层有sub_401160获取要展示的内容

int __stdcall start(int a1, int a2, int a3, int a4)
{
  char Text[128]; // [esp+0h] [ebp-84h] BYREF
  unsigned int v6; // [esp+80h] [ebp-4h]

  v6 = sub_401160(Text, asc_402008, 28u);
  Text[v6] = 0;
  MessageBoxA(0, Text, Caption, 0);
  return 0;
}

进入sub_401160,发现是通过sub_401000函数获取下一次的位置

unsigned int __cdecl sub_401160(char *a1, char *a2, unsigned int a3)
{
  unsigned int i; // [esp+4h] [ebp-4h]

  for ( i = 0; i < a3; ++i )
  {
    a2 += sub_401000((unsigned __int8 *)a1, a2);
    if ( !*a1++ )
      break;
  }
  return i;
}

进入sub_401000看到完整逻辑

int __cdecl sub_401000(unsigned __int8 *a1, char *a2)
{
  int v3; // [esp+0h] [ebp-8h]
  unsigned __int8 v4; // [esp+4h] [ebp-4h]

  if ( (int)(unsigned __int8)*a2 >> 3 == 30 )
  {
    v4 = a2[3] & 0x3F | ((a2[2] & 0x3F) << 6);
    v3 = 4;
  }
  else if ( (int)(unsigned __int8)*a2 >> 4 == 14 )
  {
    v4 = a2[2] & 0x3F | ((a2[1] & 0x3F) << 6);
    v3 = 3;
  }
  else if ( (int)(unsigned __int8)*a2 >> 5 == 6 )
  {
    v4 = a2[1] & 0x3F | ((*a2 & 0x1F) << 6);
    v3 = 2;
  }
  else
  {
    v4 = *a2;
    v3 = 1;
  }
  *a1 = v4;
  return v3;
}

直接运行程序,没有flag,根据提示,推测应该是v6 = sub_401160(Text, asc_402008, 28u);处28值太少了,结合数据段asc_402008的长度推测,应该至少到175,所以写出脚本,得到flag

exp.py

# [idc.get_wide_byte(0x402008 + i) for i in range(0xb7-0x08)]
asc_402008 = [0xe0, 0x81, 0x89, 0xc0, 0xa0, 0xc1, 0xae, 0xe0, 0x81, 0xa5, 0xc1, 0xb6, 0xf0, 0x80, 0x81, 0xa5, 0xe0, 0x81, 0xb2, 0xf0, 0x80, 0x80, 0xa0, 0xe0, 0x81, 0xa2, 0x72, 0x6f, 0xc1, 0xab, 0x65, 0xe0, 0x80, 0xa0, 0xe0, 0x81, 0xb4, 0xe0, 0x81, 0xa8, 0xc1, 0xa5, 0x20, 0xc1, 0xa5, 0xe0, 0x81, 0xae, 0x63, 0xc1, 0xaf, 0xe0, 0x81, 0xa4, 0xf0, 0x80, 0x81, 0xa9, 0x6e, 0xc1, 0xa7, 0xc0, 0xba, 0x20, 0x49, 0xf0, 0x80, 0x81, 0x9f, 0xc1, 0xa1, 0xc1, 0x9f, 0xc1, 0x8d, 0xe0, 0x81, 0x9f, 0xc1, 0xb4, 0xf0, 0x80, 0x81, 0x9f, 0xf0, 0x80, 0x81, 0xa8, 0xc1, 0x9f, 0xf0, 0x80, 0x81, 0xa5, 0xe0, 0x81, 0x9f, 0xc1, 0xa5, 0xe0, 0x81, 0x9f, 0xf0, 0x80, 0x81, 0xae, 0xc1, 0x9f, 0xf0, 0x80, 0x81, 0x83, 0xc1, 0x9f, 0xe0, 0x81, 0xaf, 0xe0, 0x81, 0x9f, 0xc1, 0x84, 0x5f, 0xe0, 0x81, 0xa9, 0xf0, 0x80, 0x81, 0x9f, 0x6e, 0xe0, 0x81, 0x9f, 0xe0, 0x81, 0xa7, 0xe0, 0x81, 0x80, 0xf0, 0x80, 0x81, 0xa6, 0xf0, 0x80, 0x81, 0xac, 0xe0, 0x81, 0xa1, 0xc1, 0xb2, 0xc1, 0xa5, 0xf0, 0x80, 0x80, 0xad, 0xf0, 0x80, 0x81, 0xaf, 0x6e, 0xc0, 0xae, 0xf0, 0x80, 0x81, 0xa3, 0x6f, 0xf0, 0x80, 0x81, 0xad]
ans = []

def sub_401000(arg2):
    v3 = 0
    v4 = 0
    if (asc_402008[arg2] >> 3 == 30):
        v4 = asc_402008[arg2 + 3] & 0x3f | ((asc_402008[arg2 + 2] & 0x3f) << 6)
        v3 = 4
    elif (asc_402008[arg2] >> 4 == 14):
        v4 = asc_402008[arg2 + 2] & 0x3f | ((asc_402008[arg2 + 1] & 0x3f) << 6)
        v3 = 3
    elif (asc_402008[arg2] >> 5 == 6):
        v4 = asc_402008[arg2 + 1] & 0x3f | ((asc_402008[arg2] & 0x3f) << 6)
        v3 = 2
    else:
        v4 = asc_402008[arg2]
        v3 = 1
    ans.append(v4)
    return v3


temp = 0
for i in range(175):
    temp += sub_401000(temp)
    if temp >= 175:
        break


print("".join([chr(i) for i in ans]))
# flag{I_a_M_t_h_e_e_n_C_o_D_i_n_g@flare-on.com}

[FlareOn3]Challenge1

没有壳,直接进IDA

2024-11-02T12:12:13.png

主函数逻辑很简单,经过函数sub_401260后与上面固定的Str2比较,那就进sub_401260

2024-11-02T12:15:36.png

又是base64加密,应该是换了base64_table的,找到byte_413000,然后还原,得到flag

exp.py

# "".join([chr(idc.get_wide_byte(0x413000 + i)) for i in range(0x3f + 1)])
Str2 = "x2dtJEOmyjacxDemx2eczT5cVS9fVUGvWTuZWjuexjRqy24rV29q"

custom_base64_table = "ZYXABCDEFGHIJKLMNOPQRSTUVWzyxabcdefghijklmnopqrstuvw0123456789+/"

standard_base64_table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

decode_map = str.maketrans(custom_base64_table, standard_base64_table)

import base64

print(base64.b64decode(Str2.translate(decode_map)).decode())
# flag{sh00ting_phish_in_a_barrel@flare-on.com}

[ACTF新生赛2020]Oruga

没壳,直接进IDA

2024-11-02T12:24:16.png

主函数逻辑清晰,读取输入,判断是否为actf{开头,然后进入sub_78A判断

2024-11-02T15:14:47.png

好吧居然又是走迷宫,WMJE对应上下左右,但是和其他迷宫的不同,他是向一个方向走到阻碍才停止,因此部分边界空白区域会直接出去,限制住了多解的情况。

迷宫地图+flag

word_201020 = [0x0, 0x0, 0x0, 0x0, 0x23, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x23, 0x23, 0x23, 0x23, 0x0, 0x0, 0x0, 0x23, 0x23, 0x0, 0x0, 0x0, 0x4f, 0x4f, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4f, 0x4f, 0x0, 0x50, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4c, 0x0, 0x4f, 0x4f, 0x0, 0x4f, 0x4f, 0x0, 0x50, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4c, 0x0, 0x4f, 0x4f, 0x0, 0x4f, 0x4f, 0x0, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4c, 0x4c, 0x0, 0x4f, 0x4f, 0x0, 0x0, 0x0, 0x0, 0x50, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4f, 0x4f, 0x0, 0x0, 0x0, 0x0, 0x50, 0x0, 0x0, 0x0, 0x0, 0x23, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x23, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4d, 0x4d, 0x4d, 0x0, 0x0, 0x0, 0x23, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4d, 0x4d, 0x4d, 0x0, 0x0, 0x0, 0x0, 0x45, 0x45, 0x0, 0x0, 0x0, 0x30, 0x0, 0x4d, 0x0, 0x4d, 0x0, 0x4d, 0x0, 0x0, 0x0, 0x0, 0x45, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x45, 0x45, 0x54, 0x54, 0x54, 0x49, 0x0, 0x4d, 0x0, 0x4d, 0x0, 0x4d, 0x0, 0x0, 0x0, 0x0, 0x45, 0x0, 0x0, 0x54, 0x0, 0x49, 0x0, 0x4d, 0x0, 0x4d, 0x0, 0x4d, 0x0, 0x0, 0x0, 0x0, 0x45, 0x0, 0x0, 0x54, 0x0, 0x49, 0x0, 0x4d, 0x0, 0x4d, 0x0, 0x4d, 0x21, 0x0, 0x0, 0x0, 0x45, 0x45]



for i in range(16):
    for j in range(16):
        print(chr(word_201020[i * 16 + j]) if word_201020[i * 16 + j] else ".", end="")
    print()

# a1 = "actf{MEWEWEMEMJMJ}"
a1 = "actf{MEWEMEWJMEWJM}"

def test(a1):
    v2 = 0
    v4 = 0
    v3 = 5
    while word_201020[v2] != 0x21:
        v2 -= v4
        if (a1[v3] != 'W') or v4 == -16:
            if (a1[v3] != 'E') or v4 == 1:
                if (a1[v3] != 'M') or v4 == 16:
                    if (a1[v3] != 'J') or v4 == -1:
                        return -1
                    v4 = -1
                else:
                    v4 = 16
            else:
                v4 = 1
        else:
            v4 = -16

        v3 += 1
        while not word_201020[v2]:
            if (v4 == -1) and (v2 & 0xf == 0):
                return -2
            if (v4 == 1) and (v2 % 16 == 15):
                return -3
            if (v4 == 16) and ((v2 - 240) & 0xffffffff <= 0xf):
                return -4
            if (v4 == -16) and ((v2 + 15) & 0xffffffff <= 0x1e):
                return -5
            v2 += v4
    return a1[v3] == '}'

print(test(a1))
....#.......####
...##...OO......
........OO.PP...
...L.OO.OO.PP...
...L.OO.OO.P....
..LL.OO....P....
.....OO....P....
#...............
............#...
......MMM...#...
.......MMM....EE
...0.M.M.M....E.
..............EE
TTTI.M.M.M....E.
.T.I.M.M.M....E.
.T.I.M.M.M!...EE

BUUOJ_FLAG: flag{MEWEMEWJMEWJM}

[ACTF新生赛2020]Universe_final_answer

无壳,直接进IDA,直接去sub_860分析

2024-11-02T15:20:37.png

2024-11-02T15:22:47.png

what can i say? Z3 in!

exp.py+flag

from z3 import *

solver = Solver()
v1 = BitVec("v1", 32)
v2 = BitVec("v0", 32)
v3 = BitVec("v2", 32)
v4 = BitVec("v3", 32)
v5 = BitVec("v4", 32)
v6 = BitVec("v6", 32)
v7 = BitVec("v5", 32)
v8 = BitVec("v7", 32)
v9 = BitVec("v8", 32)
v11 = BitVec("v9", 32)

solver.add(v1 > 0)
solver.add(v2 > 0)
solver.add(v3 > 0)
solver.add(v4 > 0)
solver.add(v5 > 0)
solver.add(v6 > 0)
solver.add(v7 > 0)
solver.add(v8 > 0)
solver.add(v9 > 0)
solver.add(v11 > 0)

solver.add(-85 * v9 + 58 * v8 + 97 * v6 + v7 + -45 * v5 + 84 * v4 + 95 * v2 - 20 * v1 + 12 * v3 == 12613)
solver.add(30 * v11 + -70 * v9 + -122 * v6 + -81 * v7 + -66 * v5 + -115 * v4 + -41 * v3 + -86 * v1 - 15 * v2 - 30 * v8 == -54400)
solver.add(-103 * v11 + 120 * v8 + 108 * v7 + 48 * v4 + -89 * v3 + 78 * v1 - 41 * v2 + 31 * v5 - (v6 << 6) - 120 * v9 == -10283)
solver.add(71 * v6 + (v7 << 7) + 99 * v5 + -111 * v3 + 85 * v1 + 79 * v2 - 30 * v4 - 119 * v8 + 48 * v9 - 16 * v11 == 22855)
solver.add(5 * v11 + 23 * v9 + 122 * v8 + -19 * v6 + 99 * v7 + -117 * v5 + -69 * v3 + 22 * v1 - 98 * v2 + 10 * v4 == -2944)
solver.add(-54 * v11 + -23 * v8 + -82 * v3 + -85 * v2 + 124 * v1 - 11 * v4 - 8 * v5 - 60 * v7 + 95 * v6 + 100 * v9 == -2222)
solver.add(-83 * v11 + -111 * v7 + -57 * v2 + 41 * v1 + 73 * v3 - 18 * v4 + 26 * v5 + 16 * v6 + 77 * v8 - 63 * v9 == -13258)
solver.add(81 * v11 + -48 * v9 + 66 * v8 + -104 * v6 + -121 * v7 + 95 * v5 + 85 * v4 + 60 * v3 + -85 * v2 + 80 * v1 == -1559)
solver.add(101 * v11 + -85 * v9 + 7 * v6 + 117 * v7 + -83 * v5 + -101 * v4 + 90 * v3 + -28 * v1 + 18 * v2 - v8 == 6308)
solver.add(99 * v11 + -28 * v9 + 5 * v8 + 93 * v6 + -18 * v7 + -127 * v5 + 6 * v4 + -9 * v3 + -93 * v1 + 58 * v2 == -1697)

ans = {}
if solver.check() == z3.sat:
    model = solver.model()
    for i in list(model):
        ans[int(str(i)[1:])] = chr(model[i].as_long())

print("".join([ans[i] if i in ans else "" for i in range(10)]))

不是哥们,v的顺序和数组是乱的,然后出来的结果还不是flag,还得分析sub_C50?不!直接执行,出最终结果

2024-11-02T15:56:41.png

[Zer0pts2020]easy strcmp

无壳,直接进IDA,主函数非常干净,线索不多,于是看其他的函数

2024-11-02T17:45:12.png

得找a1和a2在哪里被处理了,发现sub_6EA中有蹊跷

2024-11-02T17:46:20.png

使用python还原请注意大小端序,已经请已8 bytes为一组,不要单独还原

exp.py

# Python>[idc.get_wide_byte(0x201068 + i) for i in range(0x7F - 0x68 + 1)]
flag_encoded = [
    0x42,
    0x9,
    0x4A,
    0x49,
    0x35,
    0x43,
    0xA,
    0x41,
    0xF0,
    0x19,
    0xE6,
    0xB,
    0xF5,
    0xF2,
    0xE,
    0xB,
    0x2B,
    0x28,
    0x35,
    0x4A,
    0x6,
    0x3A,
    0xA,
    0x4F,
]
encoded_flag = list("********CENSORED********")

ans = []

for i in range(3):
    part_of_flag = flag_encoded[i * 8 : (i + 1) * 8]
    part_of_flag_in_int = int(
        (
            "0x" + "".join([str(hex(i))[2:].zfill(2) for i in part_of_flag[::-1]])
        ).encode(),
        16,
    )
    part_of_encoded_flag = encoded_flag[i * 8 : (i + 1) * 8]
    part_of_encoded_flag_in_int = int(
        (
            "0x"
            + "".join(
                [str(hex(ord(i)))[2:].zfill(2) for i in part_of_encoded_flag[::-1]]
            )
        ).encode(),
        16,
    )
    original = part_of_flag_in_int + part_of_encoded_flag_in_int
    for j in range(14, -1, -2):
        ans.append(chr(int(hex(original)[2:][j : j + 2], 16)))

print("".join(ans))

# flag{l3ts_m4k3_4_DETOUR_t0d4y}

[BJDCTF2020]BJD hamburger competition

添柴般题目,不会,记录一下

浅谈CTF中的unity游戏逆向

摘抄


0x01 准备工具

逆向最简单的Unity3D类安卓游戏建议使用安装好 JAVA 环境的Windows系统(涉及到dll文件的修改,所以Windows平台更加适合)。并且下载好专用于.net逆向反编译的dnspy

安卓apk逆向三件套
一般 APK 逆向,常使用到 apktool、dex2jar、jd-gui。在逆向 Unity3D 安卓游戏时,仅仅只需要使用到 apktool

Apktool: 用于解压/重新打包安卓APK。
dex2jar: 将解压出来的dex文件变成jar,方便使用jd-gui查看
jd-gui: 查看dex文件逻辑

dll文件逆向三件套
一般的 Unity3D 安卓游戏的主逻辑都在dll文件中,所以我们还需要 dll文件逆向/重新打包 的工具。

ILSpy: 用于查看dll程序逻辑

ILDASM: 用于反编译dll文件,生成il文件(存放了dll反编译后的指令)和res文件(反编译后的资源文件),可以安装Windows SDK或者从网上下载。

ilasm: .net4.0自带了,位置在 C:\Windows\Microsofr.NET\Framework\v4.0.30319\ilasm.exe

0x02 Unity开发的前世今生

Unity3D这款游戏引擎想必大家都不陌生,独立游戏制作者们很多人都在用它,甚至一些大公司也用在很商业的游戏制作上。

Unity3D最大的一个特点是一次制作,多平台部署,而 这一核心功能是靠Mono实现的。可以说一直以来Mono是Unity3D核心中的核心,是Unity3D跨平台的根本。这种形式一直持续到2014年年中,Unity3D官方博客上发了一篇“The future of scripting in unity”的文章,引出了IL2CPP的概念,这种相比Mono来说安全性更强的方式。

Mono与IL
Mono:一个由 Xamarin公司主持的自由开放源代码项目,目标是创建一系列符合ECMA标准(Ecma- 334和Ecma-335)的.NET工具包括C#编译器和通用语言架构。与微软的.NET Framework(共通语言运行平台)不同Mono项目不仅可以运行于Windows系统上,还可以运行于 Linux,FreeBSD,Unix,OS X和Solaris,甚至一些游戏平台。Mono使得C#这门语言有了很好的跨平台能力。

IL:全称是 Intermediate Language。翻译过来就是中间语言。它是一种属于 通用语言架构和.NET框架的低阶(lowest-level)的人类可读的编程语言。简单来说,IL类似于一个面向对象的汇编语言。

0x03 unity游戏逆向基本思路

unity主要可以看成两类,dll游戏和libil2cpp游戏,dll游戏比较简单,其核心代码都在 game/assets/bin/data/Managed/Assembly-CSarp.dll这个 dll 文件中,并且由于c#类似Js的语言特性,几乎就是可以明文随便篡改。为了提高安全性,用来转换dll to so 的libil2cpp应运而生,但实际上逆向的时候使用ida分析libil2cpp的时候也差不多,有点汇编基础不难看懂,题型要么是结合frida去动态断点一些位置,要么是使用dwarf去动态调试一些位置。

一般dll类型的unity游戏逆向,唯一核心就是逆向/修改某个 dll 文件就可以了。而一般IL2CPP的Unity3D游戏的逆向,大多只需要根据global-metadata.dat和libil2cpp.so来进行就可以了。目标异常明确,这也是 Unity3D 和 其它安卓逆向不同的地方。

这道题的核心在BJD hamburger competition_Data\Managed\Assembly-CSharp.dll (为什么在这里?看上面折叠里的内容)
扔进dnSpyEx 分析,在ButtonSpawnFruit里找到了验证逻辑

2024-11-03T11:14:59.png

string text = Init.secret.ToString();
if (ButtonSpawnFruit.Sha1(text) == "DD01903921EA24941C26A48F2CEC24E0BB0E8CC7")
{
    this.result = "BJDCTF{" + ButtonSpawnFruit.Md5(text) + "}";
    Debug.Log(this.result);
}

关注这一段逻辑即可,flag是这一个Sha1的值的原始内容取Md5,用cmd5,找到text应该是1001

但是直接交不对,还是得去看Sha1和Md5的计算

2024-11-03T11:17:08.png

发现计算没有问题,但是Md5值返回了前20位,最后使用全部大写提交flag通过

flag

buuoj: flag{B8C37E33DEFDE51CF91E}

[WUSTCTF2020]level4

无壳,进IDA

2024-11-03T08:10:44.png

逻辑很简单,分别进type1type2查看

2024-11-03T08:12:01.png

2024-11-03T08:12:16.png

分别是中序遍历和后序遍历,根据主函数看到还有type3没有写完,推测应该是前序遍历,那就是用中序遍历结果和后序遍历结果,还原出树,然后输出前序遍历结果是flag

exp.py

def build_preorder(inorder, postorder):
    if not inorder or not postorder:
        return ""
    
    root = postorder[-1]
    
    root_index = inorder.index(root)
    
    left_inorder = inorder[:root_index]
    right_inorder = inorder[root_index + 1:]
    
    left_postorder = postorder[:root_index]
    right_postorder = postorder[root_index:-1]
    
    left_preorder = build_preorder(left_inorder, left_postorder)
    right_preorder = build_preorder(right_inorder, right_postorder)
    
    return root + left_preorder + right_preorder

inorder = "2f0t02T{hcsiI_SwA__r7Ee}"
postorder = "20f0Th{2tsIS_icArE}e7__w"

preorder = build_preorder(inorder, postorder)
print("Pre-order Traversal:", preorder)
# buuoj: flag{This_IS_A_7reE}

[羊城杯 2020]easyre

无壳,进IDA

2024-11-03T08:19:39.png

分析主函数,长度应该是38,然后经过三次加密与Str2比较,先进第一个函数encode_one查看

2024-11-03T08:45:12.png

啊,经典base64,看一眼alphabet,似乎没有改变,那就可以直接解密,再来看看encode_two

2024-11-03T08:56:20.png

这个逻辑就简单多了,主要是一个位置交换

  • a1[26]-a1[38] -> a3[0] -a3[12]
  • a1[0] -a1[12] -> a3[13]-a3[25]
  • a1[39]-a1[51] -> a3[26]-a3[38]
  • a1[13]-a1[25] -> a3[39]-a3[51]

接着进encode_three

2024-11-03T08:59:51.png

分析函数,发现他进行了以下处理

  • 对于所有字母和数字,在各自范围内,向右循环移动3个
  • 对于字符,不变

所以写出还原脚本,得到flag

exp.py

Str2 = "EmBmP5Pmn7QcPU4gLYKv5QcMmB3PWHcP5YkPq3=cT6QckkPckoRG"
TEMP1 = ""
TEMP2 = ""

# Decrypt_Three
custom_table = "DEFGHIJKLMNOPQRSTUVWXYZABCdefghijklmnopqrstuvwxyzabc3456789012"
standard_table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
decoded_table = str.maketrans(custom_table, standard_table)
TEMP1 = Str2.translate(decoded_table)

# Decrypt_Two
TEMP2 = TEMP1[13:26] + TEMP1[39:] + TEMP1[:13] + TEMP1[26:39]

# Decrypt_One
import base64

print(base64.b64decode(TEMP2).decode())
# buuoj: flag{672cc4778a38e80cb362987341133ea2}

最后修改:2024 年 11 月 03 日
如果觉得我的文章对你有用,请随意赞赏