(okwary) 小叹的学习园地

与天斗?不够高~ 与地斗?不够阔 与人斗? 脸皮不够厚

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
远程线程的代码重定位:就是修正函数、变量的地址使它们能在新的进程(线程)中正常调用.常用方法是使用偏移量,使这些地址用[偏移量+变量名]的形式来表现。用函数时,参数要用到全局变量的时候,要先lea出地址来;

//--------取偏移量方法 ----------------------------------------------------

    call @vstart

  @vstart:

    pop ebx

    sub ebx,offset @vStart //把绝对地址和相对偏移相减就可以获得相对偏移与绝对地址的转换关系

    mov eax, [ebx+offset var] //offset 关键字好似可省写

ebx为编移量.

//一个实例.

.code
 
 
  mycode:
 
  lpmsg   dd  ?
  sztil   db  '密码为: ',0
  szstr   db 'password is '
  szpwd   db  70h dup (0)
  
 ; --------------------

  startcode:
 
  pushad
 
  call l1
  l1:
  pop ebx

  sub ebx,offset l1

  ;很经典的代码自定位技术,不陌生吧!

  lea edi, [ebx + offset szpwd]

  mov edx,[ebp - 8]

  add edi,edx

  mov byte ptr [edi],al

  inc edx

  mov eax,[ebp -4]

  cmp edx,eax

  jl ext1

-------------------------------------------------

关于远程线程中的自定位: 


      dwvar  dd    ?  ;全局变量dwvar
              call @F
       @@:
              pop ebx
              sub ebx,offset @B
              mov    eax,[ebx + offset dwvar]

@F 向下最近的@@标号的地址

@B 向上最近的@@标号的地址

call 所取得是 函数地址

$ 则是当前地址

这里的offset @B,是相对偏移地址,那么将程序的一部分(其中包含以上自定位代码)写到其它的进程中,以便在其它进程中产生远程线程
那么这时的offset @B的相对偏移地址怎么解释,哪位大虾帮讲解一下!!!

感觉offset @B应该是@@标号,相对于本地进程的偏移地址,但本地进程只是把其中的一部分代码写进了其它进程,那么相对于写进其它进程的这部分代码,它的offset @B应该不对呀?

offset @B是@@标号在源进程中的地址,是一个立即数,这段代码复制到其它进程后,这个数是不变的。“sub ebx,offset @B”,ebx是@@标号在当前进程(被注入的进程)中的地址,向减之后就是被注入的代码在当前进程与源进程中偏移量的差。

对于注入的代码,因为注入的地址与原本的地址不同,所有代码中不能使用绝对地址。通常情况下,只要不使用直接寻址(直接使用全局变量),不使用标号的偏移量(OFFSET伪指令),就不会有问题。当使用全局变量或者偏移量时,可以使用上面ebx+的方法来修正,如果在其它函数中也需要使用全局变量或者偏移量时,可以用同样的方法,也可以在调用函数的时候把ebx作为参数传递过去。

 

 

//--------------------------------------------------------------------------------
在病毒里面经常会使用到这种技术,因为病毒的启动往往不是通过 windows 来加载,那么各个地址的重定位也就需要手工来完成;
如果代码本身就具备重定位功能的话,那么手工加载病毒就会容易的多,可以轻易把病毒塞入一块任意的由 VirtualAlloc 分配的内存.
实际上我们做的工作就是windows加载时要做的工作,代码如果通过windows来加载完成,那么相对偏移就会被windows重定位,与绝对地址的差值为0.
但是如果是手工加载,那么这个差值就与加载地址密切相关了.

//----------------------------

    call vStart  // call这个动作发生的时候,会把返回地址退入堆栈的顶部,此时返回地址就是vStart所在位置的绝对地址,当然call这个函数是通过相对偏移来调用的,不存在重定位的问题

vStart:

    pop ebp  //这里取出返回地址,注意此地址是进程空间里面的绝对地址

    sub ebp,offset vStart //把绝对地址和相对偏移相减就可以获得相对偏移与绝对地址的转换关系

    mov eax, [ebp+kernel32]  ;//有了转换关系之后就可以轻松调用各个由相对地址指定的数据段或者函数

    kernel32        dd     ?

