近期解题 2023.12.26

文章目录

擂台赛 - 123456789

题目源代码如下

1from secret import flag
2data = input('> ')
3assert len(data) <= 9 and all(i not in '123456789' for i in data) and int(data) == 123456789
4print(flag)

看上去不是很麻烦。题目会在远程服务器上运行,secret 模块及其中的 flag 常量都存储在远程服务器上。 如果我们输入的 data 能够满足这个 assert 中的条件,flag 就会自己跳出来。

条件如下:

  1. data 的长度小于等于 9

  2. data 中不含 “123456789” 中的任意一个字符

  3. data 转化为整型后与 123456789 相等

似乎条件 2、3 矛盾了,两个条件不可以同时实现。但是真的如此吗?

Unicode 编码归一化

文字处理软件在实现统一码字符串的搜索和排序时,须考虑到等价性的存在。如果没有此特性的话,用户在搜索时将无法找到在视觉上无法区分的字形。

通俗来讲,例如说在 Unicode 中合字 ffi (U+FB03) 在视觉上等同于 ffi 三个字符拼凑而成,因此需要计算机软件能够识别 ffi 三个字符等同于 ffi 合字字符,以便于用户检索。

实际上的 Unicode 编码归一化的算法比这要更为复杂,并分为了 NFDNFCNFKDNFKC 四种算法,每一种算法都有不同的用处。

话说回来,这和我们要解决的问题有什么关系吗?Natürlich! 这道题目使用的语言 Python 支持 NFKC ,也就是说,我们应该找到一个由“视觉上相同”的 123456789 字符串。这样的话,这个字符串中的任意一个字符都与 ASCII 编码中的 123456789 不同,而将其强制转化为整型的操作则会被归一为将 ASCII 编码中的 123456789 字符串转化为整型的操作。也就是说,我们需要找到合适的 Unicode 字符。

这里有一个可以查找 Unicode 字符的网站:List of Unicode Characters of Bidirectional Class “European Number”,经过筛选,我们找到一下几组可能能成功的字符。由于懒得一个个复制,我们不妨遍历一下。

 1// filename: Exp_123456789
 2
 3public class Exp_123456789 {
 4    public static void main(String[] args) {
 5        // superscript numbers
 6        System.out.printf("%c%c%c", '\u00B9', '\u00B2', '\u00B3');
 7        for(char i = '\u2074'; i <= '\u2079'; i++) {
 8            System.out.print(i);
 9        }
10        System.out.print('\n');
11        // subscript numbers
12        for(char i = '\u2081'; i <= '\u2089'; i++) {
13            System.out.print(i);
14        }
15        System.out.print('\n');
16        // numbers with full stop
17        for(char i = '\u2488'; i <= '\u2490'; i++) {
18            System.out.print(i);
19        }
20        System.out.print('\n');
21        //full width numbers
22        for(char i = '\uFF11'; i <= '\uFF19'; i++) {
23            System.out.print(i);
24        }
25    }
26}
27
28/*
29output:
30¹²³⁴⁵⁶⁷⁸⁹
31₁₂₃₄₅₆₇₈₉
32⒈⒉⒊⒋⒌⒍⒎⒏⒐
33123456789
34*/

Oops! 虽然我们找到可以明确知道有哪些字符至少看上去和一般数字一致,但这似乎不能直接判断这些在 Python 中是否可以被归一为一般数字。唉,在 Python 中重新来一遍。

 1code = "assert len(data) <= 9 and all(i not in '123456789' for i in data) and int(data) == 123456789"
 2try:
 3    data = "¹²³⁴⁵⁶⁷⁸⁹" # superscript numbers
 4    exec(code)
 5    print(data)
 6except:
 7    try:
 8        data = "₁₂₃₄₅₆₇₈₉" # subscript numbers
 9        exec(code)
10        print(data)
11    except:
12        try:
13            data = "⒈⒉⒊⒋⒌⒍⒎⒏⒐" # numbers with full stop
14            exec(code)
15            print(data)
16        except:
17            data = "123456789" # full width numbers
18            exec(code)       
19             print(data)
20
21# output: 123456789

看来只有最后一组字符串是可行的。打开题目环境输入这串字符,果然出了 flag!

flag{U_kn0w_NFKC&Un1c0d3,_r1gh4?}

攻防世界 - crackme

先拖进 IDA

哇哦,这个函数数量,这个伪代码恶心程度,很难不让人想到它是加了壳的。现在拖进 DIE 再看看

果然,有一个 NsPacK 的壳。在网上浏览了一圈没有找到适当的脱壳工具。不妨先试一试曾经尝试过的方法,即运行到程序完全解压后重新分析。把它重新拖进 IDA 并调试

现在程序运行到这个位置,应该已经解压完成了,接下来重新分析。

似乎还是老样子……只好自己尝试手脱壳了。这次尝试一下”esp 定律“脱壳法。

拖进 x64dbg 并直接运行到 pushaf 与 pushad 的位置。pushaf 是将标志位寄存器的值入栈,pushad 是将八个通用寄存器的值入栈。这一步是解压程序运行的开始。

