pwn备忘

注意事项

  • read函数读取终端的输入,循环读入一直持续到按Ctrl + D即EOF为止,此时输入不包含\n
  • system参数前面有地址,需要参数截断($0同sh):&&sh ||sh ;sh;
  • 以cin,scanf读入时遇到0x0a,0x20会截断,因此如果地址有0x0a,0x20要设法绕过(+0x8)!(sample:myzoo-安恒月赛-2018-10)
  • 连续open文件1024次,使文件描述符数组满了后,无法再次打开文件,用于绕过读取random文件验证。
  • scanf("%d")输入+或者-不会改变原来栈上内容。(sample:easy_rop-NCTF2019)
  • read函数需要保证整段输入缓冲区buff+nbytes都有写权限,否则即使只读取一个字节也会失败!因此,如果伪造输入缓冲区到栈Alloc_to_stack时,需要额外小心是否已经超出栈底,可以通过加argv参数进行验证。(sample:smallorange-安恒月赛-2018-12)
  • close(1),关闭标准输出后,通过write(0,"123\n",4)向标准输入打印也是可以获得输出值的,但是本地调试的时候采用p = process('./blind_note'),pwntools无法得到输出,因此需要通过socat tcp-l:9999,reuseaddr,fork exec:./blind_note &将程序绑定到9999端口,然后模拟远程连接p = remote('127.0.0.1', 9999),pwntools才能得到输出。get shell之后,同样需要将输出重定向到标准输入才可看到输出值:ls >&0(sample:blind_note-安恒月赛-2018-11)

linux_64与linux_86的区别

  • 内存地址的范围由32位变成了64位。但是可以使用的内存地址不能大于0x00007fffffffffff,否则会抛出异常。
  • 函数参数的传递方式发生了改变,x86中参数都是保存在栈上,但在x64中的前六个参数依次保存在RDI, RSI, RDX, RCX, R8和 R9中,如果还有更多的参数的话才会保存在栈上。
  • 泄漏ELF,32位从0x08048000开始,64位从0x400000开始

函数入口与出口汇编指令

push ebp  # 将ebp压栈
mov ebp, esp #将esp的值赋给ebp

leave = mov esp, ebp # 将ebp的值赋给esp
        pop ebp #弹出ebp
ret = pop eip #弹出栈顶元素作为程序下一个执行地址

格式化字符串

  1. 利用%x来获取对应栈的内存,但建议使用%p,可以不用考虑位数的区别
  2. 利用%s来获取变量所对应地址的内容,只不过有零截断
  3. 利用%n$x来获取指定参数的值,利用%n$s来获取指定参数对应地址的内容。获取栈中被视为第n+1个参数的值
  4. %12$n不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。%hhn向某个地址写入单字节,利用%hn向某个地址写入双字节,%$lln表示写入的地址空间为8字节
  5. %*25$d从栈中取变量作为N,比如:栈25$处的值是0x100,那么这个格式化字符串就相当于%256d。(sample:pwn4-MidnightsunCTF-2020)

利用pwntools工具:

求偏移fmtarg

1
2
3
4
5
fmtstr_payload(offset, writes, numbwritten=0, write_size='byte')
第一个参数表示格式化字符串的偏移;
第二个参数表示需要利用%n写入的数据,采用字典形式{目标地址:修改的值}
第三个参数表示已经输出的字符个数,默认为0;
第四个参数表示写入方式,是按字节(byte)、按双字节(short)还是按四字节(int),对应着hhn、hn和n,默认值是byte,即按hhn写。

利用

  1. hijack GOTfmtstr_payload(7, {puts_got: system_addr})
  2. hijack retaddrp64(ret_addr) + "%2218d%8$hn"
  3. 堆上格式化字符串漏洞:修改上层函数保存的ebp(即上上层函数的 ebp) 为堆上预先存储system_addr的地址-4,当执行leave=mov esp, ebp; pop ebp时,ebp=system_addr-4。当再执行leave,mov esp, ebp将esp=ebp=system_addr-4,pop ebp出栈操作将esp+4,指向system_addr,此时,ret将EIP指向system_addr,从而getshell。

沙箱保护

seccomp

1
2
3
#include <linux/seccomp.h>
ctx = seccomp_init(SCMP_ACT_ALLOW); //默认允许所有
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0); //如果arg_cnt不为0,那arg_cnt表示后面限制的参数的个数,也就是只有调用execve,且参数满足要求时,才会拦截syscall.

