“1001夜”学生节线上解谜活动1001/key题解

http://www.net9.org/StudentFestival/1001/是模仿东京大学设计的海报谜题:

前几关的解法可以参考http://ppwwyyxx.com/2013/Student-Festival-Puzzle/,下面以出题人角度给出其中的1001/key的题解。

1001/key下载

本题是一道crackme,直接运行程序会提示输入密码,如果密码输入错误就会显示meow

gdb打开程序,输入run命令执行会发现程序陷入了死循环。此时使用C-c停止程序可以发现当前eip之前的几条指令中有int 0x80进行了系统调用,eax为26代表ptrace。Linux i386的系统调用约定是当参数不超过6时,各参数分别储存在ebxecxedxesiediebp,结合ebxecxedx等的零值可以知道程序调用了syscall(SYS_ptrace, PTRACE_ME, 0, 0, 0),当返回值小于零时就陷入死循环。一个已经被PTRACE_ATTACH调试的程序再次执行上述调用返回值为-1,因此在gdb中直接执行会陷入死循环。绕过这个反调试技巧很容易,把int 0x80指令改成两条nop即可,或者使用其他方式比如修改条件跳转指令的目标到下一条指令等。

这个陷阱我是通过函数的__attribute__((constructor))修饰符添加到程序里的,另外可以使用objdump -sj.ctors 1001/key发现引用该函数的指针(以big-endian形式储存):

1
2
3
4
5
6
% objdump -sj.ctors 1001/key

hh_stripped: file format elf32-i386

Contents of section .ctors:
8049f08 ffffffff 40840408 00000000 ....@.......

anti函数的基址为0x08048440,可以看到出现在.ctors段中。anti的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void anti() __attribute__((constructor));
void anti()
{
__asm__ volatile(
"movl %0, %%eax;"
"xorl %%ebx, %%ebx;"
"xorl %%ecx, %%ecx;"
"xorl %%edx, %%edx;"
"xorl %%esi, %%esi;"
"int $0x80;"
"test %%eax, %%eax;"
"1: jl 1b;"
:
: "i"(SYS_ptrace)
: "eax", "ebx", "ecx", "edx", "esi"
);
}

如果直接使用objdump -d 1001/key查看反汇编结果,会发现输出meow的那个函数(基址为0x080486c1)的输出很奇怪。如果在gdb里动态调试就会发现进入这个函数执行完function prologue不久之后反汇编指令(disassemble命令)发生了变化。这是因为这个elf添加了一个简单的反汇编技巧让这类反汇编器产生了错误的输出。考察这个函数被调用的地方可以看到atoi,再根据反汇编前几行可以发现这个函数期待一个 <987654 的参数值。后面有若干条件判断指令检验参数是否被接受,之后可以通过查看汇编的方式获取函数的keygen算法,另外也有一个取巧的办法。因为参数 <987654,取值范围不大,所以我们可以尝试brute-force枚举参数传递给这个函数考察是否会输出meow以外的串。由于程序使用了getpass从终端设备获取输入,所以简单地使用重定向标准输入的方式或者使用管道不会奏效。查看glibc的源码isc/getpass.c可以发现我们只需让进程失去控制终端即可,因此可以使用socat让进程创建自己的会话从而丢失控制终端:

1
2
% echo a | socat - exec:1001/key,setsid |& xxd
0000000: 5061 7373 776f 7264 3a20 6d65 6f77 0a Password: meow.

另外也可以用LD_PRELOAD替换掉getpass或者修改二进制实现替换。

这个隐藏函数的源码如下:

1
2
3
4
5
6
7
void hidden(int pass)
{
for (const char *p = "u|c\x92vammv\x8c\x91\x88g7"; *p; p++) {
putchar(*p ^ (pass % 31));
pass = pass * 71;
}
}

最后会发现有几个参数值都能使这个函数输出meow以外的串:I don't know the secret.这个地方解决的方式有很多,我很想知道各位选手都采用了什么方法解决的,有任何思路都欢迎告知我 <i#maskray,me>。

