• 在做题之前先要查看二进制文件,看看是怎么样的类型,使用checksec工具

https://blog.csdn.net/qq_43430261/article/details/105516051

checksec

pwndbg动态调试

  • pwndbg的启动直接在终端输入gdb即可

image-20240318163944545

  • 可以注释掉pwndbg里面的折叠代码(这里没发现区别,之后在搞),动调的时候会更清晰
1
2
# 进入pwndbg里面的编辑文档
vim ~/pwndbg/pwndbg/commands/telescope.py

术语解释

1
2
3
4
5
6
7
运行
步入,步过,步出,步止
断点(设置,删除,显示)
查看内存、寄存器、各种参数
设置内存、寄存器、各种参数(加载文件)
远程调试
其他辅助功能
  • 步入、步过、步出、步止

    • 步入:

      当程序执行到某个函数调用时,调试器不仅执行该调用,而且会进入到被调用的函数内部,从函数的第一行代码开始逐行执行

      单步步入指令:s

    • 步过:

      逐行执行代码,但当遇到函数调用时不会进入该函数内部,而是直接执行该函数调用,并将控制权返回给调用该函数的代码行之后的下一行

    • 步出:

      在已经步入一个函数之后,快速执行完该函数内剩余的代码,并将控制权返回到调用该函数的地方。

    • 步止:

      停止执行代码

  • 单步跳过(指令:n):执行当前行的代码,如果当前行是一个函数调用,则不会进入该函数内部,而是直接执行该函数调用并返回到调用后的下一行代码。

  • 断点:断点是一个标记,指示调试器在程序执行到该位置时暂停执行

安装pwndbg

  • 这里不再叙述

常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
run		# 将程序完全跑一遍
start # 运行到类似于入口点的地方
i r # 查看寄存器
i b # 查看断点
disassemble $rip # 反编译rip所在函数的位置,查看rip在什么地方
b *0x000055555555527a # 设置断点,这里b即位breakpoint
ni # 单步运行程序
c # 到下一个断点
d 2 # 删除断点
disable b 2 # 使断点失效
enable b 3 # 使断点重新生效
si # 步入
finish # 步出
x # 查看
set # 改变值
p/print # 打印值
vmmap # 查看内存基本情况
cyclic 100 #打出100长度的字符串
cyclic -l kaaaaaaa #查找字符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
x/20g rbp
x/数字显示格式 地址
x查看内存
20查看行的内存
g以16进制显示
rbp查看内存的开始是rbp的所指的地址
x/20i 以汇编显示
x/20g 以16进制,八个字节显示
x/20b 以16进制,单字节显示
x/20w 以16进制,4字节显示
x/20d 以10进制,4字节显示
x/20s 以字符串形式显示

set *0x7fffffffe550=0x61
set *((unsigned int)$ebp)=0x61
// 利用解引用符号,修改地址里面的值


基本界面

image-20240318165032816

  • registers(寄存器)
  • code(代码)
  • stack(栈)
  • backtrace(回溯)

registers

  • 最左边绿色的都是寄存器

image-20240318172843820

code

  • 左边0x55555555527d指的是内存地址
  • 最左边有个箭头表示程序运行到该位置处
  • endbr64下方的是汇编代码

image-20240318172955016

stack

  • 最左边的是栈的相对地址
  • 中间蓝色的是栈的实际地址
  • —>箭头指向的是栈中储存的值

image-20240318173142253

  • 如果想看更多栈的情况可以输入

stack 40

  • 由于下图调试的程序调用的栈有限,所以只显示了25行的栈

image-20240318174006730

backtrace

  • 显示的函数调用情况(从下往上倒着看)
    • main函数调用了gets函数
    • gets函数调用了上面3、2、1行的内核函数

image-20240318173658258

实际操作

  • 会将常用命令都用上一遍
  • 编译指令gcc question_1.c -o question_1_x64,x64编译
  • 调试程序代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char sh[]="/bin/sh";
int init_func(){
setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);
return 0;
}

int func(char *cmd){
system(cmd);
return 0;
}

int main(){
char a[8] = {};
char b[8] = {};
//char a[1] = {'b'};
puts("input:");
gets(a);
printf(a);
if(b[0]=='a'){
func(sh);
}
return 0;
}