prctl函数

1
2
3
4
5
6
7
#include <sys/prctl.h>
int prctl(int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5);
//参数1设置为PR_SET_NO_NEW_PRIVS(38),参数2设置为1,不能够进行execve的系统调用
//参数1设置为PR_SET_SECCOMP(22),参数2设置为2,使用Berkeley Packet Filter(BPF)设置seccomp沙箱规则
/** /usr/include/linux/prctl.h **/
#define PR_SET_SECCOMP  22
#define PR_SET_NO_NEW_PRIVS     38

借助seccomp-tools检查禁用的系统调用

1
$ seccomp-tools dump warmup

通过OpenReadWrite的方式直接读取输出,过滤了open,可以用openat代替。

fd = open('flag',0) --> read(fd,buf,0x100) --> write(1,buf,0x100)

1
2
3
4
5
6
7
8
payload=p64(pop_rdi_ret)+p64(0)+p64(pop_rsi_ret)+p64(free_hook)+p64(pop_rdx_ret)+p64(4)+p64(read_addr) #read(0, free_hook, 4),没开pie保护可选用bss,开启所有保护时,借助free_hook作为输入输出缓冲区
payload+=p64(pop_rdi_ret)+p64(free_hook)+p64(pop_rsi_ret)+p64(0)+p64(open_addr) #open(free_hook, O_RDONLY)
payload+=p64(pop_rdi_ret)+p64(3)+p64(pop_rsi_ret)+p64(free_hook)+p64(pop_rdx_ret)+p64(0x30)+p64(read_addr) #read(3, free_hook, 0x30),从fd=3(默认0/1/2是STDIN/OUT/ERR,文件指针从3开始)读0x30个字节到free_hook缓冲区
payload+=p64(pop_rdi_ret)+p64(free_hook)+p64(puts_addr) #puts(free_hook)输出
#/usr/include/x86_64-linux-gnu/bits/fcntl-linux.h
#define O_RDONLY             00
#define O_WRONLY             01
#define O_RDWR               02

如果溢出空间太小(x64环境下至少溢出0x10个字节),需要借助栈迁移。

1
2
#预先设置好bss
payload='a'*offset + p64(bss) + p64(leave_ret)

栈迁移(stack pivoting)

原理:借助leave=mov esp,ebp; pop ebp;控制栈顶指针esp

适用条件:读入长度太少,无法直接在当前栈完成攻击,需要将栈迁移到可控区域(多为bss段)。(sample:lab 6-HITCON-Training

1
2
3
4
payload += bss + read_plt + leave_ret + 0 + bss + 0x100 #read(0, bss, 0x100)
send(payload)
payload = bss_1 + put_plt + pop + x_got + read_plt + leave_ret + 0 + bss_1 + 0x100
send(payload)

先pop ebp使得ebp=bss,再调用read布置bss,当从read返回调用leave_ret时,esp=ebp=bss,实现控制esp到bss的目的,同理,接下来可以多次迁移。

syscall系统调用

运行在用户空间的程序向操作系统内核请求更高权限运行的服务。

32位

传参方式:首先将系统调用号传入eax,然后将参数从左到右依次存入ebx,ecx,edx寄存器中,返回值存在eax寄存器

调用号(/usr/include/asm/unistd_32.h):read的调用号为3,sys_write的调用号为4,execve的调用号为11

调用方式:使用int 80h中断进行系统调用

64位

传参方式:首先将系统调用号传入rax,然后将参数从左到右依次存入rdi,rsi,rdx寄存器中,返回值存在rax寄存器

调用号(/usr/include/asm/unistd_64.h):read的调用号为0,sys_write的调用号为1,execve的调用号为59

调用方式: 使用syscall进行系统调用

静态编译恢复libc符号

1
python lscan.py -S i386/sig -f /mnt/hgfs/matches/2023/230414wdbawd/pwn2/bin

然后根据将sig-database中对应的sig文件,放到IDA安装目录的sig/pc(切记一定是pc目录)

shift+F5,右键Apply new signature,选择对应的libc。

ARM-ROP

函数调用约定,函数的第1-4个参数分别保存在r0-r3寄存器中, 剩下的参数从右向左依次入栈,被调用者实现栈平衡,函数的返回值保存在r0中。r13栈指针,pc寄存器相当于x86 的eip,保存下一条指令的地址,也是我们要控制的目标。

b/bl等指令实现跳转。