Reverse

Binary 103: Linux 64-bit Assembly

category
Reverse
date
Mar 6, 2021
slug
binary-103-linux-64-bit-assembly
author
status
Public
tags
binary analysis
reverse
summary
Cơ bản về x86_64 Assembly trên Linux, cấu trúc tệp ELF64.
type
Post
thumbnail
updatedAt
Mar 4, 2023 01:34 AM
Phần này giới thiệu cho bạn đọc những kiến thức cơ bản về x86_64 Assembly trên Linux, nhìn chung không khác biệt quá nhiều so với x86 Assembly. Điểm khác biệt dễ nhận thấy nhất ở x86_64 Assembly là về số lượng các thanh ghi, độ rộng thanh ghi và quá trình thực hiện System Calls, tất cả sẽ được trình bày trong bài này.

1. Các thanh ghi trong x86_64 Assembly

Các thanh ghi trong x86_64 Assembly là sự mở rộng của x86 Assembly từ 32-bits lên 64-bits và các thanh ghi này hoạt động tương tự các thanh ghi 32-bits, khi cần thiết chúng đều có thể được chia nhỏ thành các thanh ghi con 32-bits, 16-bits và 8-bits.
  • Nhóm thanh ghi chung: Mở rộng lên 64-bits, vai trò của các thanh ghi không thay đổi, vẫn sẽ có các thanh ghi: RAX, RBX, RCX, RDX, RSI, RDI, RSP, RBP
  • Thanh ghi cờ - RFLAGS: Mở rộng lên 64-bits và 32-bits thấp của thanh ghi này vẫn hoạt động như thanh ghi cờ ở x86 Assembly.
  • Thanh ghi con trỏ lệnh - RIP: Mở rộng lên 64-bits và hỗ trợ thêm một chế độ địa chỉ mới là: RIP - Relative Addressing.
  • Nhóm thanh ghi mới ở x86_64 Assembly: 8 Thanh ghi 64-bits mới được bổ sung: R8, R9, R10, R11, R12, R13, R14, R15. Các thanh ghi này cũng chứa các thanh ghi: 32, 16, 8 bit lần lượt tương ứng hậu tố: D, W, L. Ví dụ R8 có thể chia nhỏ hơn thành R8D (32 bits), R8W (16 bits), và R8L (8 bits).
Ví dụ các thanh ghi trong x86 Assembly:
notion image
Ví dụ các thanh ghi trong x86_64 Assembly:
notion image

2. Các lệnh thường gặp trong x86_64 Assembly

Các lệnh thường gặp trong x86_64 Assembly hầu hết đều giống với x86 Assembly trình bày ở phần trước. Điểm khác biệt là độ rộng của thanh ghi được tăng lên 64-bits, RIP hỗ trợ Relative addressing. Một số ví dụ:
Ví dụ lệnh MOV
mov rax,rbx mov rcx,0x1122334455667788 mov dl,0x11 mov rax,[r8]
Ví dụ lệnh IC, DEC
inc eax inc rdx inc al inc [ax] dec ebx dec rbx dec bl dec [bx]
Ví dụ lệnh ADD, SUB, MUL, DIV
add ebx,eax add bx,ax add rax,rbx add cl,0x2 sub edx,ecx sub dx,cx sub rdx,rcx sub cl,0x2 mul rdi mul bx mul cl mul 0x1122334455667788 div bx div ecx div cl
Ví dụ lệnh LEA và XCHG
lea rax,[rcx+8] xchg rdi,rsi
Ví dụ lệnh XOR, AND, OR
xor rax,rax and rbl,al or bx,bx or cx,0xfff
Ví dụ lệnh PUSH và POP
push rdi pop r12

3. x86_64 Assembly System Calls trên Linux

