用机器人获得OJ输入数据——HDU3337解题报告
Posted by watashi in perl, solution, tags: bots, cookie, cpp, HDOJ, OJ, perl, solution昨天的HDOJ第三场月赛中hhanger出了一道非主流的Guess the number。援引官方解题报告:
本题属于非正常题,纯属娱乐。因为本题最多只有16个字符,所以可以用X分提交法来套取输入数据,可以利用的返回结果至少有6种,把字符先统一转化成小写后,基本上两次提交可以确定一个字符,因此可以在期望时间内得到解。
相信很多acmer对利用返回结果来套取输入数据并不陌生,我们经常用这招来获得case数或检验输入数据是否与题目描述不符。下面这段程序可以判断第off个字符在哪个范围内,利用了HDOJ中G++的TLE, MLE, OLE, RE(ACCESS_VIOLATION, STACK_OVERFLOW, DIVIDE_BY_ZERO)和WA七种不同返回结果。但平时编译器对包括尾递归、空循环和常量的优化此时却成了绊脚石,为了生成我们预期的返回结果,只好让代码复杂一点或产生一些副作用。
// author: watashi
#include <cctype>
#include <cstdio>
#include <cstring>
void gao(int ch) {
if (ch < $_[1]) { // Time Limit Exceeded
while (true);
} else if (ch < $_[2]) { // Memory Limit Exceeded
char* p = new char[128 << 20];
memset(p, 0xff, 128 << 20);
} else if (ch < $_[3]) { // Output Limit Exceeded
while (true) {
fputs("[Output Limit Exceeded] (http://watashi.ws/wabots) quick brown fox jumps over the lazy dog", stdout);
}
} else if (ch < $_[4]) { // Runtime Error (ACCESS_VIOLATION)
int p[1 << 10] = {-1};
putchar(p[1 << 20]);
} else if (ch < $_[5]) { // Runtime Error (STACK_OVERFLOW)
gao(ch);
} else if (ch < $_[6]) { // Runtime Error (INTEGER_DIVIDE_BY_ZERO)
int p = sizeof(char);
printf("%d", sizeof(int) / --p);
} else { // Wrong Answer
return;
}
}
int main() {
int off = $_[0];
for (int i = 0; i < off; ++i) {
getchar();
}
gao(tolower(getchar()));
return 0;
}
有了这段程序,理论上就可以在32次内得到输入数据了。但由于人肉提交难免手抖,判断易出差错,而且需要很多的肉,实际次数远在这之上,不少人都提交上百次后才AC。对于又缺少肉,又容易手抖的我,连尝试的勇气都没有。不过,却可以写个从不手抖,有着用不完的肉的机器人来代劳。于是先实现一个HDOJ的自动提交机模块。
# HDOJAgent.pm
package HDOJAgent;
use strict;
use warnings;
use LWP::UserAgent;
my $prefix = "http://acm.hdu.edu.cn";
my $interval = 60;
my $maxretry = 2;
sub new {
my $class = shift;
my $self = {
user => $_[0] || '',
problemid => $_[1] || 1000,
language => $_[2] || 0,
ua => new LWP::UserAgent(
agent => 'HDOJAgent (http://watashi.ws/wabots)',
cookie_jar => {},
)
};
bless $self, $class;
return $self;
}
sub AUTOLOAD {
my $self = shift;
my $name = $HDOJAgent::AUTOLOAD;
$name =~ s/.*://;
return if $name eq 'DESTROY';
$self->{$name} = shift if @_;
return $self->{$name};
}
sub post {
my ($self, $url, $form) = @_;
my $ua = $self->ua;
for (1 .. $maxretry) {
my $response = $ua->post($url, $form);
if (!$response->is_error) {
return $response->decoded_content;
}
sleep $interval;
}
warn "maxretry exceeded!";
return undef;
}
sub login {
my ($self, $pass) = @_;
$self->post("$prefix/userloginex.php?action=login", {
username => $self->user,
userpass => $pass,
login => 'Sign In'
});
}
sub submit {
my ($self, $code) = @_;
$self->post("$prefix/submit.php?action=submit", {
problemid => $self->problemid,
language => $self->language,
usercode => $code
});
}
sub laststatus {
my $self = shift;
my $user = $self->user;
while (1) {
my $_ = $self->post("http://acm.hdu.edu.cn/status.php?user=$user");
s{^[\s\S]*Pro\.ID.*Exe\.Time.*Exe\.Memory}{}gs;
s{</td><td><a href="/showproblem\.php\?pid=.*$}{}gs;
s{^.*<td>}{}gs;
s{^\s*|\s*|<[^>]*>}{}gs;
return $_ unless /^$|Queuing|Compiling|Running/;
sleep $interval;
}
}
要完成提交操作需要提供cookie,通常有两种办法,一是直接在WebClient.Headers里设置好cookie,以前我用C#写的一个ZOJ的自动提交机就是这么实现的;更简单的办法是给UserAgent初始化一个空的cookie,通过完成login来设置cookie。有了cookie后就可以submit了,submit需要提供problemid, language和usercode。submit后可以通过laststatus来获得你最近一次提交的返回结果。先用A + B Problem来测试一下模块,这里用了caller函数,实现模块的测试和使用两不误。
# HDOJAgent.pm
return 1 if caller;
my $hdoj = new HDOJAgent('wabots');
$hdoj->login('~!@#$%^&*()_+');
$hdoj->problemid(1000); # A + B Problem
$hdoj->language(1); # GCC
$hdoj->submit(<<GCC
main(a,b){while(scanf("%d%d",&a,&b)>0)printf("%d\n",a+b);}
GCC
);
print $hdoj->laststatus, "\n";
最后在wabots.pl中使用HDOJAgent模块,不断通过七分法提交HDU3337,以得到输入数据中的字符,直到EOF。得到的输入数据,答案也就显而易见啦^ ^
#!/usr/bin/perl -w
# http://watashi.ws/wabots
use strict;
use warnings;
use HDOJAgent;
$| = 1;
sub getcpp {
return <<CPP;
...
CPP
}
sub getpos {
my ($min, $max, $cnt) = @_;
my @ret = ();
$max -= $min;
for (my $i = 0; $i <= $cnt; ++$i) {
push @ret, $min + int($max * $i / $cnt);
}
return @ret;
}
my @status = qw(Time Memory Output ACCESS STACK INTEGER Wrong);
my @charset = (' ', '0' .. '9', 'a' .. 'z');
@charset = sort {$a <=> $b} map {ord $_} @charset;
unshift @charset, -1;
my $hdoj = new HDOJAgent('wabots', 3337, 0);
$hdoj->login('~!@#$%^&*()_+');
my ($try, $off, $min, $max, $res) = (0, 0, 0, scalar @charset, '');
while (1) {
++$try;
print "wabots# TRY #$try: [$off] in [$min, $max)\n";
my @pos = getpos($min, $max, scalar @status);
$hdoj->submit(getcpp($off, @charset[@pos[1 .. $#pos - 1]]));
my $status = $hdoj->laststatus;
print "wabots# \t$status\n";
for (my $i = 0; $i < @status; ++$i) {
if ($status =~ /$status[$i]/i) {
$min = $pos[$i];
$max = $pos[$i + 1];
}
}
if ($min == $max - 1) {
last if $charset[$min] < 0;
$res .= chr $charset[$min];
print "wabots# \t[$off] = $charset[$min] ($res)\n";
++$off;
$min = 0;
$max = @charset;
}
sleep 5;
}
print "RESULT = $res\n";
运行上面的程序,输出的日志如下:
声明:请不要使用本文中的任何代码做包括但不限于攻击的任何坏事,本人不希望因此给HDOJ或其他OJ还有自己带来任何麻烦。
Entries (RSS)
仰慕学长的标称。。
其实我觉得能写出这个机器人的至少肉比我多。。。
实际上那个C++程序不用7分,用机器人的话2分就够了;这样wabots.pl也可以简单不少
人肉提交……望而却步啊……
-.- 看到这个题目直接就想到写个机器人了…
zan 同感啊
ym
我也写过lua版本的,发现zoj比poj/uva返回的结果多一些,不过相比有STACK_OVERFLOW的HDOJ比,还是少了一个 -.-
case 1:puts(”I Wanna WA – -”);break;//WA
case 2:for(;;puts(”I Wanna OLE – -”));break;//OLE
case 3:fprintf(stderr, “%dI Wanna FPE – -”, 2/0);break;//FPE
case 4:for(;;);//TLE
case 5:while(1){char *xxe=(char*)malloc(9123456);xxe[12345]=1;}//MLE
case 6:{int *t;t=(int*)&main;t[3]=2;break;}//SegmentationFault
ym. 你这段代码不错 ^ ^
HDOJ FAQ里还有很多种RE,不过浮点数的应该搞不出来
G++也有些问题,while(true) do sth;有的会判WA。可能是silent crash了