标题: 利用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
The following
$ ./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
$ ./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
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)