纯nasm32位汇编下实现MySQL查询系统

192 阅读7分钟

前言

本文介绍的是在纯32位汇编下实现MySQL查询,并通过HTTP返回给客户端,本文设计的知识点不多,分别如下:

  1. nasm 32位汇编语法
  2. http请求、响应报文
  3. mysql认证、查询、响应报文
  4. linux下系统调用
  5. 基础的socket通信

下面分别一一简单介绍

NASM

汇编语言就不介绍了,nasm也是汇编器的一种,其他的还有masn,它针对windows,还有gas,本文使用的nasm可以在linux上汇编,也可以在windows上汇编,由于在linux上写比较简单,所以本文最终实现是在linux上,只要有一个linux环境+mysql环境就可以运行。

下面介绍一个hello nasm的例子。

nasm汇编基本格式通常由以下几部分组成:

  • 数据段:用于声明带有初始化值的数据元素
  • bss段:用于声明零值初始化的数据元素
  • 文本段:写代码的的地方

section .data
    hello_nasm db "hello nasm", 0

section .bss


section .text
    global _start


_start:
  mov eax,0x4
  mov ebx,1
  mov ecx,hello_nasm
  mov edx,10
  int 0x80


通过以下命令即可运行

nasm  -g   -f elf32 enter.asm &&  ld    -m elf_i386  enter.o                           
/a.out
hello nasm zsh: segmentation fault (core dumped)

运行后,虽然输出了hello nasm,但是同时伴随着一个错误,这是因为程序没有正确退出导致的,在linux下,程序要正确退出,需要执行sys_exit系统调用,下面会介绍linux的系统调用。

除此之外,还需要了解函数的调用、栈的管理。

函数调用使用 call 关键字,它会将当前指令的下一条地址(也称为返回地址)压入栈中,然后跳转到指定的目标函数地址执行。当目标函数执行完成后,通过 ret 指令将栈顶的返回地址弹出并跳转回去继续执行,所以在目标函数中,不能瞎push,如果有push,在ret前面,一定要pop(出栈),或者进行add esp n,其中n是所有 push操作的字节总数,总之要注意栈平衡,否则ret后跳转的内存错误,程序也就崩溃了。

通常实现的时候会有这样一种规范。

fun_test:
   push ebp
   mov ebp,esp
   sub esp,12
   
   .....
   
   leave
   ret

这种规范是典型的 函数调用栈帧管理 模式,称为 标准调用约定。当然不遵循也可以。

它的主要作用是为函数分配独立的栈帧,便于管理局部变量和保存调用环境,随便反编译一些程序,查看其中的汇编,都会看到这样的身影。这种方式会更清晰的管理栈,通过 ebpesp 明确划分栈帧,方便访问参数和局部变量。

下面详细介绍下这几个指令:

  • push ebp
    将调用者的栈帧指针(ebp)压入栈中,保存当前函数的调用环境。这样,在函数返回时可以恢复到调用者的栈帧。

  • mov ebp, esp
    将当前栈指针(esp)的值赋给栈帧指针(ebp),建立当前函数的栈帧基地址。之后,通过 ebp 可以访问函数的参数和局部变量。

  • sub esp, 12
    向栈中分配 12 字节的空间,用于存储局部变量。此时,esp 指向局部变量区域的底部。

  • 函数体部分
    具体函数实现的操作,例如对局部变量的操作可以通过 [ebp-4][ebp-8] 等访问,函数参数可以通过 [ebp+8][ebp+12] 等访问(调用约定决定参数的位置)。

  • leave
    恢复调用者的栈帧,等价于:

    • mov esp, ebp:将栈指针恢复到当前函数栈帧基地址。
    • pop ebp:弹出调用者的栈帧指针到 ebp 中。
  • ret
    从栈中弹出返回地址并跳转到该地址,恢复到调用者代码的执行位置。

HTTP报文

http报文部分是本文最简单的一部分,简单说就是http请求发送给后端的完整数据,和后端响应给http客户端的完整数据,通常在框架的加持下,我们只关心请求体、请求参数、响应体部分,其他的数据由框架为我们拼接。

如下,这是http请求报文的完整格式

image.png

这个是http响应报文格式

image.png

在本系统中,我们不需要解析所有请求数据,只要有请求进来,就返回给查询到的数据即可。

MySQL协议

这是本文的第二难点,需要参考MySQL协议的格式,解析出数据,其中最难的是加密部分,由于MySQL 8 默认的认证方式是 caching_sha2_password,这个方式涉及到了SHA-256 的加密算法,也就是说,要使用汇编实现一个加密算法,当然可以改成mysql_native_password,这个加密比较简单,但是既然都用汇编写了,那就难度直接拉满,使用caching_sha2_password。

MySQL通信分为5部分,下面使用Wireshark抓包的结果。

  1. 客户端连接到MySQL服务器,MySQL会先返回一些服务器信息、加密方式、还有随机数种子(用于加密)。

image.png 3. 客户端通过SHA-256加密密码,还有用户名信息,发送给MySQL服务器,当然这里还不单单是对密码加密,还要和上一步返回的随机数进行xor。最终的算法是这样的。

XOR(SHA256(password), SHA256(SHA256(SHA256(password)) + 随机数))

image.png 5. 如果MySQL服务器校验通过,会返回一个Ok包,并等待客户端发送SQL信息

image.png

  1. 客户端构建SQL命令,发送给服务器

image.png 9. 服务器返回查询到的字段、行信息给客户端,客户端按照约定的格式解析即可

image.png

当然在实际中,还要有许多细节要处理, 比如单元格数据超过255,又是另外一种情况,另外要对响应的错误码做不同逻辑,这些小细节在本文会全部忽略。

linux系统调用

本系统中有大量的系统调用,系统调用就是调用linux提供的函数,比如write、read等,在本系统涉及到了socket的调用、read、write等的调用,其实本例子中大部分操作都是在读和写。

系统调用触发是使用int 0x80,具体调用的功能号放在eax中,比如4就是输出,另外的参数放在ebx、ecx、edx、esi、 edi,但是可以发现,最大的数量就是6个参数,如果超过6个,可以使用ebx寄存器保存指向输入参数的内存位置,按照参数的连续的顺序存储即可。

关于系统调用号,可以在这里查看

github.com/torvalds/li…

如果要查看参数,可以直接在linux库中搜索,比如sys_write的参数。

image.png

socket通信

在汇编下实现socket的创建、读写比较简单,难得部分是解析数据,拼接数据,汇编没有像高级语言那种"h"+"e"+"l"+"l"+"o"就可以拼接出"hello",如果在汇编中写的话,可能需要几十行。

socket server创建流程大家应该也熟悉,就是create socket、bind、listener、accept,然后对客户端的描述符进行read、write。

代码实现

大致的流程是,创建服务器socket,然后等待客户端的连接,如果有请求,则连接到mysql,执行select查询后,将拼接的数据输出到客户端描述符号中。

效果如下,将从数据库里面通过sql select * from user.users;查询所有的用户名并且返回

image.png

代码过长不宜展示,移步到github: github.com/houxinlin/n…

程序默认启动的端口是8080,并且连接127.0.0.1:3306的mysql

运行方式:

nasm -f elf32 server.asm 
ld -m elf_i386  server.o
./a.out

curl  http://localhost:8080