Khi thực hiện một System Call trong x86_64 Assembly sẽ không còn sử dụng NGẮT (INT 0X80) như trước nữa, thay vào đó nó sử dụng lệnh SYSCALL. Quy định về các thanh ghi lưu các tham số cũng khác so với x86 Assembly.
Tra cứu các System Call Number của x86_64 Assembly trong tệp: /usr/include/x86_64-linux-gnu/asm/unistd_64.h
$ cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h #ifndef _ASM_X86_UNISTD_64_H #define _ASM_X86_UNISTD_64_H 1 #define __NR_read 0 #define __NR_write 1 #define __NR_open 2 #define __NR_close 3 ...
Vẫn sử dụng Man Page để tra cứu cách sử dụng một API. Ví dụ với hàm read có System Call Number là 1
$ man 2 read
Kết quả cho biết hàm nhận vào 3 tham số như dưới đây:
READ(2) Linux Programmer's Manual READ(2) NAME read - read from a file descriptor SYNOPSIS #include <unistd.h> ssize_t read(int fd, void *buf, size_t count); ...
Các Parameter truyền vào khi gọi hàm tuân theo quy tắc sau đây:
Ta có “công thức” cần nhớ:
💡
- x86_64 Assembly thực hiện System Call thông qua lệnh: SYSCALL - Thanh ghi RAX/EAX/AX sẽ lưu System Call NumberResult của System Call. - Các tham số theo thứ tự sau: RDI, RSI, RDX, R10, R8, R9 - Tra cứu các System Call Number tại: unistd_64.h - Tra cứu các API bằng Man Page của Linux

4. Phân tích một chương trình x86_64 Assembly đơn giản

Source code:
1 ; ch03_helloworld64.asm 2 3 global _start 4 section .text 5 6 _start: 7 ; __NR_write 1 8 ; ssize_t write(int fd, const void *buf, size_t count); 9 xor rax,rax 10 xor rdi,rdi 11 xor rsi,rsi 12 xor rdx,rdx 13 xor r14,r14 14 xor r15,r15 15 inc rax 16 inc rdi 17 mov r14,0x00000a21646c726f 18 mov r15,0x57202c6f6c6c6548 19 push r14 20 push r15 21 mov rsi,rsp 22 mov dl,0xf 23 syscall 24 25 ; __NR_exit 60 26 ; void _exit(int status); 27 xor rax,rax 28 xor rdi,rdi 29 mov al,0x3c 30 syscall
Biên dịch, liên kết và chạy chương trình:
$ nasm -f elf64 -o ch03-helloworld64.o ch03-helloworld64.asm $ ld -o ch03-helloworld64 ch03-helloworld64.o $ chmod +x ch03-helloworld64 $ ./ch03-helloworld64 Hello, World!
Giải thích chi tiết:
  • Dòng 9, 10, 11, 12, 13, 14: khởi tạo giá trị 0 cho các thanh ghi RAX, RDI, RSI, RDX, R14, R15
  • Dòng 15: RAX = 0x1 ⇒ Tra System Call Number trong unistd_64.h ta được: #define __NR_write 1. Hàm write trong Man Page: ssize_t write(int fd, const void *buf, size_t count); sẽ nhận vào 3 tham số.
  • Dòng 16: RDI = 0x1 ⇒ Đây là tham số đầu tiên của hàm write. Ta có các hằng số định nghĩa File Descriptor như sau: 0=STDIN, 1=STDOUT, 2=STDERR. Vậy trường hợp này fd=STDOUT.
  • Dòng 17, 18: Sao chép dữ liệu dạng Hexa vào các thanh ghi R14, R15
  • Dòng 19, 20: Đẩy dữ liệu của R14, R15 lên Stack. Dựa theo Little-Endian ta decode dữ liệu này như sau:
    • $ python >>> a = '00000a21646c726f'.decode('hex') >>> b = '57202c6f6c6c6548'.decode('hex') >>> final = a + b >>> final[::-1] # Little-Endian, Reverse bytes 'Hello, World!\n\x00\x00'
  • Nhớ lại chương trình x86 Assembly phần trước, chương trình phải đẩy 4 lần dữ liệu lên Stack trong khi với x86_64 Assembly chỉ với 2 lần. Lý do vì độ rộng của vùng nhớ trên Stack lúc này tăng từ 32-bits lên 64-bits.
  • Dòng 21: RSI lúc này trỏ vào đỉnh Stack, tức là đang trỏ đến chuỗi: 'Hello, World!\n\x00\x00' ⇒ Vậy tham số thứ 2 của hàm write: *buf='Hello, World!\n\x00\x00'
  • Dòng 22: DL = 0xF ⇒ RDX = 0xF ⇒ Vậy tham số cuối cùng hàm write: count=15
  • Dòng 23: Thực hiện System Call
  • Dòng 27, 28: Khởi tạo lại giá trị 0 cho các thanh ghi RAX, RDI
  • dòng 29: AL=0x3C ⇒ RAX=0x3C ⇒ Tra cứu System call number trong unistd_64.h ta được hàm: #define __NR_exit 60. Được mô tả như sau: void exit(int status);
  • Dòng 30: Thực hiện System call với hàm exit với tham số: status=0
