1

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++;
                        }
                }
        }

竟然是一个二重循环?所以说,如果我们六个字节都填一个数字,只需要保证随机数字中的一个与之匹配就行了,这样看来概率也挺大的.直接手动输入,,,,,,多试几次就过了.没什么难度


StupidMagpie
6 声望0 粉丝