零碎的知识

函数相关

64系统调用传参

rax:系统调用号

rdi->rsi->rdx->rcx->r8->r9

函数调用约定

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

stdcall: windows API默认方式,参数从右向左入栈,被调函数负责栈平衡。
fastcall:快速调用方式。所谓快速,这种方式选择将参数优先从寄存器传入(ECX和EDX),剩下的参数再从右向左从栈传入。因为栈是位于内存的区域,而寄存器位于CPU内,故存取方式快于内存,故其名曰“fastcall”。

函数返回参数传递

32位

字节 返回参数
小于4字节 eax
4-8字节 eax(低),ebx(高)
大于8字节 eax指针

函数使用

alarm()

unsigned int alarm(unsigned int seconds);

参数

  • unsigned int seconds:指定在多少秒后发送 SIGALRM 信号。如果参数为 0,则取消任何先前设置的闹钟。

返回值

  • 成功时返回先前设置的闹钟剩余的时间(以秒为单位)。
  • 如果没有先前设置的闹钟,返回 0。

关于for循环

__int64 sub_B24()
{
  int i; // [rsp+0h] [rbp-4h]

  for ( i = 0; i <= 19; ++i )
  {
    if ( !*((_QWORD *)off_202010 + i) )
      return (unsigned int)i;
  }
  return 0xFFFFFFFFLL;
}

这里((_QWORD *)off_202010 + i)把off_202010强制转化成指向 64 位整型数组的指针,所以这里+i是按照QWORD(八个字节)来移动的。也就是说每次加1实际上是移动8字节

危险函数特点

get()在遇到'\x00'之前不会停止读取

fgets()从stdin读取输入,直到遇到换行符,EOF,或者读取指定大小的字符才会结束读取

python

语法


1. 字符串处理

p.recv() 通常返回一个字节流或者字符串数据,学会如何操作字符串是第一步。

重点内容:

  • strip()

    • 用于移除字符串两端的多余字符(如换行符 \n、空格、制表符等)。
    data = "  0x7ffff7a05c00\n"
    clean_data = data.strip()  # "0x7ffff7a05c00"
    
  • split()join()

    • 按特定分隔符分割字符串,或者将列表合并为字符串。
    # 分割
    data = "0x7ffff7a05c00: info"
    addr, info = data.split(":")  # addr = "0x7ffff7a05c00", info = " info"
    
    # 合并
    parts = ["0x7ffff7a0", "5c00"]
    joined = "".join(parts)  # "0x7ffff7a05c00"
    

学习目标:

  • 理解字符串与字节流的基本操作。
  • 掌握 strip() 和相关的字符串清理方法。

2. 类型转换

这部分涉及如何将接收到的字符串形式的地址或数字转换为整数。

重点内容:

  • int()

    • 将字符串形式的数字转换为整数,可以指定进制(如 16 表示十六进制)。
    hex_str = "0x7ffff7a05c00"
    addr = int(hex_str, 16)  # 140737348824576
    
  • hex()bin()

    • 将整数转换为十六进制或二进制字符串。
    addr = 140737348824576
    hex_str = hex(addr)  # '0x7ffff7a05c00'
    bin_str = bin(addr)  # '0b111111111111111011111110100000010111001100000000'
    
  • 进制理解:

    • 十六进制(0x 开头):基数为 16,常用于内存地址。
    • 十进制:普通整数形式。

3. 字节流与字符串

Pwn 脚本中的 p.recv() 通常返回字节流 (bytes) 数据,你需要知道如何将它转换为字符串。

重点内容:

  • decode()encode()

    • 字节流和字符串的互相转换。
    data = b'0x7ffff7a05c00\n'
    string_data = data.decode('utf-8')  # '0x7ffff7a05c00\n'
    byte_data = string_data.encode('utf-8')  # b'0x7ffff7a05c00\n'
    
  • strip() 对字节流同样有效

    data = b'  0x7ffff7a05c00\n'
    clean_data = data.strip()  # b'0x7ffff7a05c00'
    

学习目标:

  • 理解字节流 (bytes) 和字符串 (str) 的区别。
  • 掌握常见的编码 (encode) 和解码 (decode) 方法。

4. 数据解析

在 Pwn 中,经常需要处理接收到的数据,将其转化为有用的信息。