Tóm lại: Ta có thể chia chương trình thành 2 khối thực thi:
  • Khi đó ta được:
    • write(fd=1, *buf="Hello, World!\n\0", count=15); exit(status=0);

5. Cấu trúc tệp ELF64 trên Linux

Cấu trúc tệp ELF64 về so với ELF32 không có sự khác biệt nhiều, chỉ thay đổi một vài thông số cho phù hợp với hệ thống 64-bits. Phần này sẽ không bàn quá nhiều về cấu trúc chi tiết như phần trước về ELF32, phần này tập chung vào sự khác biệt của tệp ELF32/64 sau khi biên dịch của C mà Assembly sau khi biên dịch không có.
Nhìn chung chương trình viết bằng Assembly cho kích thước nhỏ hơn, cấu trúc tệp tinh gọn hơn, không có nhiều thông tin "thừa" đi kèm tệp:
Chương trình viết bằng Assembly có kích thước bé hơn
notion image
Chương trình viết bằng Asembly có ít thông tin hơn về thư viện, trình biên dịch
notion image
Tệp ELF của C đi kèm với rất nhiều thông tin: thư viện, compiler, symbols, strings,.v.v.. Đáng chú ý nhất là xuất hiện rất nhiều các Section được trình biên dịch thêm vào:
  • .text: Chứa code thực thi. Khi phân tích Binary chủ yếu tập chung vào Section này.
  • .bss: Chứa dữ liệu (variable) chưa đc khởi tạo giá trị. Phần này nằm trong Data Segment
  • .data: Chứa dữ liệu (variable) đã đc khởi tạo giá trị. Phần này cũng nằm trong Data Segment
  • .rodata: Chứa dữ liệu chỉ đọc (const), và nó đc sử dụng cho các Segment non-writable
  • .shstrtab: Chứa header string table, chứa tên của tất cả các Section trong tệp nhị phân
  • .symtab: Chứa mảng các tham chiếu đến các symbol dc linker và loader sử dụng
  • .strtab: Chứa bảng các chuỗi kết thúc bằng null-terminated
  • .init: Chịu trách nhiệm khởi tạo image tiến trình cho tệp ELF
  • .fini: Chịu trách nhiệm về mã kết thúc cho tiến trình
  • .plt: Chứa Procedure Linkage Table và dữ liệu chuyển hướng các hàm thư viện đến vị trí tuyệt đối của chúng trong bộ nhớ
  • .got: Có thể ghi vào đc, và nó chứa Global Offset Table, resolve các shared library data trong quá trình chạy và còn đc sử dụng với Procedure Linkage Table
  • .got.plt: Hoạt động cùng với Procedure Linkage Table, chứa địa chỉ cho các hàm đc sử dụng bởi Procedure Linkage Table trong quá trình liên kết động
Một số Segment thường gặp:
  • Text Segment: Chứa một số section như: .text, .rodata, .hash, .dynsym, .dynstr, .plt, .rel.got
  • Data Segment: Có thể ghi vào đc, chứa một số section như: .data, .dynamic, .got, .bss
 
ReadELF với chương trình viết bằng C:
Section .rela.plt và bảng .dynsym cho biết chương trình có dùng hàm printf của thư viện GLIBC_2.2.5
notion image
 
Bảng .symtab chứa rất nhiều symbol
notion image
Nó cũng cho biết symbol printf được sử dụng với type là FUNC
notion image