查看nm -D 1001/key会发现程序调用了一个库函数putchar,但用strace 1001/key执行正常流程却无法找到这个函数被调用的地方。在objdump -d 1001/key里搜索putchar就会发现0x08048623处有个隐藏的函数,在正常的程序执行流程中没有被调用,而这个函数也接受一个int32_t参数。找到0x080486c1被调用的地方:

1
2
3
80484b9:       e8 5e ff ff ff          call   804841c <atoi@plt>
80484be: 89 04 24 mov DWORD PTR [esp],eax
80484c1: e8 9b 01 00 00 call 8048661 <puts@plt+0x235>

call指令修改成调用0x08048623即可:

1
perl -pe 's/\x9b\x01\x00\x00/pack("l<",unpack("l<",$&)-0x08048661+0x08048623)/ge' 1001/key2 > 1001/key3

把之前得到的几个密码依次传给这个函数会发现其中一个密码能使该函数输出可读的串:dajialaiwanai9,即这个elf所要加密的信息,意义就是“大家来玩ai9”,这里打一下广告吧,智能体在线对战平台:http://ai.net9.org

后记

策划同学找到我问有什么出题思路,我表示毫无想法……后来考虑到计算机系有个之前有个汇编小学期,大家之前都做过bomblab和buflab,于是就往这方面靠。我对于逆向技术也只是初窥门径,第一次出题,原来题目只是一段简单的加密。策划同学夏雨认为太简单了会导致礼物不好分……说要难一些,于是我就在网上翻各种反调试和反汇编技巧,在elf中加入了两个片段阻挠大家,那个隐藏函数出题同学也觉得不太优美,为此可能耽误了大家的好多时间……目前已知的是有两位大牛浪费了好长时间熬夜做题,导致白天在澳门游玩时精神不振,感觉过意不去,向各位致歉了!

ppwwyyxx给的题解相当不错,里面给出的lilydjwg把01串转换成二进制的方法真棒:python2 -c 'while True: import sys; sys.stdout.write(chr(int(sys.stdin.read(8).strip() or sys.exit(), 2)))'去年八月有幸一睹lilydjwg行云流水般的操作,惊异之至,最近又见到两位如此神速的,一位是京JS 2013上见到的James Halliday (substack),另一位是这次澳门香港之行见到的Ricky Zhou。

本题实际上是CTF风格的逆向题,CTF是信息安全领域的一类比赛的名称,考察Web、取证、密码学、二进制、隐写等知识。在国内算法类竞赛很流行,但信息安全受到的关注就比较少。出题人这里当然是有一些私心杂念的……如果认真研究了题目入口网页http://www.net9.org/StudentFestival/1001,就会发现源代码里有一段HTML注释,是一段广告,号召大家来参加http://bctf.cn(BCTF“百度杯”全国网络安全技术对抗赛)。另外也有一支源自清华大学网络与信息安全实验室的战队blue-lotus (http://www.blue-lotus.net),现在也吸纳了包括来自浙江大学、上海交大、青岛理工、中国海洋大学、杭州电子科大等高校的多名学生,以及若干绿盟、阿里巴巴等公司的年轻安全技术人员,曾作为中国的团队首次闯入全球顶级的DEFCON CTF总决赛。但是作为酒井一员还是觉得为什么贵系人士这么少……大家有兴趣参与进来的话请联系blue.lotus.ctf#gmail,com。

学生节的这个解谜活动似乎很受欢迎,我这一周都在澳门/香港,没有人民币没有澳元没有港元,网络条件不佳,而且也因此错过了学生节,迟交了几个作业……伤心事还是不提了,问了几位大牛,本来还想再添加一些PPC和Web的题目,实际上也基本出出来了,但考虑到在这道坑人的题后再加那些难度低的没啥意思……期待这个活动明年办得更加精彩,那些题还是明年再和大家相见吧……