Skip to content

标题: 利用Keystone快速汇编并提取机器码

创建: 2018-11-07 18:03 更新: 2019-01-14 12:44 链接: https://scz.617.cn/misc/201811071803.txt

ARM汇编写相对地址的bl,已知PC和目标地址,想写个.s,利用.org之类的指示符指 定PC,然后"-c"编译,"objdump -d"查看机器码。不考虑ldr+blx。更进一步,想将 几个固定地址的片段写在同一个.s里,比如代码、数据在不同地址。不考虑位置无关 代码的自定位技巧。原始意图大致如此,但我用gcc未能得逞。

.org的第一形参是有符号整型,对于32-bits来说,0x80000000及以上的数被认为是 负数,达不到预期目的。即使让它成为无符号整型或者动用64-bits,gcc/gas的.org 指示符会实际填充,导致.o文件庞大。总之,原生.org不满足需求。

一度考虑用rasm2:

$ rasm2 -a arm -b 32 -o 0xf0e0dee0 -D "3a 06 00 eb" 0xf0e0dee0 4 3a0600eb bl 4041275344

$ rasm2 -a arm -b 32 -o 0xf0e0dee0 "bl 0xf0e0f7d0" Branch into out of range Cannot assemble 'bl 0xf0e0df70' at line 3 invalid

rasm2的反汇编正确,汇编失败,这可能与32-bits有关。nasm不支持ARM。挣扎很久 之后,bluerust推荐:

Keystone is a lightweight multi-platform, multi-architecture assembler framework https://github.com/keystone-engine/keystone

源码可以在Linux、Windows下顺利编译,自带一个kstool用于演示。

$ ./kstool Kstool v0.9.1 for Keystone Assembler Engine (www.keystone-engine.org) By Nguyen Anh Quynh, 2016-2018

Syntax: ./kstool [start-address-in-hex-format]

The following options are supported: x16: X86 16bit, Intel syntax x32: X86 32bit, Intel syntax x64: X86 64bit, Intel syntax x16att: X86 16bit, AT&T syntax x32att: X86 32bit, AT&T syntax x64att: X86 64bit, AT&T syntax x16nasm: X86 16bit, NASM syntax x32nasm: X86 32bit, NASM syntax x64nasm: X86 64bit, NASM syntax arm: ARM - little endian armbe: ARM - big endian thumb: Thumb - little endian thumbbe: Thumb - big endian armv8: ARM V8 - little endian armv8be: ARM V8 - big endian thumbv8: Thumb V8 - little endian thumbv8be: Thumb V8 - big endian arm64: AArch64 hexagon: Hexagon mips: Mips - little endian mipsbe: Mips - big endian mips64: Mips64 - little endian mips64be: Mips64 - big endian ppc32be: PowerPC32 - big endian ppc64: PowerPC64 - little endian ppc64be: PowerPC64 - big endian sparc: Sparc - little endian sparcbe: Sparc - big endian sparc64be: Sparc64 - big endian systemz: SystemZ (S390x) evm: Ethereum Virtual Machine

$ ./kstool arm "bl 0xf0e0f7d0" 0xf0e0dee0 bl 0xf0e0f7d0 = [ 3a 06 00 eb ]

上例指定了bl指令所在地址,生成的机器码是相对跳转。动用分号后可以单行输入多 条指令:

$ echo -n "push {ip, lr};mov r0, #0xff000000" | ./kstool arm push {ip, lr};mov r0, #0xff000000 = [ 00 50 2d e9 ff 04 a0 e3 ]

kstool在*nix下支持从stdin读入,Windows版不支持。从kstool.cpp看看,它曾经打 算支持多行输入,但未能真正实现。下面是段无聊的对话:


scz

处理stdin输入时,它用while/fgets,应该是试图支持多行输入。但拼装input
时并未自动追加分号,导致多行输入无效。这是未经测试的BUG?

bluerust

只能这么理解了,写到kstool时,程序员已经累了。前些天重看《系统设计》这
本书,有一处注释错了,作者说,很明显,程序员写到这里已经很累了,他写反
了这个函数的意图。

scz

这是个万能解释,不错,赞

为了利用Keystone,很简单,大致这么几步:

ks_open() 指定CPU ks_asm() 指定基址、汇编指令 printf() 输出机器码 ks_free() 释放动态分配的机器码空间 ks_close() 关闭

kstool_arm_sample.cpp如下:


include

include

include