重点内容:

  • 提取有用部分: 使用正则表达式或字符串切片来提取地址等信息。

    data = "Leak: 0x7ffff7a05c00\n"
    addr_str = data.split(":")[1].strip()  # "0x7ffff7a05c00"
    addr = int(addr_str, 16)  # 转换为整数
    
  • 格式化数据:使用

    f-string
    

    .format()
    

    格式化数据。

    addr = 140737348824576
    print(f"Leaked address: {addr:#x}")  # 输出 'Leaked address: 0x7ffff7a05c00'
    

5. 调试技巧

在 Pwn 脚本中调试是必备技能,熟悉以下内容能帮助你快速定位问题:

重点内容:

  • print()repr()

    • 检查接收到的数据和处理过程。
    data = p.recv()
    print(f"Received: {data!r}")  # 带调试信息显示原始数据
    
  • type()

    • 检查变量类型,确保操作方法正确。
    print(type(data))  # <class 'bytes'>
    

数据处理

1. 返回内容的可能性

情况 1:\x00\x5c\xa0\xf7\xff\x7f(字节流,原始二进制数据)

  • 表示:这是纯二进制数据,用于表示地址或其他数据结构。

  • 场景:

    • 目标程序直接发送内存地址的二进制表示。
    • 常见于用 write 系统调用直接泄露内存时。
  • 示例:

    leaked_data = p.recv(6)  # 假设泄露了6字节地址
    print(leaked_data)  # 输出类似 b'\x00\x5c\xa0\xf7\xff\x7f'
    
    # 转换为整数
    addr = int.from_bytes(leaked_data, 'little')  # 假设小端序
    print(hex(addr))  # 输出 0x7ffff7a05c00
    

情况 2:"0x7ffff7a05c00"(字符串,内存地址的十六进制表示)

  • 表示:目标程序将内存地址以字符串形式发送,带有 0x 前缀。

  • 场景:

    • 通常在目标程序中,使用 printf("%p") 格式化输出地址。
    • 常见于文本协议或 debug 信息输出。
  • 示例:

    leaked_data = p.recvline()  # 假设程序输出地址并换行
    print(leaked_data)  # b'0x7ffff7a05c00\n'
    
    # 转换为整数
    addr = int(leaked_data.strip(), 16)
    print(hex(addr))  # 输出 0x7ffff7a05c00
    

情况 3:b'0x7ffff7a05c00'(字节形式的字符串)

  • 表示:类似情况 2,但仍是字节流形式。

    • b'...' 表示 Python 中的字节类型,而不是普通字符串。
  • 场景:

    • 目标程序返回的内容是文本,但没有被自动转换为字符串(Python 的 p.recv() 默认返回字节流)。
  • 示例:

    leaked_data = p.recvline()
    print(leaked_data)  # 输出 b'0x7ffff7a05c00\n'
    
    # 解码为字符串
    string_data = leaked_data.decode('utf-8').strip()
    addr = int(string_data, 16)
    print(hex(addr))  # 输出 0x7ffff7a05c00
    

2. 这三者的区别

表示形式 数据类型 内容 用法
\x00\x5c\xa0\xf7\xff\x7f bytes 原始二进制数据,表示内存地址 需要用 int.from_bytes 转换为整数。
"0x7ffff7a05c00" str 普通字符串,表示地址的十六进制形式 可直接用 int(data, 16) 转换为整数。
b'0x7ffff7a05c00' bytes 字节类型的字符串,带有 0x 前缀 先用 .decode('utf-8') 转换为字符串,再用 int(data, 16) 转换为整数。

3. 如何判断目标程序返回的数据类型?

  • 直接打印输出: 调试时可以直接使用 print(repr(data)) 查看返回数据的真实类型和内容。

    data = p.recv()
    print(repr(data))  # b'0x7ffff7a05c00\n' 或 b'\x00\x5c\xa0\xf7\xff\x7f'
    
  • 根据协议判断

    • 如果目标程序使用 printf("%p"),返回的通常是 b'0x...' 的字符串形式
    • 如果目标程序直接泄露内存(如 write),返回的是 原始二进制数据

4. 实际应用中的处理方法

根据返回的数据类型,采用不同的方式进行解析和处理:

如果是二进制数据(\x00\x5c\xa0\xf7\xff\x7f):

data = p.recv(6)  # 接收 6 字节
addr = int.from_bytes(data, 'little')  # 小端序转换为整数
print(hex(addr))  # 输出 0x7ffff7a05c00

