pwnable.kr网站上有很多ctf题目,最近我开始从Toddler's Bottle级别入手,记录一些对我有帮助的题目的解题过程。
leg
刚拿到题目时,有三个文件——可执行leg,代码leg.c和反汇编代码leg.asm
首先是leg.c:
//leg.c
#include <stdio.h>
#include <fcntl.h>
int key1(){
asm("mov r3, pc\n");
}
int key2(){
asm(
"push {r6}\n"
"add r6, pc, $1\n"
"bx r6\n"
".code 16\n"
"mov r3, pc\n"
"add r3, $0x4\n"
"push {r3}\n"
"pop {pc}\n"
".code 32\n"
"pop {r6}\n"
);
}
int key3(){
asm("mov r3, lr\n");
}
int main(){
int key=0;
printf("Daddy has very strong arm! : ");
scanf("%d", &key);
if( (key1()+key2()+key3()) == key ){
printf("Congratz!\n");
int fd = open("flag", O_RDONLY);
char buf[100];
int r = read(fd, buf, 100);
write(0, buf, r);
}
else{
printf("I have strong leg :P\n");
}
return 0;
}
main函数主体部分就是读取一个数,如果和key1,key2,key3的返回值之和相等就能够获得正确的flag.但是key函数中都嵌入了汇编代码,从该文件中分析有点棘手,我们来看看leg.asm:
// leg.asm
(gdb) disass main
Dump of assembler code for function main:
0x00008d3c <+0>: push {r4, r11, lr}
0x00008d40 <+4>: add r11, sp, #8
0x00008d44 <+8>: sub sp, sp, #12
0x00008d48 <+12>: mov r3, #0
0x00008d4c <+16>: str r3, [r11, #-16]
0x00008d50 <+20>: ldr r0, [pc, #104] ; 0x8dc0 <main+132>
0x00008d54 <+24>: bl 0xfb6c <printf>
0x00008d58 <+28>: sub r3, r11, #16
0x00008d5c <+32>: ldr r0, [pc, #96] ; 0x8dc4 <main+136>
0x00008d60 <+36>: mov r1, r3
0x00008d64 <+40>: bl 0xfbd8 <__isoc99_scanf>
0x00008d68 <+44>: bl 0x8cd4 <key1>
0x00008d6c <+48>: mov r4, r0
0x00008d70 <+52>: bl 0x8cf0 <key2>
0x00008d74 <+56>: mov r3, r0
0x00008d78 <+60>: add r4, r4, r3
0x00008d7c <+64>: bl 0x8d20 <key3>
0x00008d80 <+68>: mov r3, r0
0x00008d84 <+72>: add r2, r4, r3
0x00008d88 <+76>: ldr r3, [r11, #-16]
0x00008d8c <+80>: cmp r2, r3
0x00008d90 <+84>: bne 0x8da8 <main+108>
0x00008d94 <+88>: ldr r0, [pc, #44] ; 0x8dc8 <main+140>
0x00008d98 <+92>: bl 0x1050c <puts>
0x00008d9c <+96>: ldr r0, [pc, #40] ; 0x8dcc <main+144>
0x00008da0 <+100>: bl 0xf89c <system>
0x00008da4 <+104>: b 0x8db0 <main+116>
0x00008da8 <+108>: ldr r0, [pc, #32] ; 0x8dd0 <main+148>
0x00008dac <+112>: bl 0x1050c <puts>
0x00008db0 <+116>: mov r3, #0
0x00008db4 <+120>: mov r0, r3
0x00008db8 <+124>: sub sp, r11, #8
0x00008dbc <+128>: pop {r4, r11, pc}
0x00008dc0 <+132>: andeq r10, r6, r12, lsl #9
0x00008dc4 <+136>: andeq r10, r6, r12, lsr #9
0x00008dc8 <+140>: ; <UNDEFINED> instruction: 0x0006a4b0
0x00008dcc <+144>: ; <UNDEFINED> instruction: 0x0006a4bc
0x00008dd0 <+148>: andeq r10, r6, r4, asr #9
End of assembler dump.
(gdb) disass key1
Dump of assembler code for function key1:
0x00008cd4 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008cd8 <+4>: add r11, sp, #0
0x00008cdc <+8>: mov r3, pc
0x00008ce0 <+12>: mov r0, r3
0x00008ce4 <+16>: sub sp, r11, #0
0x00008ce8 <+20>: pop {r11} ; (ldr r11, [sp], #4)
0x00008cec <+24>: bx lr
End of assembler dump.
(gdb) disass key2
Dump of assembler code for function key2:
0x00008cf0 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008cf4 <+4>: add r11, sp, #0
0x00008cf8 <+8>: push {r6} ; (str r6, [sp, #-4]!)
0x00008cfc <+12>: add r6, pc, #1
0x00008d00 <+16>: bx r6
0x00008d04 <+20>: mov r3, pc
0x00008d06 <+22>: adds r3, #4
0x00008d08 <+24>: push {r3}
0x00008d0a <+26>: pop {pc}
0x00008d0c <+28>: pop {r6} ; (ldr r6, [sp], #4)
0x00008d10 <+32>: mov r0, r3
0x00008d14 <+36>: sub sp, r11, #0
0x00008d18 <+40>: pop {r11} ; (ldr r11, [sp], #4)
0x00008d1c <+44>: bx lr
End of assembler dump.
(gdb) disass key3
Dump of assembler code for function key3:
0x00008d20 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008d24 <+4>: add r11, sp, #0
0x00008d28 <+8>: mov r3, lr
0x00008d2c <+12>: mov r0, r3
0x00008d30 <+16>: sub sp, r11, #0
0x00008d34 <+20>: pop {r11} ; (ldr r11, [sp], #4)
0x00008d38 <+24>: bx lr
End of assembler dump.
(gdb)
我在先前只接触过x86汇编,对arm并不了解,首要疑问就是函数的返回值究竟是怎么确定的,经过查阅资料,返回的是r0寄存器中的值。我们先看key1(),和r0有关的操作似乎只有以下部分:
0x00008cdc <+8>: mov r3, pc
0x00008ce0 <+12>: mov r0, r3
当第一次看到pc时,我用x86汇编的经验推断它是指令寄存器,指向下一条指令的地址,也即0x8ce0,然而查阅了相关资料之后才发现,arm流水线分为3级,当前执行指令、第二条指令(正在译码)、第三条指令(正在读取),而在arm模式中pc寄存器存放的是第三条指令的地址
因此key1()这些指令之后r0寄存器会被填入0x8ce4.至于最后的指令bx lr,则是跳转到lr寄存器保存的地址位置,和x86中的ret指令基本一样.
再看key3()部分:
0x00008d28 <+8>: mov r3, lr
0x00008d2c <+12>: mov r0, r3
仅仅是把lr寄存器的值放入r0,根据我们的推断,lr应该存放着执行完key3应该跳转的位置,也就是main()函数中的0x8d80位置.
最后我们再看key2():
0x00008cf0 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008cf4 <+4>: add r11, sp, #0
0x00008cf8 <+8>: push {r6} ; (str r6, [sp, #-4]!)
0x00008cfc <+12>: add r6, pc, #1
0x00008d00 <+16>: bx r6
0x00008d04 <+20>: mov r3, pc
0x00008d06 <+22>: adds r3, #4
0x00008d08 <+24>: push {r3}
0x00008d0a <+26>: pop {pc}
0x00008d0c <+28>: pop {r6} ; (ldr r6, [sp], #4)
0x00008d10 <+32>: mov r0, r3
0x00008d14 <+36>: sub sp, r11, #0
0x00008d18 <+40>: pop {r11} ; (ldr r11, [sp], #4)
0x00008d1c <+44>: bx lr
首先就是add指令的操作数,有时为2个,有时为3个.在arm中,add r1,r2,r3会将r2和r3相加并存入r1中,至于两个操作数,我找到了这样的一篇回答:
https://www.coder.work/article/7266021
也就是说,如果省略第一个参数,就和x86中的add指令行为一致,加到第一个源寄存器中
然后是注释中的str,ldr指令,都是加载数据的指令.
当和源代码对比时,发现其中有两个没有见过的标签.code 16和.code32,根据格式应该是伪代码,经过查阅资料,在arm32中(指令4字节)可以使用thumb指令(指令2字节),.code 16就是告诉编译器后面的部分是thumb指令,而再次出现.code 32时就回到了arm32。而我们前面所研究的pc寄存器问题,在thumb指令部分也会发生相应变化,不再是当前指令地址+8,而变成了当前指令地址+4
"push {r6}\n"
"add r6, pc, $1\n"
"bx r6\n"
".code 16\n"
"mov r3, pc\n"
"add r3, $0x4\n"
"push {r3}\n"
"pop {pc}\n"
".code 32\n"
"pop {r6}\n"
到此为止,唯一的疑惑点就只剩key2汇编代码中的这一行: 0x00008cfc <+12>: add r6, pc, #1
如果将pc中的值+1,跳转到那里不久乱套了吗?查了很久才知道+1或者+0只是为了让bx指令知道自己要跳转的是那一类型的指令处(+1对应thumb,+0对应arm),在真正跳转到的目标会忽略最后一个比特。
当弄清楚了上面的东西,我们可以很容易知道key2()返回的值是0x8d0c,将这三个数字加起来就是十进制108400,输入即可得到flag
mistake
mistake.c代码如下:
#include <stdio.h>
#include <fcntl.h>
#define PW_LEN 10
#define XORKEY 1
void xor(char* s, int len){
int i;
for(i=0; i<len; i++){
s[i] ^= XORKEY;
}
}
int main(int argc, char* argv[]){
int fd;
if(fd=open("/home/mistake/password",O_RDONLY,0400) < 0){
printf("can't open password %d\n", fd);
return 0;
}
printf("do not bruteforce...\n");
sleep(time(0)%20);
char pw_buf[PW_LEN+1];
int len;
if(!(len=read(fd,pw_buf,PW_LEN) > 0)){
printf("read error\n");
close(fd);
return 0;
}
char pw_buf2[PW_LEN+1];
printf("input password : ");
scanf("%10s", pw_buf2);
// xor your input
xor(pw_buf2, 10);
if(!strncmp(pw_buf, pw_buf2, PW_LEN)){
printf("Password OK\n");
system("/bin/cat flag\n");
}
else{
printf("Wrong Password\n");
}
close(fd);
return 0;
}
读取password文件;然后读取用户输入,将10个字符最后一个比特与1异或后再与password比较,如果相符就输出flag.
从代码也能看出,执行程序后会先休眠一段时间,然后读取用户的输入。然而在我执行程序的过程中,发现,即使在等待相当长的时间后,也只有按下回车才会再出现“input password”的提示,起初我并没有在意这一的细节。
刚开始我甚至考虑过能否利用溢出,但scanf("%10s")似乎只会读取10个字节,而且题目的提示说不用什么技巧:
We all make mistakes, let's move on.
(don't take this too seriously, no fancy hacking skill is required at all)
This task is based on real event
Thanks to dhmonkey
hint : operator priority
提示说注意运算符优先级,到底在哪里能用到?反复看了好几遍代码,才猛然发现:
if(fd=open("/home/mistake/password",O_RDONLY,0400) < 0)
/*
...
*/
if(!(len=read(fd,pw_buf,PW_LEN) > 0))
这两句话的优先级存在问题!fd和open的值分别是后面那个表达式的值,突然一个大胆想法进入我的脑海——有没有一种可能,让open()<0为假,这样fd=0,接着read不就能不从password文件中输入,而是转而由stdin来输入呢?这不就绕开了检查,实现了由我们控制吗?
事实上,open打开成功,返回的值是文件描述符,显然是正数,这导致fd永远是0,也就是说,刚才的“反常现象”其实是伏笔!程序一直在等待我们输入!只是因为没有任何提示,所以非常隐晦,而且我每次都是输入的回车就匆匆过去了......
到这里就很清晰了,只需要分别输入BBBBBBBBBB和CCCCCCCCCC即可得到flag.
这道题相当有意思,如果没有提示,我感觉我还真发现不了...(T_T)
lotto
登录主机后,让我们完成一个猜数游戏,代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
unsigned char submit[6];
void play(){
int i;
printf("Submit your 6 lotto bytes : ");
fflush(stdout);
int r;
r = read(0, submit, 6);
printf("Lotto Start!\n");
//sleep(1);
// generate lotto numbers
int fd = open("/dev/urandom", O_RDONLY);
if(fd==-1){
printf("error. tell admin\n");
exit(-1);
}
unsigned char lotto[6];
if(read(fd, lotto, 6) != 6){
printf("error2. tell admin\n");
exit(-1);
}
for(i=0; i<6; i++){
lotto[i] = (lotto[i] % 45) + 1; // 1 ~ 45
}
close(fd);
// calculate lotto score
int match = 0, j = 0;
for(i=0; i<6; i++){
for(j=0; j<6; j++){
if(lotto[i] == submit[j]){
match++;
}
}
}
// win!
if(match == 6){
system("/bin/cat flag");
}
else{
printf("bad luck...\n");
}
}
void help(){
printf("- nLotto Rule -\n");
printf("nlotto is consisted with 6 random natural numbers less than 46\n");
printf("your goal is to match lotto numbers as many as you can\n");
printf("if you win lottery for *1st place*, you will get reward\n");
printf("for more details, follow the link below\n");
printf("http://www.nlotto.co.kr/counsel.do?method=playerGuide#buying_guide01\n\n");
printf("mathematical chance to win this game is known to be 1/8145060.\n");
}
int main(int argc, char* argv[]){
// menu
unsigned int menu;
while(1){
printf("- Select Menu -\n");
printf("1. Play Lotto\n");
printf("2. Help\n");
printf("3. Exit\n");
scanf("%d", &menu);
switch(menu){
case 1:
play();
break;
case 2:
help();
break;
case 3:
printf("bye\n");
return 0;
default:
printf("invalid menu\n");
break;
}
}
return 0;
}
可以发现随机数的产生使用了/dev/urandom文件,查阅资料可知/dev/random和/dev/urandom都是系统的随机数产生文件,能够捕获系统中的各种随机事件,来增强随机化.
我们的目标是达到6个匹配,初次看可能感觉很难,但是我们发现判断条件有点端倪:
for(i=0; i<6; i++){
for(j=0; j<6; j++){
if(lotto[i] == submit[j]){
match++;
}
}
}
竟然是一个二重循环?所以说,如果我们六个字节都填一个数字,只需要保证随机数字中的一个与之匹配就行了,这样看来概率也挺大的.直接手动输入,,,,,,多试几次就过了.没什么难度
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。