//-------------------------------


一、重定位的原因:

 

  都说病毒第一步要重定位,那到底为什么要重定位呢?我们写正常程序的时候根本不用去关心变量(常量)的位置,因为源程序在编译的时候它的内存中的位置郡被计算好了。

程序装入内存时,系统不会为它重定位。我们需要用到变量(常量)的时候直接用变量名访问它就行了。

病毒不可避免也要用到变量 (常量),当病毒感染HOST程序后,由于其依附到HOST程序中的位置各有不同,病毒随着HOST载入内存后病毒中的各个变量 (常量)在内存中的位置自然也不相同。既然这些变量没有固定的地址,病毒在运行的过程中应该如何引用这些变量呢?所以,病毒只有自己帮助自己重定位,这样就可以正常地访问自己的相关资源了。


二、如何重定位:

 

大家都知道CALL是一条函数调用指令,也可以当成是跳转指令。它可以跳到目的地址继续执行,执行完毕后,会返回到主程序继续执行。那系统如何知道返回地址的呢?当CALL执行时,CPU首先把要返回的地址 (即下一条指令的地址)压火堆栈,然后跳到我们目的地址执行。可以看出,在跳转之后只要执行一条POP指令或MOV EXX,[ESP]就可以得到下一条指令在内存中的实际位置了。其实,对于任何一个变量,我们都可以采用这种方式进行重定位。


  好了,原理都讲完了,现在让我们总结一下重定位的基本步骤 (这里假设下一条指令为z1):
  (1)用CALL指令跳转到下一条指令,使z1在内存中的实际地址进栈。
  (2)用POP xx 或MOV EXX,[ESP]取出栈顶的内容,这样就得到了z1的地址 (BaSe)。
  (3)其他指令 (变量、常量)的实际地址就等于Base+(0ffSetLabe1-OffSet vstart)。

 

三、实例说明:


  现在,就让我们看一下重定位的具体代码:

    call vstart

  vstart:

    pop ebx

    sub ebx,offset vStart

    mov eax, [ebx+kernel32]

  这里VStart这个标号的位置就是z1的位置了。

下面看看代码是怎么实现的:

Call VStart跳到vStart,然后pop ebX把堆栈顶端的内容 (即VStart在内存中的地址)放到ebx。

这样。以后用到其他变量的时候就可以用ebX+(OffSet XXX-OffSet VStart)得到其在内存中的真正偏移地址了。
 

下面再具体一点。譬如我们想取变量abc的内容时,则可先取地址到esi中,然后使用 "mov eax,[esi]"指令即可得到abc的内容。


  abc dd 0
  ...
  call vstart
vstart:

  pop ebx
  ...
  lea esi,[ebx+(abc-vstart)]

上面我们提到偏移地址可以通过ebx+(Offset XXX-OffSet VStart)计算得到。

我们通常也可以看到如下重定位方式:


  abc dd.0
  ...
  call vstart
vstart:
  pop ebx
  sub ebx,offset vstart
  ...
  mov eax,[ebx+abc]

其实这和上面那种方法最终结果是一样的,只不过是换了一种形式,即 (ebX-0ffSetVStart)+OffSet XXX;

另外,在实际过程中还会碰到其他重定位方式,并且需要重定位的绝对不仅局限于变量和常量,不过所有原理都是一样的。

 

 

假如下面那段代码是在自己程序中的代码:

 

因为pop ebp的地址也是00401005,这时sub ebp,00401005的结果ebp就是等于0了。

不过现在假如将这段编译好的二进制代码拷贝到00402000处运行,你想会怎样?

  00401000 call 00401005 ; call nStart

  00401005 pop ebp

  00401006 sub ebp, 00401005 ; sub ebp,offset nStart

假如将这段代码拷贝到00402000处运行,就会变成如下面这样,你还认为是没用的吗?

  00402000 call 00402005

  00402005 pop ebp ; ebp == 00402005

  00402006 sub ebp, 00401005 ;

