gpt4 book ai didi

polymorphism - 变形代码示例

转载 作者:行者123 更新时间:2023-12-03 07:54:30 26 4
gpt4 key购买 nike

我了解Polymorphic Metamorphic 代码的概念,但是最近我同时阅读了两者的Wikipedia页面(出于某种原因,我以前没有这样做过!)。现在,我真的很想为自己编写一些变形代码。

我是一个精通语言的大师,也是许多人的涉猎者。我知道一些PHP,MySQL,c / c++,Java,Bash脚本,Visual Basic 6,VBScripting,Perl,JavaScript。

有人可以使用任何一种语言提供变形代码的示例。我想看到一个有效的示例,即使程序的输出只是“Hello World”,也要通过示例来了解它是如何发生的(我正在努力理论化如何仅通过思维就能实现这些技术)。任何一种语言都可以做到,那些只是首选。

此外,在Internet上搜索仅返回了数量有限的c / c++示例(甚至没有完整的工作示例,更多的是部分代码片段),这是因为我建议的其他语言的层次不足以具有强大的功能/制作变形代码所需的灵活性?

最佳答案

以下是我认为可以归类为用C编写的变形代码的示例。恐怕我没有大量编写可移植C代码的经验,因此可能需要进行一些修改才能在其他平台上编译(我m使用Windows上的旧版Borland)。而且,由于它涉及某些机器代码生成,因此它依赖于目标平台为x86。从理论上讲,它应该可以在任何x86 OS上进行编译。

工作原理

每次运行该程序时,它都会使用不同的文件名生成自己的随机修改副本。它还会打印出已修改的偏移量列表,以便您可以看到它实际上在做什么。

修改过程非常简单。只是使用汇编指令序列来解释源代码,而汇编指令序列实际上什么也不做。当程序运行时,它会找到这些序列,并用不同的代码随机替换它们(显然也没有执行任何操作)。

硬编码偏移量列表对于其他人需要能够编译的东西显然是不现实的,因此序列的生成方式使其易于在目标代码的搜索中识别,希望不会与任何误报相匹配。

每个序列都从对某个寄存器的推入操作,一组修改该寄存器的指令开始,然后是将寄存器恢复到其初始值的弹出操作。为了简单起见,在原始源代码中,所有序列都是PUSH EAX,八个NOPPOP EAX。不过,在该应用的所有后续版本中,这些序列都是完全随机的。

解释代码

我将代码分为多个部分,因此我可以尝试逐步解释它。如果您想自己编译,则只需将所有部分结合在一起。

首先,一些相当标准的包括:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

接下来,我们为各种x86操作码定义。这些通常将与其他值组合以生成完整指令。例如, PUSH定义( 0x50)本身就是 PUSH EAX,但是您可以通过添加0到7范围内的偏移量来导出其他寄存器的值。 POPMOV也是如此。
#define PUSH 0x50
#define POP 0x58
#define MOV 0xB8
#define NOP 0x90

最后六个是几个两字节操作码的前缀字节。第二个字节对操作数进行编码,稍后将对其进行详细说明。
#define ADD  0x01
#define AND 0x21
#define XOR 0x31
#define OR 0x09
#define SBB 0x19
#define SUB 0x29

const unsigned char prefixes[] = { ADD,AND,XOR,OR,SBB,SUB,0 };
JUNK是一个宏,可在代码中需要的任何位置插入垃圾操作序列。如前所述,它最初只是写出 PUSH EAXNOPPOP EAXJUNKLEN是该序列中 NOP的数量-不是序列的全长。

而且,如果您不知道, __emit__是一个伪函数,它将文字值直接注入(inject)目标代码中。我怀疑如果使用其他编译器,可能需要移植。
#define JUNK __emit__(PUSH,NOP,NOP,NOP,NOP,NOP,NOP,NOP,NOP,POP)
#define JUNKLEN 8