步过到 call 指令,这条指令会执行解压函数,从而得到真正的程序

当 eip 指向 call 指令时,我们就可以用 esp 寄存器中存储的地址设置断点了。在内存一窗口中右键地址 -> 断点 -> 硬件,访问 -> 2 字节。随后就能在寄存器窗口看到 DR0 的值已经变了。

然后直接运行到断点。这时候不难看出 eip 寄存器指向了 popfd 指令,说明程序已经解压完成了。

紧接着 popfd 指令的是一个跳转到 0x401336 的指令(当前地址 0x40641D),看来是要正式执行程序了。

步进一次使 eip 指向这一大跳,现在这个跳转过去的地方就是脱过壳的地方了。在插件 -> Scylla -> IAT Autosearch -> Dump,我们就得到了一个新的 dump 过的可执行文件。

当然也许它不可执行……

不过没有关系。我们已经拿到了脱过壳但不可执行的可执行文件。把它重新拖进 IDA。

芜湖,这就看上去简单多了。程序的逻辑非常简单,我们输入的字符串长度为 42,而且这个字符串与 byte_402130 中的数据循环异或后需要与 dword_402150 中的数据相同。两组数据我就不摆出来了,直接放脚本。

 1#include <iostream>
 2#include <cstdio>
 3#include <cstring>
 4
 5using namespace std;
 6
 7int main()
 8{
 9    unsigned char chars[] =
10    {
11      0x12, 0x04, 0x08, 0x14, 0x24,
12      0x5C, 0x4A, 0x3D, 0x56, 0x0A,
13      0x10, 0x67, 0x00, 0x41, 0x00,
14      0x01, 0x46, 0x5A, 0x44, 0x42,
15      0x6E, 0x0C, 0x44, 0x72, 0x0C,
16      0x0D, 0x40, 0x3E, 0x4B, 0x5F,
17      0x02, 0x01, 0x4C, 0x5E, 0x5B,
18      0x17, 0x6E, 0x0C, 0x16, 0x68,
19      0x5B, 0x12,
20    };
21    string tinf = "this_is_not_flag";
22    for (int i = 0; i < 42; i++)
23    {
24        printf("%c", tinf[i % 16] ^ chars[i]);
25    }
26}
27
28// output: flag{59b8ed8f-af22-11e7-bb4a-3cf862d1ee75}

NCTF - Jump For Flag

举办方将其定位到 MISC 方向中,但个人认为这是一道简单的逆向题

这道题附件是一个 Made With Unity 的小游戏。只要按下空格键跳起来,天上就会随机掉落数个二维码的像素。这些像素块在生成的时候是在它“应该在的位置上”,但是下落的过程中它们在做布尔运动,以至于落到地上后就成了散装二维码。如图

照这样说,多跳几下实际上是不能让一个二维码完整出现在地面的,它们只会变成一坨()

比较简单的一个思路,既然签到题的二维码是整坨落下来,那么可以把第二题的二维码放到签到题里。

用 dnSpy 打开第二题中的 Assembly-CSharp.dll,可以在 CubeGenerator() 方法里找到方块对象生成时的属性,在后面的 makecube() 方法里找到对方块对象属性的定义。

CubeGenerator()

CubeGenerator()

makecube()

 1private void makecube(int x, int y, int z, string col)
 2{
 3	GameObject gameObject = Object.Instantiate<GameObject>(this.cube, new Vector3((float)x, (float)y, (float)z), Quaternion.identity);
 4	MeshRenderer component = gameObject.GetComponent<MeshRenderer>();
 5	if (col == "black")
 6	{
 7		component.material.color = Color.black;
 8	}
 9	else
10	{
11		component.material.color = Color.white;
12	}
13	gameObject.GetComponent<Rigidbody>().AddForce(new Vector3(Random.Range(-10f, 10f), 0f, Random.Range(-10f, 10f)), ForceMode.Impulse);
14}

CubeGenerator() 中总共创建了 900 个方块对象,而且在每一个 new int[] 中的四个数据分别是 x轴,y轴,z轴与颜色。

再用 dnSpy 打开签到题的 Assembly-CSharp.dll

CubeGenerator()

CubeGenerator()

makecube()

 1private void makecube(int x, int y, int z, string col)
 2{
 3	MeshRenderer component = Object.Instantiate<GameObject>(this.cube, new Vector3((float)x, (float)y, (float)z), Quaternion.identity).GetComponent<MeshRenderer>();
 4	if (col == "black")
 5	{
 6		component.material.color = Color.black;
 7		return;
 8	}
 9	component.material.color = Color.white;
10}

哇哦,几乎是一样的。也就是说,这个思路基本具有可行性。下面要做的就是把第二题的二维码 900 个方块的数据 Copy-Paste 到签到题里面,然后编译保存。

NCTF{25d8fdeb-0cb6-4ad4-8da1-788a72e701f0}

Sehr gut!