注nStart的标号地址在被编译为二进制后就是固定的00401005了,不会再变了,这时你认为ebp还是等于0?

实例:

1000AAFA mov dword ptr [1000F438], ebp ; 我要把[1000F438]里放入1
1000AB00 mov dword ptr [1000F234], ebp ; 我要把[1000F234]里放入270Fh
1000AB06 call dword ptr [<&USER32.SetTimer>] ;

改成这样
1000AAFA jmp 1000CDD0 ; 在代码段最后加上些代码,然后跳到那
1000AAFF nop
1000AB00 dword ptr [1000F234], ebp ;
1000AB06 call dword ptr [<&USER32.SetTimer>] ;

代码段最后找个空白地方加上
1000CDD0 mov dword ptr [1000F438], 1
1000CDDA mov dword ptr [1000F234], 270F
1000CDE4 jmp 1000AB06 

 //改好后因为重定位无法运行,

后改为 :

1000AAFA NOP
1000AAFB MOV EAX,1000CDD0
1000AB00 JMP EAX

1000CDD0 SUB EAX,0CDD0
1000CDD5 MOV DWORD PTR [EAX+0F438],1
1000CDDF MOV DWORD PTR [EAX+0F234],270F
1000CDE9 JMP 1000AB06

//解决了重定位问题。


-------------------------------

自修改代码( self-modifying code, SMC ) 意思是自我修改的代码,使程序在运行时自我修改,用途包括:

  1) 使一些重要的跳转位置爆破无效化 (以 smc 对重要位置进行覆写)

  2) 使一些重要代码隐藏 (在必要时才实时产生重要代码段,防止程序被人静态分析,也防止一些透过搜寻的破解方法)

 

自修改代码有很广泛的用途:

1.在10到20年前使用SMC(自保护代码)保护应用程序是很难的,即使是用它来把编译的代码放到内存里;

2.在90年代中期95/NT出现了,那时的程序员对在新的操作系统下如何保护应用程序感到迷惑.不知道该如何将保护措施移植到这个新的版本下.已经不可能再自由的访问内存,硬件,和一般的操作系统,所有以前学会的技巧不得不放弃,开始人们认为除了使用VxD外没法再写SMC,这都是因为文档没跟上而遭到各方的质疑. 然后发现要想在我们的程序中继续使用SMC,我们可以采用下面两种方式:

 

  A.使用从Kernell32导出的WriteMemoryProcess


  B.将代码放到堆栈中修改


Windows 内存的组织:

在Wnidws下创建SMS并没有我想的那么直接,首先你必须面对一些特别的方式,其次要用到Windows提供给你的指南. 这也许你知道,Wondows为进程分配了4GB的虚拟内存.这个内存的地址,Windows有两个用途;

其一是CS段寄存器;

其二是给了DS,SS,和ES寄存器

他们使用同样的内存基地址,(等于0),而且同样限制在4GB 只有一个段即包含代码也包含数据,那就是进程的堆栈,可以使用NEAR 调用或者jump控制堆栈上的本地代码,即不需要使用SS寄存器去访问堆栈,而且CS寄存器的值也不等于DS,ES,和SS寄存器;

MOV dest

CS:[src]

MOV dest

DS:[src]

MOV dest

SS:[src]

指令指向的是相同的本地地址内存页包含的数据,代码和堆栈有不同的属性,事实上,代码页允许读和执行,数据页允许读和写,堆栈同时允许读,写,和执行. 尽管他们都绑定了一些安全属性,后面我们会继续讲到.

 

使用WriteProcessMemory

改变进程内字节的最简单的方式是使用WriteMemoryProcess(当然是一些安全标志没有被设置的时候) 对于我们要修改的内存中的进程第一件事情是用OpenProcess打开它,同时需要设上PROCESS_VM_OPERATION 和PROCESS_VM_WRITE属性.这儿有一些SMC的简单例子,需要在C++中用到内联的汇编语言:

例程1:使用WritePreocessMemory创建SMC