int main ( int argc, char * argv[] ) { int ret = EXIT_FAILURE; ks_engine ks = NULL; uint64_t base = 0; char assembly; unsigned char *insn = NULL; size_t size; size_t count; size_t i;

if ( argc < 2 )
{
    printf( "%s <assembly> [base]\n", argv[0] );
    goto main_exit;
}
assembly    = argv[1];
if ( argc > 2 )
{
    base    = strtoull( argv[2], NULL, 0 );
}
if ( ks_open( KS_ARCH_ARM, KS_MODE_ARM+KS_MODE_LITTLE_ENDIAN, &ks ) )
{
    printf( "Error: failed on ks_open()\n" );
    goto main_exit;
}
if ( ks_asm( ks, assembly, base, &insn, &size, &count ) )
{
    printf
    (
        "Error: failed on ks_asm() with count = %zu, error = '%s' (code = %u)\n",
        count,
        ks_strerror( ks_errno( ks ) ),
        ks_errno( ks )
    );
}
else
{
    printf( "%016llx [ ", base );
    for ( i = 0; i < size; i++ )
    {
        printf( "%02x ", insn[i] );
    }
    printf( "] %s\n", assembly );
}
if ( NULL != insn )
{
    ks_free( insn );
    insn    = NULL;
}
ret     = EXIT_SUCCESS;

main_exit:

if ( NULL != ks )
{
    ks_close( ks );
    ks  = NULL;
}
return( ret );

} / end of main /

$ kstool_arm_sample [base]

$ ./kstool_arm_sample "bl 0xf0e0f7d0" 0xf0e0dee0 00000000f0e0dee0 [ 3a 06 00 eb ] bl 0xf0e0f7d0

bluerust友情提供一个kstoolex.exe:

https://scz.617.cn/misc/kstoolex.exe

SHA1:07b897c74415bfcff544ecf0546373cc3602749c

$ kstoolex | [|q] [base]

file为"-"时,表示从stdin读取汇编指令。这个功能只在*nix上有效,Windows版不 支持。如果file不存在,视argv[2]为assembly,即汇编指令串。

n默认为4,表示单条汇编指令生成的机器码不足4字节时用空格进行填充后显示,如 果机器码实际字节数超过n,按实际字节数显示。

q表示quiet模式,按16字节一行显示机器码,不显示地址、汇编指令。

base的优先级低于.s中的.org指示符。

src.s如下:


. = 0xf0e0ded4

push    {ip, lr}
mov     r0, #0xff000000
mov     r1, #0x370000
bl      #0xf0e0f7d0
mov     r0, #0
pop     {ip, pc}

$ kstoolex arm src.s 00000000f0e0ded4 [ ] . = 0xf0e0ded4 00000000f0e0ded4 [ 00 50 2d e9 ] push {ip, lr} 00000000f0e0ded8 [ ff 04 a0 e3 ] mov r0, #0xff000000 00000000f0e0dedc [ 37 18 a0 e3 ] mov r1, #0x370000 00000000f0e0dee0 [ 3a 06 00 eb ] bl #0xf0e0f7d0 00000000f0e0dee4 [ 00 00 a0 e3 ] mov r0, #0 00000000f0e0dee8 [ 00 90 bd e8 ] pop {ip, pc}

$ kstoolex arm src.s q 00 50 2d e9 ff 04 a0 e3 37 18 a0 e3 3a 06 00 eb 00 00 a0 e3 00 90 bd e8

$ rasm2 -a arm -b 32 -o 0xf0e0ded4 -D 00502de9ff04a0e33718a0e33a0600eb0000a0e30090bde8 0xf0e0ded4 4 00502de9 push {ip, lr} 0xf0e0ded8 4 ff04a0e3 mov r0, -0x1000000 0xf0e0dedc 4 3718a0e3 mov r1, 0x370000 0xf0e0dee0 4 3a0600eb bl 4041275344 0xf0e0dee4 4 0000a0e3 mov r0, 0 0xf0e0dee8 4 0090bde8 pop {ip, pc}

说个与ARM汇编无关与x64汇编相关的事。keystone汇编某条x64指令时(见后)生成的 机器码与IDA显示不符,我以为是BUG,唆使bluerust跟作者联系一下,然后他较了一 下真,有了后续内容。

就"mov rax, qword ptr gs:[188h]"而言,有两种编码方案:

mov rax, qword ptr gs:[dword 188h] [ 65 48 8b 04 25 88 01 00 00 ] mov rax, qword ptr gs:[qword 188h] [ 65 48 a1 88 01 00 00 00 00 00 00 ]