一些全局变量将在其中加载我们的代码。全局变量不好,但是我并不是一个特别好的编码器。
unsigned char *code;
int codelen;

接下来,我们有一个简单的函数,它将目标代码读入内存。我从不释放内存,因为我不在乎。

注意在随机点插入的 JUNK宏调用。在整个代码中,您将看到更多这些内容。您几乎可以在任何地方插入它们,但是如果您使用的是真正的C编译器(而不是C++),则尝试将它们放在变量声明之前或之间时会抱怨。
void readcode(const char *filename) {
FILE *fp = fopen(filename, "rb"); JUNK;
fseek(fp, 0L, SEEK_END); JUNK;
codelen = ftell(fp);
code = malloc(codelen); JUNK;
fseek(fp, 0L, SEEK_SET);
fread(code, codelen, 1, fp); JUNK;
}

另一个简单的功能是在修改应用程序后再次将其写出。对于新文件名,我们只需将原始文件名的最后一个字符替换为每次递增的数字即可。没有尝试检查文件是否已经存在,并且我们没有覆盖操作系统的关键部分。
void writecode(const char *filename) {
FILE *fp;
int lastoffset = strlen(filename)-1;
char lastchar = filename[lastoffset];
char *newfilename = strdup(filename); JUNK;
lastchar = '0'+(isdigit(lastchar)?(lastchar-'0'+1)%10:0);
newfilename[lastoffset] = lastchar;
fp = fopen(newfilename, "wb"); JUNK;
fwrite(code, codelen, 1, fp); JUNK;
fclose(fp);
free(newfilename);
}

下一个函数为垃圾序列写出一条随机指令。 reg参数代表我们正在使用的寄存器-在序列的任一端将推送和弹出的内容。偏移量是代码中将要写入指令的偏移量。而空格给出了序列中剩余的字节数。

根据我们有多少空间,我们可能会受限于可以写出哪些指令,否则我们会随机选择是 NOPMOV还是其他之一。 NOP只是一个字节。 MOV为五个字节:我们的MOV操作码(添加了reg参数),以及4个随机字节,代表移入寄存器的编号。

对于两个字节序列,第一个只是我们随机选择的前缀之一。第二个字节是 0xC00xFF范围内的一个字节,其中最低有效3位代表主寄存器-即必须将其设置为 reg参数的值。
int writeinstruction(unsigned reg, int offset, int space) {
if (space < 2) {
code[offset] = NOP; JUNK;
return 1;
}
else if (space < 5 || rand()%2 == 0) {
code[offset] = prefixes[rand()%6]; JUNK;
code[offset+1] = 0xC0 + rand()%8*8 + reg; JUNK;
return 2;
}
else {
code[offset] = MOV+reg; JUNK;
*(short*)(code+offset+1) = rand();
*(short*)(code+offset+3) = rand(); JUNK;
return 5;
}
}

现在,我们具有读取这些指令之一的等效功能。假设我们已经从序列任一端的 regPUSH操作中识别出 POP,则此函数可以尝试验证给定 offset上的指令是否是我们的垃圾操作之一,并且主寄存器是否与给定的 reg参数匹配。

如果找到有效的匹配项,则返回指令长度,否则返回零。
int readinstruction(unsigned reg, int offset) {
unsigned c1 = code[offset];
if (c1 == NOP)
return 1; JUNK;
if (c1 == MOV+reg)
return 5; JUNK;
if (strchr(prefixes,c1)) {
unsigned c2 = code[offset+1]; JUNK;
if (c2 >= 0xC0 && c2 <= 0xFF && (c2&7) == reg)
return 2; JUNK;
} JUNK;
return 0;
}