如果是十六进制字符串("0x7ffff7a05c00"b'0x7ffff7a05c00'):

data = p.recvline()  # 接收一行数据
addr = int(data.decode('utf-8').strip(), 16)  # 解码并转换为整数
print(hex(addr))  # 输出 0x7ffff7a05c00

5. 总结

  • \x00\x5c\xa0\xf7\xff\x7f 是原始二进制数据,通常需要用 int.from_bytes 解析。
  • "0x7ffff7a05c00" 是字符串表示的地址,直接用 int(data, 16) 解析。
  • b'0x7ffff7a05c00' 是字节流形式的字符串,需要先用 .decode() 转换,再用 int() 解析。

你可以通过调试工具(如 GDB)或直接打印目标程序的输出数据,来判断返回的具体格式,然后根据实际情况选择合适的处理方法。

在 Pwntools 或类似场景中,发送 b'\x00\x5c\xa0\xf7\xff\x7f', "0x7ffff7a05c00", 和 b'0x7ffff7a05c00' 会导致程序接收到完全不同的内容。这些形式的区别在于数据的内容和格式。以下是它们的详细解释:

数据发送

1. b'\x00\x5c\xa0\xf7\xff\x7f'

内容

  • 这是纯粹的字节流,由 6 个字节组成:
    • 0x00
    • 0x5C
    • 0xA0
    • 0xF7
    • 0xFF
    • 0x7F

含义

  • 通常表示一个内存地址(如 0x7ffff7a05c00 的小端序表示)。
  • 在程序中,机器码、内存地址或原始数据一般以这种二进制字节流的形式表示。
  • 发送后: 程序会接收到原始的 6 个字节,解读时会依据上下文(比如小端序或大端序)解析为地址、数据或指令。

示例

p.send(b'\x00\x5c\xa0\xf7\xff\x7f')  # 发送原始字节数据

2. "0x7ffff7a05c00"

内容

  • 这是一个字符串,内容是地址的

    可读文本形式

    • 长度为 14 字符(包括 "0x")。
    • 每个字符都以 ASCII 编码发送到程序。

含义

  • 程序接收到的是一段文本,而不是原始二进制数据。
  • 若程序设计为解析字符串(如 scanf("%lx", ...) 或手动解析),它可能会将 "0x7ffff7a05c00" 转换为实际地址。
  • 发送后: 程序接收到的内容是 b'0x7ffff7a05c00'

示例

p.send("0x7ffff7a05c00")  # 发送文本表示的地址

3. b'0x7ffff7a05c00'

内容

  • 这是一个字节串,内容与

    "0x7ffff7a05c00"
    

    相同,但在 Python 中直接以

    bytes
    

    表示。

    • 每个字符仍以 ASCII 表示。
    • 长度为 14 字节。

含义

  • "0x7ffff7a05c00" 的内容完全相同,只是数据类型不同(前者是 bytes,后者是 str)。
  • 程序接收到的数据依然是字符串形式的地址,如果没有额外处理,程序不会直接将其作为内存地址。

示例

p.send(b'0x7ffff7a05c00')  # 发送字节形式的地址字符串

区别总结

形式 内容类型 长度 程序接收的实际内容 适用场景
b'\x00\x5c\xa0\xf7\xff\x7f' 原始字节流 6 字节 原始二进制数据 内存地址、机器码、原始数据传输
"0x7ffff7a05c00" 文本字符串 14 字节 文本 b'0x7ffff7a05c00' 需要传递可读字符串(程序会解析字符串成地址)
b'0x7ffff7a05c00' 字节串(ASCII 文本) 14 字节 文本 b'0x7ffff7a05c00' 同上,直接使用 bytes 类型

如何选择?

  1. 需要传递内存地址或机器码: 使用 b'\x00\x5c\xa0\xf7\xff\x7f',这会直接传递地址的原始字节流。

    比如这个python的运行结果为,可以看到二者一样

    from pwn import *
    print(p64(0x7ffff7a05c00))
    print(b'\x00\x5c\xa0\xf7\xff\x7f')
    
    #b'\x00\\\xa0\xf7\xff\x7f\x00\x00'
    #b'\x00\\\xa0\xf7\xff\x7f'
    
  2. 需要传递地址字符串供程序解析: 使用 "0x7ffff7a05c00"b'0x7ffff7a05c00',这会传递一个可读的地址字符串。