运行程序

  • run命令:完整的将程序运行一遍

  • 看白色框里面的

    image-20240318190533181

  • start命令:start命令是一个特殊命令,start命令会先在main函数的入口处设置一个断点,当程序运行到main函数的入口处,就会停止,等待调试者的进一步操作

  • 有些时候去掉符号表,gdb找不到main函数入口点,需要调试者自己去找入口点并设置断点

image-20240318190825168

对寄存器的操作

  • i r指令

  • 核心是rip

1
2
3
4
5
rbp rsp # 与栈有关,保护栈
rax # 储存程序的返回值,return的数据都储存在rax
rip # 存放当前执行的指令地址
rsp # 存放当前栈帧的栈顶地址
rbp # 存放当前栈帧的栈底
  • 使用i r指令查看寄存器

image-20240318195159167

  • 反汇编rip寄存器指向的函数(默认反汇编是AT&T格式)
  • 要用intel汇编格式要输入set disassembly-flavor intel

image-20240318195401586

断点的设置与使用

断点的设置、查看、删除、失效

  • 设置断点b *0x5555555552819b + 指令地址

image-20240318200634942

  • 查看断点i b

image-20240318200731012

  • 使断点失效disable b 2
  • 使断点失效后断点仍然存在,但是在实际调试的过程中不会起到断点的作用
  • 建议之后在调试的过程中,对不需要的断点使用该操作,这样在之后需要使用该断点调试的时候就不用花时间找断点了

image-20240318201133541

  • 删除断点 d 2 (这里的2指的是上图中断点的Num)

image-20240318201335502

断点的作用

  • c命令执行到断点
  • 然后反汇编

image-20240318202705520

  • 查看此时寄存器的值,此时还没有执行mov rbp,rsp指令

image-20240318204519709

  • 输入ni指令(执行单步)后再查看寄存器,此时完成了mov rpb,rsp

image-20240318204723075

ni与si的区别

  • ni指令之后,会直接跳过input函数的内部执行过程,直接执行箭头会直接指向main函数的下跳指令
  • 下图的input就会被打印出来

image-20240318211504372

  • si指令则会进入到input的实现的过程中里面去,先是进入到input的plt表中

image-20240318212137348

  • 此时再对rip寄存器指向的地址处进行反汇编,会发现反汇编的地址不是在main函数中

image-20240318212300428

步出

  • finish

image-20240318212458622

  • 步出之后再进行反汇编,反汇编的指令就会在main函数中

image-20240318212542650

调试漏洞

  • 介绍指令x指令查看内存、set指令修改内存值、p指令显示值

  • 将该位置设置为断点,稍微在前面一两个

image-20240319115740670

  • 然后使用start指令开始进行调试,c指令走到断点
  • 反汇编查看汇编代码

image-20240319115927756

  • 发现这一步cmp al,0x61,这正好对应着代码

image-20240319120051012

  • 如果满足b[0]=’a’那么程序就会进入shell里面,这时就获取了程序控制权了
  • 接下来查看寄存器,发现rax的值为0,那么ax值肯定也是0,如果要跳转那么就要让ax的值为61,就会满足跳转条件
  • 再查看cmp语句的上一句:
1
2
3
movzx  eax,BYTE PTR [rbp-0x10]
意识是按字节赋值,PTR是指针的意思
将rpb-0x10这个地址里面的值中的第一个字节赋值给eax
  • 得到再执行cmp指令之前,eax会先被赋值,

image-20240319120301052

这里先介绍两个指令

1
2
3
4
5
6
7
8
9
10
11
12
x/20g rbp
x/数字显示格式 地址
x查看内存
20查看行的内存
g以16进制显示
rbp查看内存的开始是rbp的所指的地址
x/20i 以汇编显示
x/20g 以16进制,八个字节显示
x/20b 以16进制,单字节显示
x/20w 以16进制,4字节显示
x/20d 以10进制,4字节显示
x/20s 以字符串形式显示
  • x指令与disassemble指令的区别

image-20240319122236219

  • x指令是从rip所指的位置开始反汇编,而disassemble指令是直接反汇编rip所在的函数的全部内容

image-20240319122131825

  • 继续调试

  • 先用p指令查看rbp和rbp-0x10的值

  • 再用x指令查看rbp-0x10内存(字节形式查看)

image-20240319122644873

image-20240319122918480

  • 所以movzx eax,BYTE PTR [rbp-0x10]就是把上图文件中白色框rbp-0x10所指的值第一个字节给eax,也就是把0赋值给了eax

  • 利用set改变rbp-0x10里面的值,使0x61赋值给eax

