最近有人到我们学校的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::string
、std::map
和你的其他类和结构体等),在Reader
之前定义并且设置优先级。
- 使用
rewind/fseek
将stdin
重新移至开头并读取。
对于,我们所要做的,就是在交互库读取完数据之后,将stdin
关掉。如果想更好玩一些,可以打开个/dev/urandom
给它读:
freopen("/dev/urandom", "r", stdin);
- 暴力试出token长度从而利用
fseek
来修改交互库输出。
类似的,交互库其实并不需要token,交互库的token似乎并没有任何卵用,因为利用fseek
能够绕过,setbuf
也可以偷到......
为此我们可以在Reader
中将stdout
也给换掉(换成NULL
或者/dev/null
给它玩),然后交互库输出时又换回来即可。
也可以使用Unix中的dup
和dup2
来保存stdout
。
交互库输出完毕后需要将stdout
干掉。
- 使用
dup
复制输入输出句柄,用fdopen
重新打开stdin/stdout
。
充分利用了Unix那套文件操作的强大。对于此,我们需要自己使用dup
将stdin/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帮助。