标题: 利用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 [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 00000000`ff662ff8 65488b042588010000 mov rax,qword ptr gs:[188h] 00000000`ff663001 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)