1
2
3
set *0x7fffffffe550=0x61
// 利用解引用符号,修改地址里面的值

image-20240319123320309

  • 修改完值后通过几次ni指令即可执行到下图箭头指向处

image-20240319124354426

  • 在箭头指向该位置时,si进入func里面去

image-20240319124522401

image-20240319124540718

  • 这样就可以取到shell了

image-20240319125421307

在程序输入点操作

  • Linux采用小端序,在一个字节中,低位低地址,高位高地址
1
2
3
4
5
0x7fffffffe550:	0x0000006161616161	0x8526886159646000
0x7fffffffe560: 0x0000000000000001 0x00007ffff7c29d90
8字节 ↑ 8字节

0x7fffffffe560
  • get是一个不安全的函数

image-20240319125606862

  • 当输入a输入进去,只要覆盖到百色框里面的数值,就可以完成栈溢出操作,使得程序被跳转进去

image-20240319131146834

image-20240319131245754

调试x86程序

1
2
gcc -m32 question_1.c -fno-omit-frame-pointer -o question_1_x86_esp
gcc question_1.c -fno-omit-frame-pointer -o question_1_x64_esp
  • 在调试的时候可能会出现如下情况

image-20240319215717164

使用该命令即可sudo chmod +x /home/myheart/pwn_learn/character1/test_3/question_1_x86_esp

  • 程序源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char sh[]="/bin/sh";
int init_func(){
setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);
return 0;
}

int func(char *cmd){
system(cmd);
return 0;
}

int main(){
char a[8] = {};
char b[8] = {};
//char a[1] = {'b'};
puts("input:");
gets(a);
printf(a);
if(b[0]=='a'){
func(sh);
}
return 0;
}
  • 使用gcc编译32位程序出现错误
  • 使用如下指令
1
sudo apt-get install gcc-multilib g++-multilib module-assistant

image-20240319204804494

ROP

安装工具

ROPgadget

参考博客:

ROPgadget 安装 错误处理 与使用_ropgadget安装报错-CSDN博客

  • 更新一下软件源
1
2
sudo add-apt-repository ppa:launchpad-net-ubuntu-capstone-developers/capstone/ubuntu
sudo apt-get update
  • 下载python-capstone
1
sudo apt-get install python-capstone
  • 拉取ROPgadget
1
git clone https://github.com/JonathanSalwan/ROPgadget.git
  • 下载好ROPgadget解压,并进入文件夹中,进行安装
1
2
cd ROPgadget/
sudo python3 setup.py install

one_gadget

参考博客:one_gadget 下载 安装 与使用_obe_gadget-CSDN博客

  • one_gadget就是用来去查找动态链接库里execve(“/bin/sh”, rsp+0x70, environ)函数的地址的

  • 安装

1
2
sudo apt -y install ruby
sudo gem install one_gadget
  • sudo gem install one_gadget出现问题

参考博客:ERROR: Could not find a valid gem ‘bundler’ (>= 0)解决方法_error: could not find a valid gem ‘bundler’ (>= 0)-CSDN博客

  • 查看源
1
gem sources -l

image-20240324120527864

  • 换源,并删除原有的源
1
2
gem sources --add https://gems.ruby-china.com/ --remove https://rubygems.org/

image-20240324120720462

  • 继续安装,如果仍然失败可能是网络问题
1
sudo gem install one_gadget

ROPPER

参考博客:Ubuntu Ropper keystone-engine安装 - 简书 (jianshu.com)

  • 安装Keystone-engine
1
sudo pip3 install keystone-engine
  • 安装ROPPER
1
2
3
4
5
6
7
8
9
$ git clone https://github.com/keystone-engine/keystone.git
$ cd keystone
$ mkdir build
$ cd build
$ ../make-share.sh
$ sudo make install
$ sudo ldconfig
$ cd ../bindings/python
$ sudo make install3 # or sudo make install for python2-bindings
  • 在第5步遇到问题,原因:没有安装cmake,退回根目录安装cmake
1
2
3
4
sudo apt-get update  
sudo apt-get install cmake
# 安装好后检查是否安装成功
cmake --version
  • 安装好Cmake后重新回到第5步
  • 在第五步又出现问题
    • CMake弃用警告
    • C编译器未找到

第一个问题:

1
2
修改在keystone\samples目录下的CMakeLists.txt文件
cmake_minimum_required(VERSION 3.10)

第二个问题:

