pwnable-Col

  • 2019 年 10 月 10 日
  • 笔记

文章来自渗透云笔记作者团&掣雷小组内部成员;Johnny

还是一样打开网站,链接服务器(此部分省略)

查看服务器里有什么?

查看源代码

涉及到的知识点:

1.指针类型转换 2.大小端序 3.字符串转换ASCII码

知识点1:指针类型转换

当我们初始化一个指针或给一个指针赋值时,赋值号的左边是一个指针,赋值号的右边是一个指针表达式。在我们前面所举的例子中,绝大多数情况下,指针的类型和指针表达式的类型是一样的,指针所指向的类型和指针表达式所指向的类型是一样的。

float f=12.3;  float *fptr=&f;  int *p;

在上面的例子中,假如我们想让指针p 指向实数f,应该怎么办?

是用下面的语句吗? p=&f;

不对。因为指针p 的类型是int *,它指向的类型是int。表达式&f 的结果是一个指针,指针的类型是float *,它指向的类型是float。

两者不一致,直接赋值的方法是不行的。至少在我的MSVC++6.0 上,对指针的赋值语句要求赋值号两边的类型一致,所指向的类型也一致,其它的编译器上我没试过,大家可以试试。为了实现我们的目的,需要进行”强制类型转换”: p=(int*)&f;

如果有一个指针p,我们需要把它的类型和所指向的类型改为TYEP *TYPE, 那么语法格式是:(TYPE *)p; 这样强制类型转换的结果是一个新指针,该新指针的类型是TYPE *,它指向的类型是TYPE,它指向的地址就是原指针指向的地址。 而原来的指针p 的一切属性都没有被修改。(切记) 一个函数如果使用了指针1作为形参,那么在函数调用语句的实参和形参的结合过程中,必须保证类型一致,否则需要强制转换!

再来看一段代码:

void fun(char*);      int a=125,b;      fun((char*)&a);      void fun(char*s)      {          charc;          c=*(s+3);*(s+3)=*(s+0);*(s+0)=c;          c=*(s+2);*(s+2)=*(s+1);*(s+1)=c;      }

注意这是一个32 位程序,故int 类型占了四个字节,char 类型占一个字节。函数fun 的作用是把一个整数的四个字节的顺序来个颠倒。注意到了吗?在函数调用语句中,实参&a 的结果是一个指针,它的类型是int *,它指向的类型是int。形参这个指针的类型是char *,它指向的类型是char。这样,在实参和形参的结合过程中,我们必须进行一次从int *类型到char *类型的转换。

结合这个例子,我们可以这样想:

想象编译器进行转换的过程:编译器先构造一个临时指针char *temp,然后执行temp=(char *)&a,最后再把temp 的值传递给s。所以最后的结果是:s 的类型是char *,它指向的类型是char,它指向的地址就是a 的首地址。 我们已经知道,指针的值就是指针指向的地址,在32 位程序中,指针的值其实是一个32 位整数。

知识点2:大小端序

我一直不理解,为什么要有字节序,每次读写都要区分,多麻烦!统一使用大端字节序,不是更方便吗?

  • 上周,我读到了一篇文章,解答了所有的疑问。而且,我发现原来的理解是错的,字节序其实很简单。

首先,为什么会有小端字节序?

答案是,计算机电路先处理低位字节,效率比较高,因为计算都是从低位开始的。所以,计算机的内部处理都是小端字节序。 但是,人类还是习惯读写大端字节序。所以,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。

计算机处理字节序的时候,不知道什么是高位字节,什么是低位字节。它只知道按顺序读取字节,先读第一个字节,再读第二个字节。 如果是大端字节序,先读到的就是高位字节,后读到的就是低位字节。小端字节序正好相反。 理解这一点,才能理解计算机如何处理字节序。

字节序的处理,就是一句话:

*”只有读取的时候,才必须区分字节序,其他情况都不用考虑。”*

处理器读取外部数据的时候,必须知道数据的字节序,将其转成正确的值。然后,就正常使用这个值,完全不用再考虑字节序。 即使是向外部设备写入数据,也不用考虑字节序,正常写入一个值即可。外部设备会自己处理字节序的问题。

举一个例子,比如数字0x12 34 56 78在内存中的表示形式为:

1)大端模式: 低地址 —————–> 高地址 0x12 | 0x34 | 0x56 | 0x78

2)小端模式: 低地址 ——————> 高地址 0x78 | 0x56 | 0x34 | 0x12

可见,大端模式和字符串的存储模式类似。

知识点3:字符串转换ASCII码

我们输入进的是字符串,内存里存放的是以ASCII码值来储存的

开始做题:

回顾代码:

#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 bytesn");  		return 0;  	}    	if(hashcode == check_password( argv[1] )){  		system("/bin/cat flag");  		return 0;  	}  	else  		printf("wrong passcode.n");  	return 0;  }

源代码简要说明:

check_password函数的大致意思是定义一个不可变的char类型指针p(形参),将char类型的p指针强制转换成int类型指针,让int类型的ip指针指向转换后的int类型p指针,然后定义一个int类型i变量,初始化int类型res变量值为0,for循环遍历res=res+ip[i]5次,返回res的值

main函数的大致意思是让你输入一串check_password()函数里返回的字符串,如果与之前定义的hashcode相等,呢么输出flag

解决思路:

  • 所以你现在知道你要输入什么进去了,但是你不能直接输入字符串进去,得把要输入的字符串对应的ASCII码值输入进去,为什么?看之前的知识点!

0x21DD09EC是我们要输入的答案

  • 呢么哪五个数相加等于0x21DD09EC呢,我们来做个计算:
0x21DD09EC-4 //这里减4是为了凑整,减其他的也行,就是必须是整数  得到:0x21DD09E8  0x21DD09E8除以5 //因为这里循环5组  ‭得到:0x06C5CEC8‬  0x06C5CEC8‬ *4加上0x06C5CEC8+4  得0x21DD09EC

开始构造payload:

执行代码 :  ./col $(python -c "print 'xC8xCExC5x06' * 4 + 'xCCxCExC5x06'")

注意:必须是小端格式 ‘xC8xCExC5x06’ * 4 加上 xCCxCExC5x06 刚好是 0x21DD09EC,所以利用成功。

成功!

参考链接

C语言指针详解(经典,非常详细) 详解大端模式和小端模式) PWN-Col教学