此处的188h是displacement,IDA、gas对此做了优化,使用32-bits displacement, ks_asm()死活使用64-bits displacement。

$ ./kstool x64 "mov rax, qword ptr gs:[0x188]" mov rax, qword ptr gs:[0x188] = [ 65 48 a1 88 01 00 00 00 00 00 00 ]

nasm语法可以对立即数指定位宽描述符:


BITS 64

mov rax, [dword gs:0x188]
mov rax, [qword gs:0x188]

$ nasm -f bin -o test.bin test.nasm $ xxd -g 1 test.bin 00000000: 65 48 8b 04 25 88 01 00 00 65 48 a1 88 01 00 00 eH..%....eH..... 00000010: 00 00 00 00 .... $ rasm2 -a x86 -b 64 -s intel -D 65488b0425880100006548a18801000000000000 0x00000000 9 65488b042588010000 mov rax, qword gs:[0x188] 0x00000009 11 6548a18801000000000000 movabs rax, qword gs:[0x188]

rasm2反汇编"mov rax, [qword gs:0x188]"时将mov显示成movabs,gas处理movabs时 使用64-bits displacement。cdb则对两种情况都显示成mov,但为了强调64-bits, 将立即数显示成"gs:[0000000000000188h]"。

eb rip 65 48 8b 04 25 88 01 00 00 65 48 a1 88 01 00 00 00 00 00 00 u rip l 2 00000000ff662ff8 65488b042588010000 mov rax,qword ptr gs:[188h] 00000000ff663001 6548a18801000000000000 mov rax,qword ptr gs:[0000000000000188h]

上面两条指令效果完全一样,但机器码不同。

keystone支持nasm语法,但支持得不完整,比如不支持立即数位宽描述符:

$ ./kstool x64nasm "mov rax, qword gs:[0x188]" mov rax, qword gs:[0x188] = [ 65 48 a1 88 01 00 00 00 00 00 00 ] $ ./kstool x64nasm "mov rax, qword gs:[dword 0x188]" ERROR: failed on ks_asm() with count = 0, error = 'Invalid operand (KS_ERR_ASM_INVALIDOPERAND)' (code = 512)

2018-11-10 17:38 bluerust

就目前看来,先说结论:

. 不好改 . 有BUG

不好改。在完成指令解析之后,遍历Opcode Table,遇到能容得下指令的Opcode就返 回,然后再"Emit inst to data"。遍历Opcode Table时,并不知道指令大小,当存 在多个匹配时,无法知道哪个最优。

有BUG。这个问题主要是没有考虑ASM的复杂性,对歧义指令没有处理好。比如

mov rax, qword [gs:0x188]

这条指令存在多种解释:

mov rax, qword [abs qword gs:0x188] mov rax, qword [abs dword gs:0x188] mov rax, qword [rel dword gs:0x188]

rel表示相对于RIP,abs表示绝对地址。llvm的assembler在此未做区分,上述指令会 命中两个Opcode:

mov rax, qword [abs qword gs:0x188] mov rax, qword [rel dword gs:0x188]

但不会命中:

mov rax, qword [abs dword gs:0x188]

这是它本身的一个BUG。假设指令为:

mov rax, qword gs:[abs 0x188]

非nasm正常语法,这是keystone语法。由于abs修饰符缺失时默认即为abs,上述指令 与下述指令等价:

mov rax, qword gs:[0x188]

$ kstoolex x64nasm "mov rax, qword gs:[abs 0x188]" 0000000000000000 [ 65 48 a1 88 01 00 00 00 00 00 00 ] mov rax, qword gs:[abs 0x188] $ rasm2 -a x86 -b 64 -s intel -D "65 48 a1 88 01 00 00 00 00 00 00" 0x00000000 11 6548a18801000000000000 movabs rax, qword gs:[0x188]

假设指令为:

mov rax, qword gs:[rel 0x188]

只有一个命中,但不是我们想要的。

$ kstoolex x64nasm "mov rax, qword gs:[rel 0x188]" 0000000000000000 [ 65 48 8b 05 88 01 00 00 ] mov rax, qword gs:[rel 0x188] $ rasm2 -a x86 -b 64 -s intel -D "65 48 8b 05 88 01 00 00" 0x00000000 8 65488b0588010000 mov rax, qword gs:[rip + 0x188]

llvm官方代码要比keystone的新很多,我看看能不能替换。

2018-11-16 15:20 bluerust