ROP介绍

  • ROP(Return-oriented Programming),面向返回编程。
  • 其核心在于利用ret指令,通过自己伪造函数调用栈,执行原有程序中正常代码的特定部分(gadget),从而控制数据域程序执行流程

  • ROP就是利用栈溢出,将栈高位原来的返回地址覆盖成其他地址,然程序跳转到被修改的地址,从而完成对漏洞的利用

  • gadget,是一段可以用与构建ROP链的汇编代码。

  • gadget通常指一对pop、ret指令其功能可以配置一个寄存器的值,并返回至指定地址

例如:

RET指令

  • ret指令就是return指令,这是ROP中的核心
  • ret指令本质等价于pop ip寄存器,也就是把栈顶的值给ip寄存器。
  • 在ROP中,ret指令完成了call指令一样的工作,并且没有对我们的栈空间数据进行修改
  • ret指令从寄存器rsp中寻找返回地址,然后将寻找到的地址赋值给rip,rip就可以执行接下来的语句

例如:

1
2
3
mov rax,1
mov rbx,2
ret
  • 执行完mov rbx,2,栈指针rsp恰好指向0xFFFFFF这个位置中

image-20240317191025475

  • 那么执行ret后,就会把0xFFFFFF赋值给rip,也就让rip指向地址0xFFFFFF,接下来程序就执行rip指向地址的指令

RET指令在ROP中的应用

  • 假设要控制rcx寄存器的值,并且我们发现程序中一段指令
1
2
3
4
5
6
pop rax
pop rbx
mov rcx,rbx
ret

# 这里pop rax是弹栈的意思,将rsp所指向的地址里面的值,赋值给寄存器rax
  • 如果能通过栈溢出,让程序首先跳转到pop rbx上,那么就可以使得rcx寄存器的值被控制,然后再通过ret指令,继续跳转到接下来要执行的位置。
  • 同理我们可以通过一个又一个像这样的以ret结尾的片段,从而完成ROP链式攻击

函数调用规则

_cdecl调用规则

  • C/C++默认方式,参数从右向左入栈,主调函数负责栈平衡

32位程序调用规则

  • 32位以栈空间作为参数存储空间进行传参
  • C语言cdecl调用规则要求参数从右至左压入栈

以int func(char argv1,int argv2, int argv3)为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdio.h>

int func(argv1,argv2,argv3);

int main(void)
{
func(argv1,argv2,argv3);

return 0;
}

int func(argv1,argv2,argv3)
{
int a = 1;
return 1;
}
  • 该程序汇编大致如下(纯手搓可能有错)
1
2
3
4
5
6
7
8
9
10
push rbp
mov rbp, rsp
push argv3
push argv2
push argv1
push rbp
mov rbp,rsp
# 补充
call : push rip; jump func;
leave : mov rsp,rbp; pop rbp;
  • 下图为大致栈的动作(此处遗漏了sub指令)

image-20240317195205787

image-20240317195216595

  • 实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

//This is a x32 demo
//gcc -m32 -fno-stack-protector -no-pie -o ROP_2 ROP_expriment2.c

int vulnerable_func();
int test(int argv1,int argv2, int argv3);
int main()
{
test(0x1111,0x2222,0x3333);
vulnerable_func();
return 0;
}

int vulnerable_func()
{
//stack over flow give chance to make ROP chain
char str[128];
puts("Hello there, leave me some message plz.");
return read(0,str,0x128);
}

int test(int argv1, int argv2, int argv3)
{
printf("I'm a test func,here it's your argvs:\nargv1:0x%x \nargv2:0x%x \nargv3:0x%x\n",argv1,argv2,argv3);
return 0;
}

EXP:

image-20240317195507849

64位程序调用规则

  • 64位程序调用规则依旧按照C语言cdecl调用规则,要求参数依旧从右至左。
  • 只不过64位优先以寄存器为参数存储空间,如参数超过7个,则超过部分使用栈空间进行传参(速度更快)
  • 64位函数调用,继续以int func(char argv1,int argv2, int argv3)*为例
1
2
3
4
mov rdx,0x128    argv3
mov rsi,rax argv2
mov rdi,0 argv1
call func1
  • 优先按照以下顺序使用rdi,rsi,rdx,rcx,r8,r9寄存器用来传参数

例如:

1
2
3
4
5
6
传递1个参数a那么就只使用rdi寄存器
mov rdi,1 a

