UOJ Logo riteme的博客

博客

交互题中如何防止选手程序在stdin/stdout上搞事情

2017-01-11 09:28:49 By riteme

最近有人到我们学校的OJ来给我们传授人生的经验,告诉我们如何使用C那一套强大的函数和GCC的一些黑科技来实现从stdin直接读入数据。就目前为止,我所了解的Hack手段和防治措施是这样的:

  • 定义类或结构体作为Reader,在Reader的构造函数中写代码,从而在main函数前动手。

交互库也使用一个类或结构体的Reader来读入即可。GCC默认的构造顺序是编译单元的编译顺序,因此需要在编译命令里面,交互库的文件要在选手文件之前。C++11中static全局变量保证在main函数之前构造,在C++11之前并没有定义,但是一般的编译器基本都保证了这一点。

  • 利用GCC扩展init_priority来修改构造顺序,使得选手的Reader比交互库的Reader先构造。

init_priority在GCC和Clang上都有用......

init_priority的声明方法是这样的:

TYPE NAME __attribute__ ((init_priority(n)));

其中$n$必须是正整数,且在$[101, \;65535]$范围内。权值越小优先级越高,也就意味着会在优先级低的变量之前构造。

因此,我们需要将交互库的Reader的优先级设为$101$

需要注意一个事情,假如你在使用cin来读入,那么在读入之前,cin/cout是还没有被构造的,所以需要在Reader前面使用下面的语句来提前初始化cin/cout

static std::ios_base::Init iosinit __attribute__ ((init_priority(101)));

直接使用C的I/O的不会出事。另外一点就是non-POD类型也需要提前构造(如std::stringstd::map和你的其他类和结构体等),在Reader之前定义并且设置优先级。

  • 使用rewind/fseekstdin重新移至开头并读取。

对于,我们所要做的,就是在交互库读取完数据之后,stdin关掉。如果想更好玩一些,可以打开个/dev/urandom给它读:

freopen("/dev/urandom", "r", stdin);
  • 暴力试出token长度从而利用fseek来修改交互库输出。

类似的,交互库其实并不需要token,交互库的token似乎并没有任何卵用,因为利用fseek能够绕过,setbuf也可以偷到......

为此我们可以在Readerstdout也给换掉(换成NULL或者/dev/null给它玩),然后交互库输出时又换回来即可。 也可以使用Unix中的dupdup2来保存stdout。 交互库输出完毕后需要stdout干掉

  • 使用dup复制输入输出句柄,用fdopen重新打开stdin/stdout

充分利用了Unix那套文件操作的强大。对于此,我们需要自己使用dupstdin/stdout复制一份,然后使用close关闭原来的输入输出。大致的代码如下:

#include <unistd.h>  // dup, close, fdopen, STDIN_FILENO, STDOUT_FILENO

#define MAGIC 103  // 随便选个不是很大的数字

class IO {
 public:
    IO() {
        // 读取数据...

        fclose(stdin);
        close(STDIN_FILENO);

        for (int i = 0; i < MAGIC; i++) {
            dup(STDERR_FILENO);  // 防止stdout_copy变为3
        }

        stdouot_copy = dup(STDOUT_FILENO);
        fclose(stdout);
        close(STDOUT_FILENO);
    }

    ~IO() {
        stdout = fdopen(stdout_copy, "w");

        // 输出结果...

        fclose(stdout);
        close(stdout_copy);
    }

 private:
     int stdout_copy;
};

static IO io __attribute__ ((init_priority(101)));

总结一下就是,在没有加密的输入输出中,使用全局变量的构造函数来抢先获得stdin/stdout的控制权,避免选手程序搞事。

2017.6.8: 写了一个输入输出管理类:

#include <ctime>
#include <cstdio>
#include <cstdlib>

#include <unistd.h>

class IOLocker {
 public:
    const int MAX_MOGIC_DUP = 100;

    IOLocker() : in(STDIN_FILENO), out(STDOUT_FILENO) {
        srand(time(0));
        lock();
    }

    void lock() {
        int cnt = rand() % MAX_MOGIC_DUP;

        for (int i = 0; i < cnt; i++) {
            dup(STDERR_FILENO);
        }  // for

        int nin = dup(in);
        fclose(stdin);
        close(in);
        in = nin;

        int nout = dup(out);
        fclose(stdout);
        close(out);
        out = nout;

        stdin = stdout = NULL;
    }

    void unlock() {
        stdin = fdopen(in, "r");
        stdout = fdopen(out, "w");
    }

 private:
    int in, out;
};  // struct IOLocker

食用方法如下:

static IOLocker io __attribute__((init_priority(101)));  // 声明时

// ...
// 需要使用输入输出的时候
io.unlock();

// ...

io.lock();

这样就没有使用token的必要了。

此文可能会不定期更新。

话说是不是数据加密就可以一劳永逸了?但数据加密后如何清真地支持Hack?求dalao帮助。

评论

WuHongxun
赞www
XLightGod
前排%
fjzzq2002
前排%
AntiLeaf
%%%%%%%
cy
我感觉其中一种解决方案是从与库函数交互变成与 stdio 交互,参见 <http://codeforces.com/blog/entry/45307>。 不知道这样还能不能 Hack?
qmqmqm
加密可以支持hack啊,只要将加密程序下发给构造数据的人就好
vfleaking
后排好评
YMDragon
感觉还可以是输入只提供生成数据的参数,具体的数据由交互库临时生成。 (口胡的,不知道可不可行) (只是这样好像不支持hack。。。)
dram
最安全的还是 pipe 通讯,如果要传大量数据就共享内存吧。当然是可以把这两个封装成交互库

发表评论

可以用@mike来提到mike这个用户,mike会被高亮显示。如果你真的想打“@”这个字符,请用“@@”。