汇编Assembly


融合了一些自己之前的笔记

比较流行的汇编语言有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):

  • 传递参数时,按照从左到右的顺序,将尽可能多的参数依次保存在寄存器中。存放位置的寄存器顺序是确定的:
    • 对于整数和指针,rdirsirdxrcxr8r9
    • 对于浮点数(float 和 double 类型),xmm0xmm1xmm2xmm3xmm4xmm5xmm6xmm7
  • 剩下的参数将按照从右到左的顺序压入栈中,并在调用之后 由调用函数推出栈

在进入到另一个函数地址时,会首先进行一个开栈操作,至于开多少取决于你的函数代码块有多少和编译器

如果手写汇编的话就需要自己手动计算需要开辟的栈空间大小

为了更好看清楚栈空间的结构,这里使用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:00040xffffd50c —▸ 0x56556273 (__libc_csu_init+83) ◂— add    esi, 1
02:00080xffffd510 ◂— 0x1
03:000c│     0xffffd514 ◂— 0x1
04:0010│ ebp 0xffffd518 —▸ 0xffffd538 ◂— 0x0
05:00140xffffd51c —▸ 0x56556208 (main+45) ◂— add    esp, 8
06:00180xffffd520 ◂— 0x4
07:001c│     0xffffd524 ◂— 0x2

执行leave指令后

00:0000│ esp 0xffffd51c —▸ 0x56556208 (main+45) ◂— add    esp, 8
01:00040xffffd520 ◂— 0x4
02:00080xffffd524 ◂— 0x2
03:000c│     0xffffd528 ◂— 0x0
04:00100xffffd52c ◂— 0x0
05:00140xffffd530 ◂— 0x1
06:00180xffffd534 ◂— 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

调试:

  1. 首先来看看进入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个

  1. 然后看看此时的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

  1. 可以看到在main函数开始时,进行了2个push操作,这是为了保存main函数的2个参数地址,因为在调用call puts的时候必须要保证rdi为参数二的字符串值,在进入puts函数的子函数栈空间后,同时也会操作rdi的值,这个时候如果没有在调用puts函数之前保存地址的话,那么就会发生指针丢失的问题,从而产生各种可能的错误
  2. 调用puts函数后进行了一个恢复参数值的操作,从而方便再次进入到main函数,进行一个相同的操作
  3. 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

猜你喜欢

转载自blog.csdn.net/csdn546229768/article/details/128728814