int WriteMe(void *addr, int wb)
{
HANDLE h=OpenProcess(PROCESS_VM_OPERATION|PROCESS_VM_WRITE,
true, GetCurrentProcessId());
return WriteProcessMemory(h, addr, &wb, 1, NULL);
}

int main(int argc, char* argv[])
{
_asm {
push 0x74 ; JMP --> > JZ
push offset Here
call WriteMe
add esp, 8
Here: JMP short here
}
printf("Holy Sh^& OsIX, it worked! #JMP SHORT $-2 was changed to JZ $-2n");
return 0;
}

正如你所看到的,程序用JZ指令实现了跳转.这样程序就可以继续,程序告诉我们,使用jump成功了.不过WriteMemoryProcess还有一些缺点,首先它会被有经验的解密者在入口表处发现,它很可能会自这个调用处设置一个断点,然后单步找到它想要的代码,WriteProcessMemory一般的用途是编译器的编译内存中,或者可执行文件的解包中,但一定要确保不被解密者利用到. WriteMemoryProcess 的另一个缺点是无法在内存中创建新页.它只能在已存在的页上工作.

 

将代码放到堆栈上,然后执行

在堆栈上执行代码不是不可能,但是这样会不会产生线程安全性问题呢?如果安装了不允许代码在堆栈上执行的补丁,那么程序员就无技可施了.但反过来可能的结果是你的大部分程序将无法运行,Linux就有这样一个补丁.

记得上面提到的WriteMemoryProcess的几个缺点了么?好的,这是使用堆栈执行代码的两个原因,其一是,一个攻击者在不知道的内存块上是不可能通过发送指令来修改代码,他不得不去分析保护代码,这很难成功.第二个原因是在任何时候堆栈上的可执行代码是真实存在的,应用程序可以给堆栈分配足够的内存,不需要的时候可以释放,通常情况下,系统给堆栈分配1MB的内存,如果感觉分配的内存不够时,可以在程序的配置文件中修改.

 

为什么代码重定位不好呢?

你必须明白在Windows 9X, Windows NT, and Windows 2000.中堆栈的位置是不同的.为了使你的程序在移植到其他机器中还能使用,必须进行重定位,实现起来到不难,只需要满足一些简单的规则. 幸运的是在8086里所有的short jumps和near调用是相联系的.也就是说它不使用线性地址,只是下一条指令和目标地址之间不同,这使我们的代码重定位更加的简单,但也有一些约束.

例如,下面函数会发生什么呢?

void OSIXDemo()

{

printf("Hi from OSIXn");

}

函数被拷贝到了堆栈,控制传递给它了么?因为printf的地址已经改变了,这很可能会产生错误.

 

在汇编里面,我们可以使用寄存器很容易的锁定它,重定位调用printf函数就很简单,例如:

LEA EAX,

printf NCALL EAX 

现在是绝对线性地址,而不是关联的.被放置到了EAX寄存器里,现在无论从哪调用它,控制都可以被传递给printf.

 

做这些事情需要你的编译器支持线性的汇编,

例程2:代码如何拷贝到堆栈并且执行

void Demo(int (*_printf) (const char *,...))
{
_printf("Hello, OSIX!n");
return;
}

int main(int argc, char* argv[])
{
char buff[1000];
int (*_printf) (const char *,...);
int (*_main) (int, char **);
void (*_Demo) (int (*) (const char *,...));
_printf=printf;


//加上两个赋值语句
_main = main;
_Demo = Demo;
//

int func_len = (unsigned int) _main - (unsigned int) _Demo;
for (int a=0; a<func_len; a++)
buff[a] = ((char *) _Demo)[a];
_Demo = (void (*) (int (*) (const char *,...))) &buff[0]
_Demo(_printf);
return 0;
}

如果有人告诉你高级语言无法在堆栈上执行代码那么请不要相信.

 

需要的一些优化

首先你要考虑使用哪种编译器,如果你打算使用SMC或者要在堆栈上执行代码,你一定要好好的研究编译器的使用向导.很多人第一次失败的原因都是因为没有使编译器"最优化". 怎么会发生这种现象呢?因为在纯粹的高级语言中,例如C和PASCAL,曾经被谴责无法拷贝函数的代码到堆栈和其他地方,程序员可以获得一个函数的指针,但没有它的标准,我们的程序员称它为"魔数",因为只有编译器知道.幸运的是,几乎所有的编译器使用同样的逻辑产生代码,这样程序可以假设编译的代码,程序员也可以假设.