TMD,keystone为了支持指定起始地址,在56个文件里,插入了465个改动。llvm的接 口可以直接编译.asm,ks_asm()能直接支持完整的.asm文件,但是在这模式下.org的 作用和gas是一样的。

2018-11-19 12:02 bluerust

keystone的作者很生猛,水平高低我不够格评价,但是人家这determination,I have to admire。这么多文件,一个一个改过去,也是够烦的。打个比方说ARM的 "bl #0x888700"这个指令,按标准应该被编码成"be 21 22 eb",#0x888700表示相对 偏移,而不是绝对地址。

$ rasm2 -a arm -b 32 -o 0x888000 -D "be 21 22 eb" 0x00888000 4 be2122eb bl 0x1110700

0x1110700-0x888000=0x888700

keystone的作者不知是对这个有所误解还是刻意为之,改变了"bl imm"这个指令的原 始含义,使之对应"bl addr",addr表示绝对地址,最终生成的机器码是pc-relative offset。

$ kstoolex arm "bl #0x888700" 0 0x888000 0000000000888000 [ be 01 00 eb ] bl #0x888700 $ rasm2 -a arm -b 32 -o 0x888000 -D "be 01 00 eb" 0x00888000 4 be0100eb bl 0x888700

类似的,处理x86/x64汇编时,nasm/masm不支持"jmp 0x7384838"相对跳转这样的写 法,att允许这么写,但翻译出来不是我们的本意。我们的本意是想生成0xeb、0xe9 这样的指令,但gas生成的意思是"jmp ds:[0x7384838]"。在nasm/masm/gas中,有没 有办法生成0xeb、0xe9并指定目标地址呢,我是没找着,只能使用label。keystone 也改掉了这个语义。

$ kstoolex x32 "jmp 0x7384838" 0 0x7380000 0000000007380000 [ e9 33 48 00 00 ] jmp 0x7384838 $ rasm2 -a x86 -b 32 -o 0x7380000 -D "e9 33 48 00 00" 0x07380000 5 e933480000 jmp 0x7384838

scz:

我猜keystone这样改,就是为了方便写小型shellcode。

修改:

llvm/lib/Target/X86/X86GenAsmMatcher.inc

5672行开始的这一段:


// 'Mem128' class if (Kind == MCK_Mem128) { if (!Operand.isMemOffs() && Operand.isMem128()) return MCTargetAsmParser::Match_Success; }

// 'Mem16' class if (Kind == MCK_Mem16) { if (!Operand.isMemOffs() && Operand.isMem16()) return MCTargetAsmParser::Match_Success; }

// 'Mem256' class if (Kind == MCK_Mem256) { if ( !Operand.isMemOffs() && Operand.isMem256()) return MCTargetAsmParser::Match_Success; }

// 'Mem32' class if (Kind == MCK_Mem32) { if (!Operand.isMemOffs() && Operand.isMem32()) return MCTargetAsmParser::Match_Success; }

// 'Mem512' class if (Kind == MCK_Mem512) { if (!Operand.isMemOffs() && Operand.isMem512()) return MCTargetAsmParser::Match_Success; }

// 'Mem64' class if (Kind == MCK_Mem64) { if (!Operand.isMemOffs() && Operand.isMem64()) return MCTargetAsmParser::Match_Success; }

// 'Mem80' class if (Kind == MCK_Mem80) { if (!Operand.isMemOffs() && Operand.isMem80()) return MCTargetAsmParser::Match_Success; }

// 'Mem8' class if (Kind == MCK_Mem8) { if (!Operand.isMemOffs() && Operand.isMem8()) return MCTargetAsmParser::Match_Success; }


原来没有"!Operand.isMemOffs() &&",这是bluerust新增的。

scz:

这是在改啥?

bluerust:

Patch掉Operand类型判断有误的地方。原来的判断会导致"mov rax, gs:[0x188]"这 个指令匹配到"mov rax, gs:[abs 0x188]"和"mov rax, gs:[rel 0x188]",前者才是 我们想要的,后者明显错了。

尽管寻找"mov rax, gs:[0x188]"匹配的Opcode时会先命中"mov rax, gs:[abs 0x188]", 但是其他指令不一定有这么幸运,所以就随手补了下,以防它哪天跳出来咬人。

2019-01-14 12:44 Constantinopolis

Online Assembler and Disassembler http://shell-storm.org/online/Online-Assembler-and-Disassembler/ (Online wrappers around the Keystone and Capstone projects)