汇编Assembly
文章目录
- 比较流行的汇编语言有3种:
- 不同风格的汇编语言在语法格式上会有不同:
- 实战代码:
- Intrinsic函数
- 手写汇编(8086汇编)
- 调用C的API库
- 函数调用约定
- 实际代码
- C调用汇编函数进行计算
- 纯C实现如下:
- C+ASM实现:
- 纯ASM实现:
- ASM打印命令行参数
- 数据段存储
融合了一些自己之前的笔记
比较流行的汇编语言有3种:
- NASM风格的Intel汇编语言(x86\64)
- AT&T风格的GAS汇编语言(特点是寄存器前面有%号)(Arm)
- Windows风格的汇编语言
不同风格的汇编语言在语法格式上会有不同:
- GAS(GNU Assembler)
- NASM(Netwide Assembler)
- MASM(Microsoft Macro Assembler)
descript | GAS | NASM | MASM |
---|---|---|---|
寄存器 | push %eax | push eax | |
立即数 | push $1 | push 1 | |
给寄存器赋值1 | mov $1,%eax | mov eax,1 |
实战代码:
AT&T
风格
000000000040056a <add>:
40056a: 55 push %rbp
40056b: 48 89 e5 mov %rsp,%rbp
40056e: 89 7d ec mov %edi,-0x14(%rbp)
400571: 89 75 e8 mov %esi,-0x18(%rbp)
400574: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
40057b: 8b 55 ec mov -0x14(%rbp),%edx
40057e: 8b 45 e8 mov -0x18(%rbp),%eax
400581: 01 d0 add %edx,%eax
400583: 89 45 fc mov %eax,-0x4(%rbp)
400586: 8b 45 fc mov -0x4(%rbp),%eax
400589: 5d pop %rbp
40058a: c3 retq
40058b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
Intel
风格
000000000040056a <add>:
40056a: 55 push rbp
40056b: 48 89 e5 mov rbp,rsp
40056e: 89 7d ec mov DWORD PTR [rbp-0x14],edi
400571: 89 75 e8 mov DWORD PTR [rbp-0x18],esi
400574: c7 45 fc 00 00 00 00 mov DWORD PTR [rbp-0x4],0x0
40057b: 8b 55 ec mov edx,DWORD PTR [rbp-0x14]
40057e: 8b 45 e8 mov eax,DWORD PTR [rbp-0x18]
400581: 01 d0 add eax,edx
400583: 89 45 fc mov DWORD PTR [rbp-0x4],eax
400586: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]
400589: 5d pop rbp
40058a: c3 ret
40058b: 0f 1f 44 00 00 nop DWORD PTR [rax+rax*1+0x0]
- 使用汇编编写功能函数(使用GNU-Gcc编译,因为clang会无法通过语法检查):
//gcc main.c -masm=intel
#include<stdio.h>
//intel 64 中参数一为rdi \参数二为rsi \返回值为rax
//pop rbp为收栈准备返回main函数地址
int info(int x,int y){
asm(
"add rdi,rsi;"
"mov rax,rdi;"
"pop rbp;"
"ret;"
);
}
int main(){
printf("%d\n",info(1,1));
return 0;
}
Intrinsic函数
是编译器提供的函数接口,调用Intrinsic函数可以达到代替汇编的作用
TODO
手写汇编(8086汇编)
demo.asm
环境必须是Linux Intel 64系统
; nasm -felf64 demo.asm && ld demo.o && ./a.out
global _start
section .text
_start:
;write(1,message,13)
mov rax,1
mov rdi,1
mov rsi,message
mov rdx,13
syscall
;exit(0)
mov eax,60
xor rdi,rdi
syscall
message:
db "Hi~",0xa
db "Hi~~",0xa
db "Hi~~~",0xa
message2:
data1 db 'hello'
data2 db ' world'
编译并运行:
~ $ nasm -felf64 demo.asm && ld demo.o && ./a.out
Hi~
Hi~~
Hi~~~
-f elf64
将源文件编译为64位的elf文件ld
将目标文件连接为可执行程序
代码解释:
-
;
: 代码注释的符号,表示这一行代码注释 -
global _start
: 定义全局函数 -
section .text
: 表示下面的代码段为.text
段,在C语言中你编写的代码段最终会通过编译器变为汇编代码放在.text
段 -
_start:
: 表示一个函数的开始,声明函数域,C语言中就是由_start
函数开始初始化程序,然后再调用熟悉的main
函数 -
mov rax,1
: 当rax为1时 执行下面syscall
等同于执行write()函数,其中原理就是系统调用,比如你将rax为2时,则会调用open()函数 -
mov rdi,1 mov rsi,message mov rdx,13
这一块就是构造函数调用时的参数,上面我声明了必须是64bit的系统,因为rdi、rsi、rdx就是64bit ELF文件中的常用寄存器
64bit ELF文件中调用函数时会将参数从左到右放入寄存器,寄存器分别为(顺序): rdi, rsi, rdx, rcx, r8, r9。
可以注意到这里只有6个寄存器,一旦超过6个参数时就会按照32bit ELF文件的规则将参数“从右至左”依次压入栈空间,进行传递
那么这里就很容易理解了其实就是构造了3个参数(1,message,13),这里的message就是一个字符串常量的地址
syscall
: 执行系统调用。即调用函数write(1,"Hi~",13)
exit(0)
: 也是一样的方式调用,(xor就是异或操作即:rdi = rdi ^ rdi = 0)message
: 表示数据标签段db
: 定义字节类型的数据,表示其后的数据都是字节型数据,后面的,0xa
表示换行0xa就是\n的ASCLL码值
调用C的API库
;nasm -felf64 demo.asm && gcc demo.o -no-pie -static && ./a.out
global main
extern puts
section .text
main:
mov rdi, message
call puts
ret
message:
db "hello", 0
因为是使用gcc链接器那么就只需要写一个main
函数入口,如果还有个_start
函数的话就会有冲突错误
注意:这里的编译需要关闭PIE保护(动态地址)才能编译成功,同时需要开启静态编译才能防止报段错误,具体可以用gdb开查看实现
比如:我关闭了PIE保护和开启了静态编译,那么我的main函数地址地址每次都会是0x4016c0
,并且puts函数的地址也被程序正确的找到
────────────────────────────────────────────────────── code:x86:64 ────
0x4016b5 <frame_dummy+53> cs nop WORD PTR [rax+rax*1+0x0]
0x4016bf <frame_dummy+63> nop
0x4016c0 <main+0> movabs rdi, 0x4016d0
→ 0x4016ca <main+10> call 0x40c080 <puts>
↳ 0x40c080 <puts+0> endbr64
0x40c084 <puts+4> push r13
0x40c086 <puts+6> push r12
0x40c088 <puts+8> mov r12, rdi
0x40c08b <puts+11> push rbp
0x40c08c <puts+12> push rbx
────────────────────────────────────────────── arguments (guessed) ────
puts (
$rdi = 0x000000004016d0 → 0x2e66006f6c6c6568 ("hello"?),
$rsi = 0x007fff184836e8 → 0x007fff184856d5 → "/home/hi/a.out",
$rdx = 0x007fff184836f8 → 0x007fff184856e4 → "SHELL=/bin/bash",
$rcx = 0x00000040004000
)
函数调用约定
在操作函数跳转时栈无疑是一个非常重要的通道,而决定栈空间数据使用顺序声明的就是函数的调用约定声明了,存在如下常用调用约定:
TODO:还需要搞清楚其他的调用约定用法,做好是有代码演示
stdcall (pascal) 主要用于Microsoft C++系列
cdecl (默认C语言调用约定)
fastcall
thiscall
naked call
在默认C语言调用约定中(64bit):
- 传递参数时,按照从左到右的顺序,将尽可能多的参数依次保存在寄存器中。存放位置的寄存器顺序是确定的:
- 对于整数和指针,
rdi
,rsi
,rdx
,rcx
,r8
,r9
。 - 对于浮点数(float 和 double 类型),
xmm0
,xmm1
,xmm2
,xmm3
,xmm4
,xmm5
,xmm6
,xmm7
。
- 对于整数和指针,
- 剩下的参数将按照从右到左的顺序压入栈中,并在调用之后 由调用函数推出栈
在进入到另一个函数地址时,会首先进行一个开栈操作,至于开多少取决于你的函数代码块有多少和编译器
如果手写汇编的话就需要自己手动计算需要开辟的栈空间大小
为了更好看清楚栈空间的结构,这里使用32bit程序来看效果:
- 比如在main函数调用子函数时:
-
处理完成调用参数后会执行
call 0xxxxxx
的指令 -
call会在调用函数时将eip压入栈,也就是将下一条指令的地址赋值给esp,这样就可以通过ret指令进行返回原函数继续执行了
-
- 然后到了子函数的地址处时:
-
会执行先保存当前的栈底(ebp)值,然后分别将
ebp、esp
压入栈,通过sub esp,0xxxxx
的方式来扩展栈空间 -
之后就是取main函数在调用子函数时的压入的参数了
-
- 子函数返回时:
- 会通过执行
leave
指令其实就是:-
mov esp , ebp
来关闭开辟的栈空间,然后pop ebp
移动到上个函数的位置 -
这时ebp就是函数入口时push上个函数的ebp的值,最后根据这个值进行恢复
-
- 再执行
ret
指令进行地址跳转,而ret
指令就是:-
pop eip
将堆栈段中当前SS:SP所指的字内容弹出到某个寄存器,也就是将sp的值赋值给eip,eip就是下一条指令地址 -
对应arm的pc寄存器,这就对应了call指令时压入栈的ip地址
-
- 会通过执行
实际代码
执行leave
指令前
00:0000│ esp 0xffffd508 —▸ 0x56558fdc (_GLOBAL_OFFSET_TABLE_) ◂— 0x3ee4
01:0004│ 0xffffd50c —▸ 0x56556273 (__libc_csu_init+83) ◂— add esi, 1
02:0008│ 0xffffd510 ◂— 0x1
03:000c│ 0xffffd514 ◂— 0x1
04:0010│ ebp 0xffffd518 —▸ 0xffffd538 ◂— 0x0
05:0014│ 0xffffd51c —▸ 0x56556208 (main+45) ◂— add esp, 8
06:0018│ 0xffffd520 ◂— 0x4
07:001c│ 0xffffd524 ◂— 0x2
执行leave
指令后
00:0000│ esp 0xffffd51c —▸ 0x56556208 (main+45) ◂— add esp, 8
01:0004│ 0xffffd520 ◂— 0x4
02:0008│ 0xffffd524 ◂— 0x2
03:000c│ 0xffffd528 ◂— 0x0
04:0010│ 0xffffd52c ◂— 0x0
05:0014│ 0xffffd530 ◂— 0x1
06:0018│ 0xffffd534 ◂— 0x2
07:001c│ ebp 0xffffd538 ◂— 0x0
intel架构默认调用约定组成结构:
- 32位无参数:
payload = b'a'* 0x88 + b'b' * 0x4 + p32(backdoor) +p32(main_addr)
- 32位有参数:"函数地址+返回地址+参数
payload = b'a'* 0x88 + b'b' * 0x4 + p32(backdoor) + p32(main_addr) + p32(bin_sh)//完整的rop链返回地址main
- 64无参数:
payload = b'a'* 0x88 + b'b' * 0x8 + p64(backdoor)
- 64有参数:"函数地址+参数+返回地址”
paypyload = b'a'* 0x88 + b'b' * 0x8 + p64(pop_edi) + p64(bin_sh) + p64(backdoor)
- 64无System泄露:
#第一次加载payload进行libc真实地址获取
payload = b'a' * 0x50 + b'b' * 0x8 + p64(prdi) + p64(e.got['puts']) + p64(e.plt['puts']) + p64(返回函数地址)
#重新构造payload
payload = b'a' * 0x50 + b'b' * 8
for i in range(1):
payload += p64(rtn_addr)
payload += p64(prdi) + p64(libc_addr + lic.dump("str_bin_sh")) + p64(libc_addr + libc.dump("system"))
C调用汇编函数进行计算
特点环境下说使用simd的一些优化操作,就需要使用simd指令集来操作数据,从而实现快速计算,那么此时的汇编代码块就起到了一个处理数据集功能的作用
纯C实现如下:
#include<stdio.h>
#include<time.h>
int Max(int a,int b,int c){
int ret = a;
if(ret<b) ret=b;
if(ret<c) ret=c;
return ret;
}
int main(){
srand(0x100); //为了方便ASM 和 C代码分别实现的demo运行结果对比,这里使用一样的随机种子
for (int i = 0; i < 10; ++i) {
int a = rand() , b = rand(), c = rand();
printf("{%d,%d,%d} Max=%d \n",a,b,c,Max(a,b,c));
}
return 0;
}
结果:
~ $ gcc main.c && ./a.out
{1557381903,485784087,974190345} Max=1557381903
{909832560,185226890,4869305} Max=909832560
{842916993,1066023196,370971114} Max=1066023196
{1378714000,834802215,875669745} Max=1378714000
{419994512,459245563,1733189616} Max=1733189616
{259238441,2032537841,1291879760} Max=2032537841
{1977168301,1893959658,2072065736} Max=2072065736
{1802926432,786500781,937118081} Max=1802926432
{1567600346,303276252,249486295} Max=1567600346
{2134477068,1322435152,1593906562} Max=2134477068
C+ASM实现:
demo.asm
global Max
section .text
Max:
mov rax,rdi
cmp rax,rsi
cmovl rax,rsi
cmp rax,rdx
cmovl rax,rdx
ret
main.c
#include<stdio.h>
#include<time.h>
int Max(int a,int b,int c);
int main(){
srand(0x100);
for (int i = 0; i < 10; ++i) {
int a = rand() , b = rand(), c = rand();
printf("{%d,%d,%d} Max=%d \n",a,b,c,Max(a,b,c));
}
return 0;
}
结果:
~ $ nasm -f elf64 demo.asm && gcc main.c demo.o && ./a.out
{1557381903,485784087,974190345} Max=1557381903
{909832560,185226890,4869305} Max=909832560
{842916993,1066023196,370971114} Max=1066023196
{1378714000,834802215,875669745} Max=1378714000
{419994512,459245563,1733189616} Max=1733189616
{259238441,2032537841,1291879760} Max=2032537841
{1977168301,1893959658,2072065736} Max=2072065736
{1802926432,786500781,937118081} Max=1802926432
{1567600346,303276252,249486295} Max=1567600346
{2134477068,1322435152,1593906562} Max=2134477068
纯ASM实现:
ASM打印命令行参数
global main
extern puts
section .text
main:
push rdi ;保存参数一
push rsi ;保存参数二
;sub rsp,8 ;实际在操作的时候在进入main函数时,因为使用的程序是64bit,可能需要手动的修复rbp的值,不然会导致栈空间数据错乱
mov rdi ,[rsi]
call puts
;add rsp,8 ;回收栈
pop rsi ;取出原参数值
pop rdi ;取出原参数值
add rsi,8
dec rdi
jnz main
ret
运行:
~ $ nasm -f elf64 demo.asm && gcc demo.o -static && ./a.out 1 2 3
./a.out
1
2
3
调试:
- 首先来看看进入main函数时的寄存器值
$rax : 0x000000004016c0 → <main+0> push rdi
$rbx : 0x007ffeccff0100 → 0x007ffeccff06e4 → "SHELL=/bin/bash"
$rcx : 0x154000
$rdx : 0x007ffeccff0100 → 0x007ffeccff06e4 → "SHELL=/bin/bash"
$rsp : 0x007ffeccfefef8 → 0x00000000401b0a → <__libc_start_call_main+106> mov edi, eax
$rbp : 0x1
$rsi : 0x007ffeccff00d8 → 0x007ffeccff06cf → "/home/hi/a.out"
$rdi : 0x4
$rip : 0x000000004016c0 → <main+0> push rdi
可以看到rdi
为4表示main的第一个参数的值为4,即命令行参数有4个
- 然后看看此时的
rsi
:
gef➤ telescope $rsi
0x007ffeccff00d8│+0x0000: 0x007ffeccff06cf → "/home/hi/a.out" ← $rsi, $r13
0x007ffeccff00e0│+0x0008: 0x007ffeccff06de → 0x4853003300320031 ("1"?)
0x007ffeccff00e8│+0x0010: 0x007ffeccff06e0 → 0x4c45485300330032 ("2"?)
0x007ffeccff00f0│+0x0018: 0x007ffeccff06e2 → 0x3d4c4c4548530033 ("3"?)
0x007ffeccff00f8│+0x0020: 0x0000000000000000
刚好对应着我们传进来的参数。并且最后一个参数后面的地址被置为0
- 可以看到在main函数开始时,进行了2个push操作,这是为了保存main函数的2个参数地址,因为在调用
call puts
的时候必须要保证rdi
为参数二的字符串值,在进入puts
函数的子函数栈空间后,同时也会操作rdi
的值,这个时候如果没有在调用puts
函数之前保存地址的话,那么就会发生指针丢失的问题,从而产生各种可能的错误 - 调用puts函数后进行了一个恢复参数值的操作,从而方便再次进入到main函数,进行一个相同的操作
dec指令
表示自减1,执行结果影响AF、OF、PF、SF、ZF标志位,jne指令
ZF寄存器!=0则跳转,这里主要关注ZF标志位,来看调试:
────────────────────────────────────────────────────── code:x86:64 ────
0x4016ca <main+10> pop rsi
0x4016cb <main+11> pop rdi
0x4016cc <main+12> add rsi, 0x8
●→ 0x4016d0 <main+16> dec rdi
0x4016d3 <main+19> jne 0x4016c0 <main>
0x4016d5 <main+21> ret
0x4016d6 <main+22> cs nop WORD PTR [rax+rax*1+0x0]
0x4016e0 <handle_zhaoxin+0> push rbx
0x4016e1 <handle_zhaoxin+1> mov eax, 0x4
在执行最后一个参数时,执行dec
指令前的flags寄存器如下:
gef➤ p $eflags
$8 = [ AF IF ]
//AF IF表示只有这两个寄存器的值为1
gef➤ x $eflags
0x212
gef➤ p $eflags
$7 = [ PF ZF IF ]
//ZF 可以看到这里的ZF寄存器值为1了,即表示dec指令执行后的值为0,那么就会在jne指令后满足不为zero寄存器不为0的条件进行
gef➤ x $eflags
0x246
具体的flag寄存器标记为如下:
value | 溢出 | 方向 | 中断 | 跟踪 | 符号 | 零 | 辅进位 | 奇偶 | 进位 | |||
---|---|---|---|---|---|---|---|---|---|---|---|---|
mark | OF | DF | IF | TF | SF | ZF | AF | PF | CF | |||
0x212 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 |
0x246 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 1 | 0 |
数据段存储
;nasm -f elf64 demo.asm && gcc demo.o -static && ./a.out 1 2
global main
;C API
extern atoi
extern printf
default rel
section .text
main:
dec rdi
jz nothingToAverage
mov [count], rdi; 保存浮点数参数的个数到bss变量
accumulate:
push rdi
push rsi
mov rdi, [rsi+rdi*8]; argv[rdi]
call atoi
pop rsi
pop rdi
add [sum], rax
dec rdi
jnz accumulate; loop遍历参数
average:
cvtsi2sd xmm0, [sum]
cvtsi2sd xmm1, [count]
divsd xmm0, xmm1; xmm0 现在值为 sum/count
mov rdi, format; printf输出格式
mov rax, 1; printf 是多参数的, 含有一个不是整数的参数
sub rsp, 8; 对齐栈指针
call printf
add rsp, 8
ret
nothingToAverage:
mov rdi, error
xor rax, rax
call printf
ret
;=============.data段===========================
section .data
count: dq 0
sum: dq 0
format: db "%g", 10, 0
error: db "There are no command line arguments to average", 10,0
~ $ nasm -f elf64 demo.asm && gcc demo.o -static && ./a.out 1 2
1.5