传递2个参数a,b就使用rdi、rsi寄存器
mov rsi 2 b
mov rdi 1 a
  • 实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

//This is a x32 demo
//gcc -m32 -fno-stack-protector -no-pie -o ROP_2 ROP_expriment2.c

int vulnerable_func();
int test(int argv1,int argv2, int argv3);
int main()
{
test(0x1111,0x2222,0x3333);
vulnerable_func();
return 0;
}

int vulnerable_func()
{
//stack over flow give chance to make ROP chain
char str[128];
puts("Hello there, leave me some message plz.");
return read(0,str,0x128);
}

int test(int argv1, int argv2, int argv3)
{
printf("I'm a test func,here it's your argvs:\nargv1:0x%x \nargv2:0x%x \nargv3:0x%x\n",argv1,argv2,argv3);
return 0;
}

_stdcall

  • windows API默认方式,参数从右向左入栈,被调函数负责栈平衡

_fastcall

  • 快速调用方式。快速,即这种方式选择将参数优先从寄存器传入(ECX和EDX),剩下的参数再从右向左从栈传入。因为栈是位于内存区域,而寄存器位于CPU内,故存取方式快于内存

内存对齐

ELF文件保护机制

Canary保护

介绍

  • canary意思是金丝雀。来源于过去利用金丝雀查看矿洞的安全程度。
  • canary是一种用来防护栈溢出的保护机制,其原理是在一个函数入口处,先从fs/gs寄存器中取出一个四字节(eax)或者8字节(rax)的值存到栈上,当函数结束时会检查这个栈上的值是否和存进去的值一致。
  • 而且canary的值都是以00结尾,为了阶段printf泄露栈上的值或地址

  • gcc编译指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
gcc canary_test.c -o canary_off -fno-stack-protector
gcc canary_test.c -o canary_on -fstack-protector
gcc canary_test.c -o canary_all -fstack-protector-all

// 示例代码
#include<stdio.h>
void func()
{
int a;
scanf("%d",&a);
}
int main(void)
{
func();
return 0;
}

64位canary

  • 将示例代码gcc编译后,使用IDA反汇编可以得到如下汇编代码
  • 该汇编代码显示,fs的值会先传入rax中,再将rax里面的值压入栈中
  • 在程序执行ret指令之前会将栈中canary的值传入rdx中,将rdx与fs里面的值比较。如果两者值相同,则没有发生栈溢出;反之则发生栈溢出了。
  • 在检查到发生栈溢出后,就会call 到 stack_chk_fail函数中去终止程序+

image-20240424184912727

image-20240424185303997

  • 在gdb动调里面查看canary保护
  • 在rbp之前会出现一个值,这个就是canary值。

image-20240424185522428

image-20240424185445068

32位canary

  • gcc编译后使用IDA反汇编得到的结果

image-20240424191035214

  • 跳转到的stack_chk_fail_local

image-20240424191051366

  • gdb动态调试查看canary保护

image-20240424191334132

image-20240424191532788

绕过canary保护手法

泄露栈中的canary

  • 泄露栈中的canary在栈溢出的时候进行,canary的原样输入即可绕过canary

逐位爆破金丝雀

SSP泄露

劫持_stack_chk_fail函数

PIE保护

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

  • PIE全称是(position-independent executable),中文解释为地址无关可执行文件
  • 该技术是针对代码段(.text)、数据段(.data)、未初始化全局变量段(.bss)等固定的一个防护技术
  • 在每次加载程序时都变换加载地址,从而不能通过ROPgadget等工具来帮助解题

PIE没开

  • 在PIE没有打开的时候,可执行程序的固定地址为0x400000
  • 也就是说改ELF文件的起始地址为0x400000

image-20240424195046126

image-20240424194943838

  • 开启gdb后的程序地址

image-20240424195600326

PIE开启

  • 在开启PIE保护后,反汇编后的地址就不是0x4000000开头
  • 而是从0x1000开始,而每次开启程序时,程序的地址都是随机的

image-20240424195409131

  • gdb启动程序后的地址

·

PIE绕过

partial write

泄露地址

vdso/vsyscall

RELRO保护

NX保护

作业

  • 尝试使用Docker部署一题pwn题目,题目可以复制别人的,重要的是部署的过程

https://iyheart.github.io/2024/03/18/PWN%E7%B3%BB%E5%88%97blog/pwn%E9%A2%98%E7%9B%AE%E9%83%A8%E7%BD%B2/

+