pwnable学习心得
Posted -twinkle-star
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了pwnable学习心得相关的知识,希望对你有一定的参考价值。
前言:pwnable.kr这个学习网站是我学习pwn过程中主要肝的一个网站,因为要考研等种种原因没有办法继续肝题,写这个系列是希望对自己已经会了题进行一个总结,以及以我这种萌新的角度对题目的理解希望能帮助到各位初学者,有说的不对的地方还请多指教。
我理解的pwn是寻找服务器程序的漏洞,利用漏洞达到控制服务器的目的,在题目中体现为获取flag,因此我们需要做的事情有两件:
1.找漏洞——通过已知的源代码,或是将可执行文件通过逆向工具将代码还原成汇编语言或者大致的伪代码。这个漏洞的触发大多是因为攻击者构造了特殊的输入使程序崩溃,或者产生管理员们不希望或者意料之外的事情(比如让我们获得了更高的权限),研究这种“意外”发生的原理就是我们的任务。
2.找到漏洞后——我们需要输入,那么如何输入呢,如果只是简单的输入,那么手动构造输入就行,但如果构造的语句太长,或者我们需要在服务器给我们不同的反馈的时候给予不同的输入,那么就需要用到网络编程的知识,这也是为什么大佬解题总是放一个脚本的原因。这个脚本就是用编程语言(java或者python)的socket去连接服务器,连接上后根据服务器回传的数据判断需要传送给服务器的构造输入
0x01 fd
#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 "); return 0; } //atoi()函数将字符串转化成数字 int fd = atoi( argv[1] ) - 0x1234; int len = 0; //read()函数从fd作为句柄所打开的文件里读取32字节放到buf缓冲区里 len = read(fd, buf, 32); if(!strcmp("LETMEWIN ", buf)){ printf("good job :) "); system("/bin/cat flag"); exit(0); } printf("learn about Linux file IO "); return 0; }
首先,命令行参数必须>=2,程序名本身作为第一个参数,之后的第二个参数通过atoi()函数将它从字符串转化为整型变量后减去0x1234(4660),结果作为文件句柄,打开此文件读取内容到缓冲区,判断是否为"LETMEWIN"。当你输入./fd 4660时,read函数会从对应的流里读取数据。网上大多数答案说的./fd 4660其实./fd 4661 ./fd 4662都可以,因为句柄0,1,2分别代表标准输入(键盘)、标准输出(屏幕)、标准错误输出(屏幕)。其实标准错误输出stderr也是指向屏幕的。
0x 02 collision
#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; //转化成整型指针后,指针指向的就是一个4字节的整体,也就是说20个字节的空间只含有5个整型数 for(i=0; i<5; i++){//将5个整型数字相加得到最终的返回值 res += ip[i]; } return res; } int main(int argc, char* argv[]){ if(argc<2){//参数个数大于等于2 printf("usage : %s [passcode] ", argv[0]); return 0; } if(strlen(argv[1]) != 20){//第个参数作为字符串来说必须有20个字节 printf("passcode length should be 20 bytes "); return 0; } //第二个参数经过checck_password函数处理后的返回值要等于hashcode的值 if(hashcode == check_password( argv[1] )){ system("/bin/cat flag"); return 0; } else printf("wrong passcode. "); return 0; }
也就是说我们需要传入一个20字节的字符串作为参数最终使得它经过check_password函数的解析后能通过验证,这里给出我的分析图:
所以我们构造的参数可以是0x02020202*4+0x19d501e4或者0x01010101*4+0x1dd905e8,可以写python脚本用网络编程的方式传递参数,但目标环境里安装了python我们直接在服务器的环境里使用python即可
只要你构造的数加起来满足条件即可。
0x 03 bof
#include <stdio.h> #include <string.h> #include <stdlib.h> void func(int key){ char overflowme[32]; printf("overflow me : "); //gets()函数是一个典型的容易造成溢出漏洞的函数 gets(overflowme); // smash me! if(key == 0xcafebabe){ system("/bin/sh"); } else{ printf("Nah.. "); } } int main(int argc, char* argv[]){ func(0xdeadbeef); return 0; }
栈:我们从比较底层的角度来理解,当我们依次执行某些语句时,遇到需要暂时保存当前环境里一些数据,体现为寄存器、或内存变量的值,过一会再使用,但寄存器的数量是有限的,这时我们划出一片内存空间,用特殊的进出数据的规则来约束它,让它具有保存数据和恢复现场的能力。
函数栈帧:同样我们在执行一个函数体的时候也会使用到各种寄存器,这些寄存器数量有限,所以程序在每次进入一个函数体去执行一个函数之前,需要将一些数据压栈以保存现场,然后按照程序执行函数语句,函数返回后需要将栈上一些内容弹出给对应的寄存器达到恢复现场的作用。
需要压栈的参数以及顺序为:函数的所有参数(按照调用约定,一般是从右到左)、之前函数栈帧的旧ebp的值。
压栈完成后,底层会为函数分配一片空间——函数栈帧,栈帧的大小和这个函数定义了多少变量有关,按照从高地址到低地址依次为函数中定义的变量分配空间,具体如图所示
overflowme这个数组在栈上(注意数组的分布形式),并且key这个参数在它的相对高地址的地方,而数组在填满空间的时候肯定是从低地址往高地址填充,那么假如出现某种情况能让我不停的往数组里填充,那么它肯定会有覆盖key的机会,而gets()函数就是关键,通过它输入数组的时候它不会进行边界检查,所以就算你输超了也没关系。根据题目我们需要将key覆盖成0xcafebabe才行,有一点需要说明一下,图画的不太准确,其中的每个数组单元是一个字节,而指针、整型、地址等每个单元是4个字节,但是顺序是正确的,具体的字节差值或者说填充数我们用linux下的gdb来分析和计算
限定为overflow的首地址然后读取用户输入,这里的ebp-0x2c就是数组的首地址,看到他们相差了0x2c也就是44个字节,所以我们输入44个字节后再输入4(覆盖ebp)+4(覆盖返回地址)+xbexbaxfexca即可覆盖key。(写脚本主要用到python下pwn库和zio使用方法多看看别人的代码找照猫画虎即可)
以上是关于pwnable学习心得的主要内容,如果未能解决你的问题,请参考以下文章