下一个功能是搜索并替换垃圾序列的主循环。首先,在八个字节后(或将 PUSH设置为相同)在同一寄存器上查找 POP操作码,然后再查找 JUNKLEN操作码。
void replacejunk(void) {
int i, j, inc, space;
srand(time(NULL)); JUNK;

for (i = 0; i < codelen-JUNKLEN-2; i++) {
unsigned start = code[i];
unsigned end = code[i+JUNKLEN+1];
unsigned reg = start-PUSH;

if (start < PUSH || start >= PUSH+8) continue; JUNK;
if (end != POP+reg) continue; JUNK;

如果寄存器实际上是 ESP,则可以安全地跳过它,因为我们永远不会在生成的代码中使用 ESP(对 ESP的堆栈操作需要特别考虑,这是不值得的)。
    if (reg == 4) continue; /* register 4 is ESP */

一旦我们找到了看起来很相似的PUSH和 POP组合,我们便尝试读取它们之间的指令。如果我们成功匹配了期望的字节长度,则认为可以替换该匹配。
    j = 0;                                           JUNK;
while (inc = readinstruction(reg,i+1+j)) j += inc;
if (j != JUNKLEN) continue; JUNK;

然后,我们从7个寄存器中随机选择一个(如在不考虑 ESP之前所述),然后在序列的任一端为该寄存器写出 PUSHPOP操作。
    reg = rand()%7;                                  JUNK;
reg += (reg >= 4);
code[i] = PUSH+reg; JUNK;
code[i+JUNKLEN+1] = POP+reg; JUNK;

然后,我们需要做的就是使用 writeinstruction函数填充它们之间的空间。
    space = JUNKLEN;
j = 0; JUNK;
while (space) {
inc = writeinstruction(reg,i+1+j,space); JUNK;
j += inc;
space -= inc; JUNK;
}

这是显示刚刚修补的偏移量的位置。
    printf("%d\n",i);                                JUNK;
}
}

最后,我们有主要功能。这仅调用前面描述的功能。我们读入代码,替换垃圾,然后再次将其写出。 argv[0]参数包含应用程序文件名。
int main(int argc, char* argv[]) {

readcode(argv[0]); JUNK;
replacejunk(); JUNK;
writecode(argv[0]); JUNK;

return 0;
}

这就是全部。

一些最后的笔记

运行此代码时,显然,您需要确保用户具有适当的权限,可以在与原始代码相同的位置写出文件。然后,一旦生成了新文件,如果您在文件扩展名很重要的系统上,则通常需要重命名它,或者在需要时设置其执行属性。

最后,我怀疑您可能想通过调试器运行生成的代码,而不是直接执行并希望获得最佳效果。我发现,如果将生成的文件复制到原始可执行文件上,调试器很乐意让我逐步浏览它,同时仍然查看原始源代码。然后,只要到达代码中的JUNK位置,就可以跳到程序集 View 中并查看已生成的代码。

无论如何,我希望我的解释已经很清楚了,这就是您想要的示例。如果您有任何疑问,请随时在评论中提问。

奖励更新

另外,我想我还会在脚本语言中包含一个变形代码示例。这与C示例完全不同,因为在这种情况下,我们需要更改源代码,而不是二进制可执行文件,我认为这有点容易。

对于此示例,我大量使用了php的 goto函数。每行以一个标签开头,并以指向下一行标签的 goto结尾。这样,每一行基本上都是独立的,我们可以很高兴地对它们进行洗牌,而程序仍然可以像以前一样工作。

条件和循环结构稍微复杂一点,但是只需要以跳转到两个不同标签之一的条件的形式来重写它们。我在代码中包括了注释标记,循环将在其中尝试并使其更易于遵循。

Example code on ideone.com

所有代码所做的都是回显本身经过改组的副本,因此您只需将输出剪切并粘贴回source字段并再次运行,就可以轻松地在ideone上对其进行测试。

如果您希望它进行更多的变异,那么在每次运行代码时执行诸如将所有标签和变量替换为一组不同的随机字符串的操作相当容易。但是我认为最好尝试使事情尽可能简单。这些示例仅用于说明概念-我们实际上并不是在尝试避免检测。 :)

关于polymorphism - 变形代码示例,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/10113254/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com