近期解题 2024.1.13
文章目录
2024.1.4-2024.1.13
NEFU::CTF
反静态分析-1
反编译出 main()
函数如下:
敏锐察觉到 v9
数组的四个元素,疑似为 TEA 加密算法的 key。进入 sub_411523() -> sub_415100()
,果然是一个 TEA 加密。
在 main()
函数中调用的 sub_411523()
有两个参数,第二个参数 v9
是 key,第一个参数 &v7
为 v7
数组。在变量声明的部分 int v7; // [esp+1D8h] [ebp-40h] BYREF
和 int v8; // [esp+1DCh] [ebp-3Ch]
可以看出 v7
与 v8
的地址相邻,实际上可以看作是一个数组。
写一个 TEA 解密的脚本,需要知道 v4
(即 sum)的值是多少。通过打断点动态调试找到 v4
的值 0xC6EF3720
。
上脚本
1#include <iostream>
2#include <cstdio>
3
4using namespace std;
5
6int key[4] = { 18,52,86,120 };
7unsigned int num1 = 0x60FCDEF7;
8unsigned int num2 = 0x236DBEC;
9int sum = 0xC6EF3720;
10
11void tea()
12{
13 for (int i = 0; i < 32; i++)
14 {
15 num2 -= (key[3] + (num1 >> 5)) ^ (sum + num1) ^ (key[2] + 16 * num1);
16 num1 -= (key[1] + (num2 >> 5)) ^ (sum + num2) ^ (*key + 16 * num2);
17 sum += 0x61C88647;
18 }
19 cout << num1 << ' ' << num2;
20}
21
22int main()
23{
24 tea();
25}
得到结果为 3 和 4,这与伪代码 main()
函数中 v8 = 4;
一致。运行程序,输入 3,确实进入了下一步骤。
下一个关键函数是 sub_411302()
,不过这里面出现了一些问题
TAB 过去,发现是花指令……吗?
经过一些调试,我发现这里其实是一段自修改代码(SMC, Self Modifying Code),尽管现在看起来是一坨,但是运行起来后就不是这样了。打断点、调试、重新分析、中止调试。这样,我们就得到了正确的代码片段。
不戳不戳
这个函数看上去像是一个 RC4 加密,密码为 "you_are_master",待解密的数据为 v17
。
上脚本
1#include <cstdio>
2#include <iostream>
3#include <cstring>
4
5using namespace std;
6
7int S[256], T[256];
8string K = "you_are_master";
9char D[256];
10
11void init(int klen)
12{
13 for (int i = 0; i < 256; i++)
14 {
15 S[i] = i;
16 T[i] = K[i % klen];
17 }
18}
19
20void exc()
21{
22 int j = 0;
23 int temp;
24 for (int i = 0; i < 256; i++)
25 {
26 j = (j + S[i] + T[i]) % 256;
27 temp = S[i];
28 S[i] = S[j];
29 S[j] = temp;
30 }
31}
32
33void encrypt(int dlen)
34{
35 int i = 0, j = 0, t;
36 int temp;
37 for (int h = 0; h < dlen; h++)
38 {
39 i = (i + 1) % 256;
40 j = (j + S[i]) % 256;
41 temp = S[i];
42 S[i] = S[j];
43 S[j] = temp;
44 t = (S[i] + S[j]) % 256;
45 D[h] ^= S[t];
46 }
47}
48
49int main()
50{
51 int klen = 14, dlen = 35, i = 0;
52 D[0] = 0xF;
53 D[1] = 0x94;
54 D[2] = 0xAE;
55 D[3] = 0xF2;
56 D[4] = 0xC0;
57 D[5] = 0x57;
58 D[6] = 0xC2;
59 D[7] = 0xE0;
60 D[8] = 0x9A;
61 D[9] = 0x45;
62 D[10] = 0x37;
63 D[11] = 0x50;
64 D[12] = 0xF5;
65 D[13] = 0xA0;
66 D[14] = 0x5E;
67 D[15] = 0xCB;
68 D[16] = 0x2C;
69 D[17] = 0x16;
70 D[18] = 0x28;
71 D[19] = 0x29;
72 D[20] = 0xFE;
73 D[21] = 0xFF;
74 D[22] = 0x33;
75 D[23] = 0x46;
76 D[24] = 0xE;
77 D[25] = 0x57;
78 D[26] = 0x82;
79 D[27] = 0x22;
80 D[28] = 0x52;
81 D[29] = 0x26;
82 D[30] = 0x2B;
83 D[31] = 0x6E;
84 D[32] = 0xE4;
85 D[33] = 0x82;
86 D[34] = 0x24;
87 init(klen);
88 exc();
89 encrypt(dlen);
90 for (int i = 0; i < dlen; i++)
91 {
92 cout << char(D[i]);
93 }
94}
HDCTF{y0u_ar3_rc4_t3a_smc_m4ster!!}
通过这道题,还有以下几点需要记录:
- FindCrypt 插件在它该识别出 RC4 算法时没有将其识别出来。因此熟练运用瞪眼法的能力还挺必要的。
- 就我检索到的资料来说,SMC 出现时应该有一些特征,例如使用
VirtualProtect()
函数(Windows 系统)以及mprotect()
函数(Linux 系统)来获取修改内存的权限。然而在这一道题里并没有出现这样的特征。
花指令
题目附件叫做虽然他送了我玫瑰花!.exe
。虽然题目名称叫做“花指令”,但是似乎并没有什么花指令出现。
一打开反汇编界面,main()
函数这里竟然是红的,框出来创建函数。
代码非常易于理解。输入内容的长度为 29,在 funcs_40117E
的函数列表循环操作输入的字符,最后与 v9
中的数据相同。然而 v9
只有 16 个字节,而需要判断的有 29 个字符。不出意外,v9
、v10
、v11
、v12
和 v13
的地址连续,都看作是 v9
数组。
函数列表里的五个函数也很简单:
1int __cdecl sub_401080(int a1)
2{
3 return a1 ^ 0x19;
4}
5
6int __cdecl sub_401090(int a1)
7{
8 return a1 + 18;
9}
10
11int __cdecl sub_4010A0(int a1)
12{
13 return a1 - 16;
14}
15
16int __cdecl sub_4010B0(char a1)
17{
18 return 2 * (a1 & 0x7F);
19}
20
21int __cdecl sub_4010C0(int a1)
22{
23 return a1 ^ (a1 ^ ~a1) & 0x80;
24}
脚本如下:
1#include <stdio.h>
2
3unsigned char data[29] =
4 {
5 0x7F, 0x7E, 0x51, 0xCE, 0xFB, 0x4E, 0x7A, 0x24, 0xE8, 0xDF,
6 0x59, 0x71, 0x26, 0xCA, 0xE1, 0x6C, 0x86, 0x21, 0xCC, 0xF5,
7 0x28, 0x71, 0x14, 0xD8, 0xEF, 0x6E, 0x77, 0x62, 0xFA
8 };
9unsigned char data0[29] = {0};
10
11unsigned char func0(unsigned char c)
12{
13 return (c ^ 0x19);
14}
15
16unsigned char func1(unsigned char c)
17{
18 return (c - 18);
19}
20
21unsigned char func2(unsigned char c)
22{
23 return (c + 16);
24}
25
26unsigned char func3(unsigned char c)
27{
28 return (c / 2);
29}
30
31unsigned char func4(unsigned char c)
32{
33 return (c - 0x80);
34}
35
36int main()
37{
38 for (int i = 0; i < 29; i++)
39 {
40 switch (i % 5)
41 {
42 case 0:
43 data0[i] = func0(data[i]);
44 break;
45 case 1:
46 data0[i] = func1(data[i]);
47 break;
48 case 2:
49 data0[i] = func2(data[i]);
50 break;
51 case 3:
52 data0[i] = func3(data[i]);
53 break;
54 case 4:
55 data0[i] = func4(data[i]);
56 break;
57 default:
58 break;
59 }
60 printf("%c", data0[i]);
61 }
62}
flag{Wh4t@6eaut1fu1$lower}_
做题的时候出了一些问题。我最开始在脚本中写入密文时,顺序是 6C E1 ... EF FA
,这样符合反编译的代码,但是运行脚本后会输出乱码。因此需要通过动态调试,我才能在内存中找到正确的顺序,即 7F 7E ... 62 FA
。
攻防世界
riskv-and-reward
题目附件拖到 DIE 里,程序使用了 RISC-V 架构
因此拖进 IDA 里的时候,应该手动选一个 RISC 架构
汇编指令看不了一点,F5 也罢工了,唯一有用的是找到了一个字符串,而且没有显示交叉引用。
这个字符串里有上下花括号,应该是一个乱序的 flag,怀疑是栅栏密码。不过栅栏密码遍历解密之后没有找到什么有用的信息,遂放弃,转用 Ghidra 分析。
在 Ghidra 里找到这一字符串,其中显示了引用它的函数 FUN_0001232c()
,并且可以将该函数反编译。伪代码如下:
1undefined8 FUN_0001232c(void)
2
3{
4 int aiStack_d8 [32];
5 undefined auStack_58 [68];
6 int local_14;
7
8 FUN_00012524(auStack_58,&DAT_00011040,0x40);
9 FUN_00012524(aiStack_d8,&DAT_00011080,0x80);
10 for (local_14 = 0; local_14 < 0x20; local_14 = local_14 + 1) {
11 FUN_00012724(auStack_58[aiStack_d8[local_14]]);
12 }
13 FUN_00012724(10);
14 return 0;
15}
根据以下几点:
- 0x40 与 0x80 恰好分别是
DAT_00011040
与DAT_00011080
的大小 DAT_00011080
中每隔三个 0x00 会出现一个字节的有效数据,而aiStack_d8
的类型为int
FUN_00012724(auStack_58[aiStack_d8[local_14]])
中下标嵌套下标,似乎是在查表,那么DAT_00011040
是乱序的 flag- 根据变量名来看
auStack_58
与aiStack_d8
的距离恰好是 0x80
我们不妨猜测,FUN_00012524()
函数是 memcpy()
函数,FUN_00012724()
,DAT_00011040
中存储的是乱序 flag,DAT_00011080
中存储的是 flag 中每个字符的顺序。
上脚本试试
1seq = [0x28, 0x21, 0x2F, 0x34, 0x2D, 0x36, 0x06, 0x1F, 0x25, 0x3B, 0x29, 0x03, 0x37, 0x3E, 0x1B, 0x05, 0x22, 0x13, 0x14, 0x3A, 0x31, 0x30, 0x1A, 0x10, 0x08, 0x23, 0x07, 0x24, 0x3C, 0x2C, 0x00, 0x18]
2string = "tjb3csFt0rrutrh_wiv5__fi}k_1ih`{xIcrhsoyBmyw1CyT3rvxStT_jq40_zrq"
3
4for i in seq:
5 print(string[i], end='')
BITSCTF{s0m3_r1sc5_4r3_w0rth_1t}
梅津美治郎
一道动调题,题目名字和描述都好怪,而且和题目内容没什么关系。
先解释几个函数
GetModuleHandleA()
语法如下:
1HMODULE GetModuleHandleA(
2 [in, optional] LPCSTR lpModuleName
3);
参数 [in, optional] LPCSTR lpModuleName
是待加载的模块名称,函数返回值是指定模块的句柄。
GetProcAdress()
语法如下:
1FARPROC GetProcAddress(
2 [in] HMODULE hModule,
3 [in] LPCSTR lpProcName
4);
参数 [in] hModule
是包含函数或变量的句柄,[in] lpProcName
是函数或变量名,函数返回值为函数或变量的地址。
看程序
程序没有加壳,直接进 IDA。程序前面放了一堆数,后面是程序的核心。
Str2
似乎似乎就是明文,strcmp(Str1, Str2)
里是检测第一次输入的内容,直接输入 "r0b0RUlez!" 就可以过第一层判断。
第二层判断在函数 sub_40157F() -> sub_401547()
内
即将第二层密码与 dword_40AD98
里的内容异或 2 作比较。需要打断点动调才能看到 dword_40AD98
里的内容。
异或之后得到第二段密码,拼起来就是 flag _flag{r0b0RUlez!w3lld0ne}
这道题的诡异之处在于,程序并没有按照代码里写的顺序执行,而且程序几乎所有在运行期间输出的字符串都是在程序运行的过程中计算生成的,而非一开始就存储在数据里。不知道程序运行到哪里,对于一道需要动态调试的题目来说,确实有一定迷惑性。
babyarm
TNND!flag 错误,官方 WP 错误!!!
这道题的程序使用了 ARM 架构汇编,我配置 QEMU 模拟器调试程序未果。故纯静态分析得到 flag。此外,我能找到的 IDA 8.3 都只支持 x64/x86 反编译器。遂使用 IDA 7.7。
一上来就爆红,先强制分析,手动去花指令。
下面都是类似的操作,随后重新分析。
不难看出,左侧出现了 main()
函数。F5 一键反编译后不难看出,输入的 flag 被切成两部分进行判断。
1// bad sp value at call has been detected, the output may be wrong!
2int __fastcall main(int a1, char **a2, char **a3)
3{
4 const void *v3; // r2
5 int v4; // r4
6 int v6; // r3
7 _DWORD v8[3]; // [sp+8h] [bp-78h]
8 char v9; // [sp+14h] [bp-6Ch]
9 _DWORD v10[3]; // [sp+18h] [bp-68h]
10 char v11[12]; // [sp+24h] [bp-5Ch] BYREF
11 _DWORD v12[2]; // [sp+30h] [bp-50h] BYREF
12 _DWORD v13[3]; // [sp+38h] [bp-48h]
13 int (__fastcall *v14)(_DWORD, _DWORD); // [sp+44h] [bp-3Ch]
14 int v15; // [sp+48h] [bp-38h]
15 int v16; // [sp+4Ch] [bp-34h]
16 int v17; // [sp+50h] [bp-30h]
17 void *v18; // [sp+54h] [bp-2Ch]
18 void *v19; // [sp+58h] [bp-28h]
19 int fd; // [sp+5Ch] [bp-24h]
20 void *addr; // [sp+60h] [bp-20h]
21 int v22; // [sp+64h] [bp-1Ch]
22 int i; // [sp+68h] [bp-18h]
23 int v24; // [sp+6Ch] [bp-14h]
24 void *dest; // [sp+70h] [bp-10h]
25 char v26[4]; // [sp+74h] [bp-Ch] BYREF
26
27 memset(v11, 0, sizeof(v11));
28 v12[0] = 0;
29 v12[1] = 0;
30 v13[0] = 0;
31 *(v13 + 3) = 0;
32 fd = open("/dev/zero", 0);
33 addr = mmap(0, 0x1000u, 7, 2, fd, 0);
34 v19 = addr;
35 dest = 0;
36 memcpy(0, v3, 0xC0u);
37 sub_10684(addr);
38 v18 = addr;
39 v16 = addr + 64;
40 v17 = addr + 128;
41 v15 = addr + 128;
42 v24 = 0;
43 v10[1] = addr + 64;
44 v10[2] = addr + 128;
45 v8[0] = 0x4FFED263;
46 v8[1] = 0x3F00D9B9;
47 v8[2] = 0x504380A0;
48 v9 = 0x55;
49 for ( i = 0; i <= 12; ++i )
50 {
51 v14 = v10[i % 3];
52 v4 = *(v8 + i);
53 if ( v4 != v14(v11[i], byte_2103C[i]) )
54 {
55 puts("Operation failed!!!");
56 return -1;
57 }
58 }
59 puts("Check next section");
60 v13[2] = 6;
61 while ( v22 <= 5 )
62 {
63 v6 = v22 & 1;
64 if ( v22 < 0 )
65 v6 = -v6;
66 (*&v26[4 * v6 - 116])(&byte_2104C[6 * v22++]);
67 }
68 if ( sub_10770(byte_2104C, v12 + 1) )
69 puts("you find the whole flag!");
70 else
71 puts("what a pity!");
72 munmap(addr, 0x1000u);
73 return 0;
74}
第一部分的 v14
是一个函数指针,由三个函数循环对我们输入内容的前 13 位及已知的 byte_2103C
中的数据进行操作,并与 已知的 v4
进行比对。IDA 在分析这段代码时的表现很差。有函数 *sub_105D4()
包含在 init_array
中(在 IDA View-A 中可见),先于 main()
函数执行。该函数对 byte_21088
进行操作,byte_21088
大小为 0xBF。
1void *sub_105D4()
2{
3 void *result; // r0
4 char dest[192]; // [sp+4h] [bp-C8h] BYREF
5 unsigned int i; // [sp+C4h] [bp-8h]
6
7 result = memcpy(dest, &unk_10D28, sizeof(dest));
8 for ( i = 0; i <= 0xBF; ++i )
9 byte_21088[i] ^= dest[i];
10 return result;
11}
有 sub_10684()
对 v1
中的数据进行操作,v1
大小同样是 0xBF,不妨猜测此数据块与函数 *sub_105D4()
中的 byte_21088
是指向的同一段数据。
1int sub_10684()
2{
3 int result; // r0
4 int v1; // [sp+4h] [bp-18h]
5 char v2[8]; // [sp+Ch] [bp-10h] BYREF
6 unsigned int i; // [sp+14h] [bp-8h]
7
8 result = *"SECRET";
9 strcpy(v2, "SECRET");
10 for ( i = 0; i <= 0xBF; ++i )
11 {
12 result = *(v1 + i);
13 *(v1 + i) = v2[i % 6] ^ result;
14 }
15 return result;
16}
下面为 byte_21088
中数据,由 main()
函数中 mmap()
函数猜测,这一段为 SMC。这一段数据长度也刚好为 0xBF,因此上述两个函数是 SMC 函数。由于不能动态调式,我们用 IDA Python 脚本修改这段数据,并重新分析。
1cipher_start = 0x10D28
2opcode_start = 0x21088
3cipher = "SECRET"
4
5for addr in range(0, 0xC0):
6 cip = int(idc.get_wide_byte(cipher_start + addr))
7 val = int(idc.get_wide_byte(opcode_start + addr))
8 new_val = cip ^ val ^ ord(cipher[addr % 6])
9 ida_bytes.patch_byte(opcode_start + addr, new_val)
这样我们就获得了 sub_21088()
、sub_120C8
、sub_21108()
三个函数,与 main()
函数中 v18 = addr, v16 = addr + 64, v17 = addr + 128;
中的地址偏移量相对应,也正好是 v14
中的三个函数。此时可以解出 flag 前半段。
1key = [0xFD, 0x9A, 0x9F, 0xE8, 0xC2, 0xAE, 0x9B, 0x2D, 0xC3, 0x11, 0x2A, 0x35, 0xF6]
2cip = [0x63, 0xD2, 0xFE, 0x4F, 0xB9, 0xD9, 0x00, 0x3F, 0xA0, 0x80, 0x43, 0x50, 0x55]
3
4for i in range(0, 13):
5 if i % 3 == 0:
6 print(chr((cip[i] - key[i]) & 0x7F), end="")
7 if i % 3 == 1:
8 print(chr((cip[i] + key[i]) & 0x7F), end="")
9 if (i % 3) == 2:
10 print(chr((cip[i] ^ key[i]) & 0x7F), end="")
flag{welcome_
后半段 flag 的判断是通过 sub_10770()
函数实现的。
1int __fastcall sub_10770(int a1, int a2)
2{
3 _DWORD v4[3]; // [sp+Ch] [bp-20h]
4 char v5; // [sp+18h] [bp-14h]
5 int i; // [sp+1Ch] [bp-10h]
6 int v7; // [sp+20h] [bp-Ch]
7 int v8; // [sp+24h] [bp-8h]
8
9 v8 = 4;
10 v4[0] = 0x41203E53;
11 v4[1] = 0xB242C1E;
12 v4[2] = 0x52372836;
13 v5 = 0xE;
14 for ( i = 0; i <= 12; ++i )
15 {
16 *(a2 + i) ^= *(v4 + i);
17 switch ( *(a2 + i) )
18 {
19 case 'a':
20 --v8;
21 break;
22 case 'd':
23 ++v8;
24 break;
25 case 's':
26 ++v7;
27 break;
28 case 'w':
29 --v7;
30 break;
31 default:
32 break;
33 }
34 if ( *(a1 + 6 * v7 + v8) == '*' )
35 {
36 puts("Boom!!!");
37 return 0;
38 }
39 }
40 return 1;
41}
似乎是一个迷宫,通过 WASD 控制位移,但是是异或处理过后成为 WASD。传入这个函数的另一个参数 byte_2104C
似乎真的像一个迷宫。
不过这个迷宫不大对劲。在 main()
函数里调用 sub_10770()
前先有 (*&v26[4 * v6 - 116])(&byte_2104C[6 * v22++]);
。而 (*&v26[4 * v6 - 116])
指向的函数似乎找不到。不过好在这道题里出现的函数相当少,我们不妨一个一个找找看。最后发现 sub_104FC()
与 sub_10568()
函数中 i <= 5
的循环跳出条件与 main()
中 v22 <= 5
竟然出奇地相似。故猜测这两个函数对迷宫进行初始化。不妨靠这个找出初始化后的迷宫,经过尝试,使用下面的脚本可以大体改成组成迷宫的字符串。
1start = 0x2104C
2end = 0x21088
3
4for addr in range(start, end):
5 data = int(idc.get_wide_byte(addr))
6 if chr(data) == 'T' or data == 0x88:
7 new_data = data >> 1
8 else:
9 new_data = data ^ 0x10
10 ida_bytes.patch_byte(addr, new_data)
这只是一个猜测,作出这样猜测的依据是 sub_10770()
函数中 *(a1 + 6 * v7 + v8) == '*'
的判断。据此也能判断出这一由 60 个字符绘制出的迷宫一共有 6 列 10 行。通过以下代码打印出可能的迷宫:
1print('\n')
2maze = "******* E**P***** *****P***** *****P***** *****D*******"
3for i in range(0, 6):
4 print(maze[i * 10: i * 10 + 10])
5
6'''
7 output:
8 ******
9 * E*
10 *P****
11 * ****
12 *P****
13 * *
14 ****P*
15 **** *
16 ****D*
17 ******
18'''
另外根据 v8 = 4;
可以知道,迷宫的七点在某行第四列。经过尝试,迷宫起点为 E 点,终点为 D 点。经过的路径为 aaassssdddsss,而这是经过异或后的结果。写出脚本解出第二部分 flag。
1path = "aaassssdddsss"
2crypt = [0x53, 0x3E, 0x20, 0x41, 0x1E, 0x2C, 0x24, 0xB, 0x36, 0x28, 0x37, 0x52, 0xE]
3for i in range(0, 13):
4 print(chr(ord(path[i]) ^ crypt[i]), end='')
2_A2m_WoRLD!}
完整脚本:
1key = [0xFD, 0x9A, 0x9F, 0xE8, 0xC2, 0xAE, 0x9B, 0x2D, 0xC3, 0x11, 0x2A, 0x35, 0xF6]
2cip = [0x63, 0xD2, 0xFE, 0x4F, 0xB9, 0xD9, 0x00, 0x3F, 0xA0, 0x80, 0x43, 0x50, 0x55]
3
4for i in range(0, 13):
5 if i % 3 == 0:
6 print(chr((cip[i] - key[i]) & 0x7F), end="")
7 if i % 3 == 1:
8 print(chr((cip[i] + key[i]) & 0x7F), end="")
9 if (i % 3) == 2:
10 print(chr((cip[i] ^ key[i]) & 0x7F), end="")
11
12# part 1
13
14path = "aaassssdddsss"
15
16crypt = [0x53, 0x3E, 0x20, 0x41, 0x1E, 0x2C, 0x24, 0xB, 0x36, 0x28, 0x37, 0x52, 0xE]
17for i in range(0, 13):
18 print(chr(ord(path[i]) ^ crypt[i]), end='')
19
20# part 2
flag{welcome_2_A2m_WoRLD!}
MISC - ext3
题目附件是一块磁盘的数据(有好多 00 ),有头绪但不多。我强行在 010 Editor 里找可能的关键字,找到了一个文件 flag.txt 的路径 ~root/Desktop/file/O7avZhikgKgbF/flag.txt
,最后通过尝试“Zmxh”关键字得到 Base64 编码后的 flag,ZmxhZ3tzYWpiY2lienNrampjbmJoc2J2Y2pianN6Y3N6Ymt6an0=
。
解码后得到 flag{sajbcibzskjjcnbhsbvcjbjszcszbkzj}
然而这道题正确的解法不应该是这样的。如果出题人没有使用 Base64 编码,或者使用了套娃加密的方式,那我就难以用这个方法得到 flag 了。
这是一个 Linux 下的 ext3 格式磁盘,而且是把整坨数据都存储进去了。我妄图将其后缀改为 .iso
,.img
,.rar
,但是在我打开文件的时候系统又会提醒我文件已经损坏。
怎么办呢?既然这块磁盘适配 Linux 文件系统,为什么我不将它挂载到 Linux 里呢?这叫做原汤化原食。把磁盘拖进 Kali Linux,使用命令 sudo mount '/home/jackgdn/Desktop/f1fc23f5c743425d9e0073887c846d23' /mnt
可以将磁盘挂载到 /mnt
文件夹下。根据刚才在 010 Editor 里找到的路径,可以轻松得到 Base64 编码后的 flag。
做完题后卸载磁盘,使用 sudo umount /mnt
命令。
BUUCTF
MISC - 面具下的 flag
题目附件是一张图片。
mianju.jpg
这张图片里大概率是藏了什么东西。我们不妨用 binwalk 找找图片里的“好东西”。在 Linux 里使用 binwalk -e '/home/jackgdn/Desktop/temp files/mianju.jpg'
命令,binwalk 就自动分离出了两个文件,74DFE.zip 与 flag.vmdk。其中 74DFE.zip 解压出的文件就是 flag.vmdk。
这一步也可以用另一种方法来完成。众所周知,.jpg
格式文件的文件尾是 FF D9
,在文件尾后的内容都不会被读取为图像的一部分。因此我们不妨使用 010 Editor 里找到这一文件尾,而后面又刚好是 PK 文件头。从这里到最下面 50 4B 05 06
的 .zip
文件尾部分,都是压缩文件部分。
把前面多余部分删除,剩余部分保存为 mianju.zip
哦?竟然有加密。大概率是伪加密吧。还是用 010 Editor,将压缩源文件目录区域的 09 00
改为 00 00
,密码就会消失。
拿到了一个虚拟磁盘文件。先试试头铁的办法:直接搜。这次运气不错,找到了一个 Ook!
但是这个 Ook! 似乎是损坏的。往下搜索,发现还有一坨 Ook!
不错,但是这里只有个半个 flag:
_i5_funny!}
根据刚才搜索到的内容,我们不妨假设前一半 flag 的后缀也是 .txt
,因此搜索 "t x t" 即 74 00 78 00 74
。
似乎是 brainfuck:
但是实际上这一段也是损坏的,正确的 brainfuck 也是在下面的位置。可以借出来第一段 flag。随后就可以拼成一整个 flag。
flag{N7F5_AD5_i5_funny!}
这一步还有另一个思路,即像攻防世界里 ext3 这道题一样直接将磁盘挂载到操作系统内。使用 ImDisk 工具挂载虚拟磁盘。
可惜,即使挂载到操作系统,/key_part_one/NUL
文件依然是不可打开的,/key_part_two/
下只有一个 where_is_flag_part_two.txt。先在 DiskGenius 里打开虚拟磁盘,可以查看前半段 flag 的内容。
根据前半段 flag 的提示可以知道,后半段 flag 没有出现是因为 ADS 隐写。使用 lads 找隐写的文件流并打开,就是第二段 flag。此外,alternatestreamview 工具也可以查看 ADS 隐写的文件路径。
赵师傅也提供了一种方法,将虚拟磁盘文件的后缀改为 .7z
,并放到 Linux 系统中解压,同样可以得到完整的文件。
ADS 文件隐写是在微软的 NTFS 格式的文件系统内使用的,而 Linux 使用 ext4 文件系统,因此在 Linux 里 ADS 隐写的文件就直接出现了;9982 师傅推测,NUL 文件之所以不能打开是因为 NTFS 头损坏,并不影响文件在 Linux 系统里被读取。
NSSCTF
test your Debugger
若至签到题。不管你输入什么,程序都会把 flag 算出来,直接打断点看内存就可以。它甚至贴心地告诉你,在这里打断点,生怕我拿不到 flag。
NSSCTF{44d52a77-92ea-413e-98c4-ff5846fd387d}
pwn - nc_pwnre
我的评价是:挂 pwn 头卖 re 肉。
nc
连接到环境,出现了一段汇编:
1pwn? re?no no no,this is just an easy nc-test.
2
3loc_40116D:
4mov eax, [ebp+i]
5add eax, 1
6mov [ebp+i], eax
7loc_401176:
8mov ecx, [ebp+Str]
9push ecx
10call _strlen
11add esp, 4
12cmp [ebp+i], eax
13jge short loc_40119D
14mov edx, [ebp+Str]
15add edx, [ebp+i]
16movsx eax, byte ptr [edx]
17xor eax, 10h
18mov ecx, [ebp+Str]
19add ecx, [ebp+i]
20mov [ecx], al
21jmp short loc_40116D
22maybe the result is talking about xor?
23My result:
240x44,0x7c,0x5e,0x44,0x41,0x21,0x42,0x57,0x75,0x21,0x74,0x56,0x44,0x57,0x5d,0x67,0x44,0x46,0x29,0x45,0x5d,0x56,0x29,0x67,0x46,0x22,0x25,0x76,0x74,0x6a,0x52,0x69,0x5d,0x47,0x41,0x78,0x76,0x41,0x2d,0x2d
25
26your answer?
汇编逻辑简单,将字符串中的某一个字符存入 eax
寄存器后再做一次 xor eax, 10h
操作,随后对下一个字符做相同操作。我只能说,把逆向题出道远程服务器里它也是逆向题。
1flag = [0x44,0x7c,0x5e,0x44,0x41,0x21,0x42,0x57,0x75,0x21,0x74,0x56,0x44,0x57,0x5d,0x67,0x44,0x46,0x29,0x45,0x5d,0x56,0x29,0x67,0x46,0x22,0x25,0x76,0x74,0x6a,0x52,0x69,0x5d,0x47,0x41,0x78,0x76,0x41,0x2d,0x2d]
2for i in flag:
3 print(chr(i ^ 0x10), end = '')
得到一串 Base64:TlNTQ1RGe1dFTGMwTV9UMF9wV25fdzByMWQhfQ==,解码之后是 NSSCTF{WELc0M_T0_pWn_w0r1d!}。不过这不是最终 flag,需要把它填到终端里,然后获得远程主机的权限。
NSSCTF{7f840e08-33c7-44a3-8c01-d4a34362de59}