我们返回去看看例程2.假设指向函数的指针和函数的首地址一致,主体在首地址的后面,大多数编译器使用这种"common sense compiling",许多大的编译器都使用这种规则(VC++, Borland, 等等).所以如果你没使用一些不知名的编译器,那就不要担心这些. 需要注意的是VC++,如果在Debug模式,编译器就会插入一个"适配器"并且将函数分配到其他一些地方,谴责MS,但是不要担心,只要在编译器的选项里选中"Link Incrementally"就可以了.如果你的编译器没有这样的选项,或者类似的事情,你要么不使用SMC要么改用其他的编译器.

决定函数长度是另一个问题,但这是需要技巧的.在C++里,sizeof结构不返回函数自身的长度,而是指向函数的指针的大小.但按照规则:编译器是按源代码中代码出现的顺序分配内存.所以,函数主体的长度就等于指向函数的指针和指向函数后面的指针之间的距离.容易吧!  

优化编译器还有另一件事情去做:删除那些他们认为不再使用的变量.

返回去看例程2.一些东西被写到了buff缓存.但是没有从那个地方读任何东西,大多数的编译器无法是被传递到缓存的控制.所以他们删除了正在拷贝的代码.这就是为什么控制被传递到了未被初使化的缓存.接着程序崩溃了,如果出现这种问题,请取消"Global optimization"选项.

 

如果你的程序仍然不工作,别放弃,原因很可能是编译器在每个函数的末尾插入了常规调用来监控堆栈.Microshoft' VC++就是这么做的.它在debug项目里放置了__chkesp调用.不要打算到文档里找到它,那里面没有、想一想,这种调用是相关的,没办法屏蔽它,然而,在release版本里,VC++不检查堆栈的状态.所以就不会出现这种问题.

 

在自己的APPS中使用SMC

 

现在你可以问"在堆栈上执行代码的好处是什么?"回答是:堆栈上的函数的代码可以被很灵巧的改变. 即使笨人的加密代码也使解密高手变的难堪.当然,如果调试的话会变的容易一点.但还是很难.

 

这个简单的加密算法将用异或连续的处理代码的每一行,并且重新执行一遍将产生我们需要的目标代码.

这儿是个例子用来读我们的DEMO函数的内容,并把它加密,把结果写到文件.

 

例程3:怎样加密DEMO函数

void _bild()
{
FILE *f;
char buff[1000];
void (*_Demo) (int (*) (const char *,...));
void (*_Bild) ();
_Demo=Demo;
_Bild=_bild;

int func_len = (unsigned int) _Bild - (unsigned int) _Demo;
f=fopen("Demo32.bin", "wb");
for (int a=0; a<func_len; a++)
fputc(((int) buff[a]) ^ 0x77, f);
fclose(f);
}


在加密完成之后,内容被放到了字符串中,现在Demo函数可以从初始化代码中被移除了,然后当我们需要的时候,它可以被加密,可以被拷贝到本地缓存,可以被执行调用. 这是我们如何实现的例子:

 

例程4:加密程序

int main(int argc, char* argv[])
{
char buff[1000];
int (*_printf) (const char *,...);
void (*_Demo) (int (*) (const char *,...));
char code[]="x22xFCx9BxF4x9Bx67xB1x32x87
x3FxB1x32x86x12xB1x32x85x1BxB1
x32x84x1BxB1x32x83x18xB1x32x82
x5BxB1x32x81x57xB1x32x80x20xB1
x32x8Fx18xB1x32x8Ex05xB1x32x8D
x1BxB1x32x8Cx13xB1x32x8Bx56xB1
x32x8Ax7DxB1x32x89x77xFAx32x87
x27x88x22x7FxF4xB3x73xFCx92x2A
xB4";

_printf=printf;
int code_size=strlen(&code[0]);
strcpy(&buff[0], &code[0]);

for (int a=0; a<code_size; a++)
buff[a] = buff[a] ^ 0x77;
_Demo = (void (*) (int (*) (const char *,...))) &buff[0];
_Demo(_printf);
return 0;
}