几个权限

    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      No canary found
    NX:         NX unknown - GNU_STACK missing
    PIE:        PIE enabled
    Stack:      Executable
    RWX:        Has RWX segments
    Stripped:   No

Rech

程序架构信息,判断是64位还是32位,exp编写的时候是p64还是p32

RELRO

Relocation Read-Onl(RELRO)此项技术主要针对GOT改写的攻击方式,它分成两种,Partial RELRO和FULL RELRO

Partial (部分)RELRO容易受到攻击,例如攻击者可以atoi.got为system.plt进而输入/bin/sh\x00获得shell,完全RELRO使整个GOT只读,从而无法被覆盖,但这样会大大增加程序的启动时间,因为程序在启动之前需要解析所有的符号。

gcc -o hello test.c //默认情况下,是Partial RELRO
gcc -z norelro -o hello test.c // 关闭,即No RELRO
gcc -z lazy -o hello test.c // 部分开启,即Partial RELRO
gcc -z now -o hello test.c // 全部开启,即Full RELRO

Stack-canary

栈溢出保护是一种缓冲区溢出攻击缓解手段,当函数存在缓冲区溢出攻击漏洞是,攻击者可以覆盖栈上的返回地址来让shellcode能够得到执行,当启用栈保护后,函数开始执行的时候先会往栈里插入类似cookie信息,当函数真正返回的时候会验证cookie信息是否合法,如果不合法就停止程序运行,攻击者在覆盖返回地址的时候往往会将cookie信息给覆盖掉,导致栈保护检车失败而阻止shellcode的执行,在linux中我们将cookie信息称为canary。

gcc -fno-stack-protector -o hello test.c   //禁用栈保护
gcc -fstack-protector -o hello test.c    //启用堆栈保护,不过只为局部变量中含有 char 数组的函数插入保护代码
gcc -fstack-protector-all -o hello test.c  //启用堆栈保护,为所有函数插入保护代码

NX

NX enabled如果这个保护开启就是意味着栈中数据没有执行权限,如此一来,当攻击者在堆栈上部署自己的shellcode并触发时,智慧直接造成程序的崩溃,但是可以利用rop这种方法绕过

gcc -o  hello test.c // 默认情况下,开启NX保护
gcc -z execstack -o  hello test.c // 禁用NX保护
gcc -z noexecstack -o  hello test.c // 开启NX保护

PIE

PTE(Position-Independent Executable,位置无关可执行文件)技术与ASLR技术类似,ASLR将程序运行时的堆栈以及共享库的加载地址随机化,而PIE及时则在编译时将程序编译为位置无关,即程序运行时各个段(如代码但等)加载的虚拟地址也是在装载时才确定,这就意味着。在PIE和ASLR同时开启的情况下,攻击者将对程序的内存布局一无所知,传统改写GOT表项也难以进行,因为攻击者不能获得程序的.got段的虚地址。若开始一般需在攻击时歇够地址信息

gcc -o hello test.c  // 默认情况下,不开启PIE
gcc -fpie -pie -o hello test.c  // 开启PIE,此时强度为1
gcc -fPIE -pie -o hello test.c  // 开启PIE,此时为最高强度2
(还与运行时系统ALSR设置有关)

shellcode

shellcode进阶之手写shellcode - 先知社区

关于栈

栈的图

img

一.什么是堆栈平衡(比较抽象)

img

含义就是 当函数在一步步执行的时候 一直到 ret 执行之前,堆栈栈顶的地址 一定要是 call 指令的下一个地址。

也就是说函数执行前一直到函数执行结束,函数里面的堆栈是要保持不变的。

如果堆栈变化了,那么,要在 ret 执行前将堆栈恢复成原来的样子。

第一种情况:push 影响堆栈

比如 call ...

函数:mov ... (不影响堆栈平衡)

push..... (影响堆栈平衡)

ret.....

第二种情况:堆栈传递参数

img

......

img

堆栈如下:

img

因为 PUSH 1 PUSH 2 是为了函数传参而准备的 ,当函数执行完成后 ,push1,push2 就都没用了,所以要把堆栈恢复到执行前的位置

两种解决办法 :函数外部处理和内部处理

第一种 :在函数外部添加 ADD 处理

img

第二种:在函数内部添加

img

ret 8 是把 ret 和第一种情况的 add 两条指令整合成一条指令,在函数内部完成堆栈平衡

posted @ 2024-10-23 22:47  r_0xy  阅读(39)  评论(0)    收藏  举报