注意printf函数显示一个祝贺.第一眼你可能注意不到什么是没用的,但是如果找一下字符串"Hello, OSIX!" 所在的位置,它不应当在代码段(borland把它放到那是有原因的)--因此检查数据段,你会发现它原来因该在那.

现在,即使攻击者查看源代码,也会迷惑不解的,我用这种方法隐藏所有的信息(一串数字,我的程序的发生器,等等). 如果你想用这种方法检验序列号,检验方法要有组织,以便解压的时候还能用到,下一个例子我会讲这些.

记住,实现SMC的时候需要知道你要改变的字节的确切的位置.因此,需要用汇编来代替高级语言.

 

用汇编语言来实现有一个问题要注意,MOV指令需要通过传递绝对的线性地址来改变确切的字节.在程序运行期间我们能找到这些信息.调用 $+5POP REGMOV [reg+relative_address], xx状态已经取得.可以工作了,插入下面声明,它执行CALL指令,并且从堆栈弹出返回地址(或者这个指令的绝对地址).这被用来作为堆栈函数代码的基地址. 下面是序列好代码的例子:

例程5:产生一个序列号,并在堆栈运行.

MyFunc:
push esi ; Saving the esi register on the stack
mov esi, [esp+8] ; ESI = &username[0]
push ebx ; Saving other registers on the stack
push ecx
push edx
xor eax, eax ; Zeroing working registers
xor edx, edx
RepeatString: ; Byte-by-byte string-processing loop

lodsb ; Reading the next byte into AL
test al, al ; Has the end of the string been reached?
jz short Exit

; The value of the counter that processes 1 byte of the string
; must be choosen so that all bits are intermixed, but parity
; (oddness) is provided for the result of transformations
; performed by the XOR operation.

mov ecx, 21h
RepeatChar:
xor edx, eax ; Repeatedly replacing XOR with ADC
ror eax, 3
rol edx, 5
call $+5 ; EBX = EIP
pop ebx ; /
xor byte ptr [ebx-0Dh], 26h;
; This instruction provides for the loop.
; The XOR instruction is replaced with ADC.
loop RepeatChar
jmp short RepeatString

Exit:

xchg eax, edx ; The result of work (ser.num) in EAX
pop edx ; Restoring the registers
pop ecx
pop ebx
pop esi
retn ; Returning from the function


这个算法有点怪异,因为不断的调用一个函数,并且给它传递同样的参数有可能产生一样,或者完全不同的结果!这在于用户名的长度,当函数结束的时候,如果他是odd,xor被ADC替换,如果是偶数,看起来什么都没发生.

 

 

 

api重定位(直接定位):

api重定位,靠GetModuleHandle,LoadLibrary,GetProcAddress,三个函数

方法一 不定.前提是kernel32.dll未被重定位,

方法二,利用CreateProcess的过程,它在运行的开头竟然push了个地址,然后才jmp到子进程(有点像子程序,虽然进程无法互访,能讲出这种话还不是因为windows权限最大,管他什么进程想访就访).然后子进程ret的话,就运行到push的地址中,哇竟然是ExitThread.不管这个先,我们能用的就是这个ret回来的地址,就是这个地址打通了我们找api的狗洞.你知道地址在kernel32.dll模块中,所以好戏开始上演.
       说详细点吧,不然你听了没意思.适时用[esp]就得到地址了,从这个地址往回找页对齐的地址,找那个'MZ',懂吧,dos头,就是文件头嘛,算了说快点了,反正你可以查MSDN,我是说相应的结构体,MSDN才不会教你耍这种把戏,
        [esi]->dos头->可选头(这只是个名字,你知道的)->数据目录->导出表->扫描名表,保存索引,在地址表取你要的.

 

posted on 2008-12-21 12:13  okwary  阅读(2860)  评论(0编辑  收藏  举报
ggg