2024年3月

大家好,我是老猫。

大概在月初的时候,我发了一篇文章【
当程序员之后?(真心话)
】,在这篇文章中,提及了抽象思维对一名程序员的重要性。可能说得也比较笼统,所以就有小伙伴问了“普通人应该如何提成抽象思维呢?”,当时我的回答是这样的。

回答截图

老猫觉得当时的回答太过敷衍了,甚至有点不太负责,所以在此也要向这位小伙伴道个歉。后来老猫仔细结合日常工作中的思考方式,把想到的又重新梳理总结了一下。分享给各位,希望对大家有所帮助。

关于抽象的一些简单例子

关于抽象思维,百度百科上面是这么说的:“又称词的思维或者逻辑思维,是指用词进行判断、推理并得出结论的过程。抽象思维以词为中介来反映现实。这是思维的最本质特征,也是人的思维和动物心理的根本区别。”

感觉会比较官方,当然老猫也可能尚未到达这个认知高度,老猫的理解,抽象思维应该更多的是一个概括出事物共同的、关系性比较强的本质特性,舍弃非本质特性的过程。

用我儿子最喜欢的昆虫打个比方,七星瓢虫、螳螂、蚂蚁、大兜甲虫等等,他们长得形态大小可能都不同,而且差距还是比较大的,但是这些虫子有一个比较显著的共同特性,那就是它们都有六条腿,根据这个咱们就抽象出“六足节肢动物”,也就是昆虫。(出于好奇,老猫后来也看了一段时间小朋友的昆虫绘本,发现挺有意思,其实我们日常看到的蜘蛛,马路等等多足动物不能叫做昆虫)那这样对共同特性的概括其实就是一种抽象了。

我们再举个场景抽象的例子,某东plus会员大家不晓得大家有没有用过,开通会员之后可以享受免运费、优先配送、可以加入健康关爱计划、PLUS会员可以享受全年最低价等等,这些乱七八糟的乍看起来好像也没有什么共同特性或者关系,但是我们可以看到的是,这些好像都是再给用户提供额外的服务享受。那么对于这种共性,我们可以抽象成“会员享受福利”,那么再语言文字精简一些,那就是“会员权益”。老猫刚才的描述中,其实就是一个抽象过程。

一些抽象练习

上面有朋友问,日常生活中如何提升抽象思维,那咱们就从日常锻炼思维聊起。

水平思维

水平思维是从一个点向四面八方发散出许多有直接关联或者没有直接关联的点。比如看到桌上放着的充电器,我们可以想到手机、电池、充电宝等有直接关联的点,也可能想到电动车、共享单车、太阳能等没有直接关联的点。这种思维方式的话,更多类似于“头脑风暴”。寻求看待事物的不同角度以及不同方法,追求数量以及创意。

这种思维方式要求我们致力于提出不同的看法,所有的看法都在同一层面上,每个不同的看法不是相互推导出来的,而是各自独立产生的。日常开发过程中这种水平思考有助于我们摆脱产品需求的“信息囚笼”

比较形象一点的描述就是“在不同的地方挖出不同的洞。”

如下图咱们用上面提到的plus会员举例画一幅图:

水平思维

上图中这种服务于会员的方式就是我们所说的“挖出的不同的洞”

垂直思维

上面提到水平思维,那当然还有垂直思维方式。垂直思维的方式实在水平思维的基础上,对事物特定部分(关键部分)深度分析的方法。垂直思考注重细节,同时又兼顾水平思考。

举个生活中的例子可能更好理解,我们去眼镜店挑选眼镜,有太阳镜、近视眼镜、老花眼镜等这种水平思维抽象之后就是眼镜。那么如果咱们深入垂直方式去看待呢?那么我们可能会这么理解。桌上摆放的这个眼镜镜片是什么材质的,树脂的还是其他合成材料,镜框采用的是什么材质的,另外眼镜的焦距多少等等一些列就当前这个眼镜的一些列深入的挖掘。

垂直思维能让我们在某一领域或者专业技术上更加精通透彻,成为专家。

垂直思维是以逻辑与数学为代表的传统思维模式。其特点是:根据前提一步步进行推导,既不能逾越,也不允许出现步骤上的错误。它有非常强的逻辑性、推理性,能够让我们专注于某一点,深入分析。

还是基于之前的会员的例子,咱们来看一下下图。

垂直思维

上面的图我们可以看到,我们对“Plus会员权益”中的免运费表示垂直深度思考,其中可能会包含,免运费实现的细节以及流程等等。那这种思考方式就是垂直思考方式。

从上面的图中,我们能够更加清晰的看出,咱们的“垂直思维”方式是基于“水平思维”的基础上的,那么我们平时的抽象思考方式就显而易见了,咱们需要先“水平思考”然后继而再进行“垂直思考”。

“水平思维”和“垂直思维”对比总结

上面聊了一下两种抽象思维方式,那么对于两者的关系我们可以总结出下面的三点:

  1. 运用水平思维从多个角度看待问题,尽可能多地罗列出视角和点子。水平思维的发散性能让我们更容易看到事情的整体,并且想出许多有创意性的点子。
  2. 对罗列的点进行排序,找到最重要的点。
  3. 对最重要的点进行垂直分析。

研发人员常用抽象思维

上面和大家聊到了日常生活中,咱们提升自己抽象思维的思考方式并且枚举了一些小例子。

那么我们研发人员比较实用的抽象思维方式又是什么呢?

“自顶向下思考”以及“自底向上思考”相结合

在咱们日常的生活中“水平思维”以及“垂直思维”固然重要,但是上面提及的这个点可能在我们实际中使用得更加多些。

自顶向下:咱们在梳理业务的过程中,咱们需要先明确目标用户、业务诉求。从而建立“大局观”。然后在依层次分解,一直到业务的规则和细节。

这种思考方式在日常工作中偏向于系统建模上,例如咱们在进行技术设计的时候,我们一般都会从架构着手,从大到小,我们可能会划分具体的系统有几个,然后再到每个系统之间的协作方式,然后再到每个系统内部的实现细节(在这个阶段可能就会涉及我们熟悉的流程细节以及数据模型细节)。老猫之前写过一篇文章,其熟悉业务流程的思路大概也是按照这种方式去做的,大家有兴趣可以看一下这里【
新接手一个系统,我是这么熟悉的

自底向上:这种思考方式是强调我们需要先去收集细节,从局部着眼于归纳、演绎,最终洞见宏观层面。这种思考方式很多时候使用于产品经理进行做产品设计的时候,产品经理在做相关产品设计的时候,往往会去先调研业务诉求,业务诉求一般都是比较零散而且没有组织的,比较稀碎。然后产品经理会将这些稀碎的需求进行归纳,抽象升华出相关的产品形态。那这种过程其实就是自底向上的过程。

总结

老猫觉得这种思考方式可能无论对于软件行业还是非软件行业的人都有比较好的帮助。老猫之前做过相关大客户的业务,和一些销售也打过一些交道,在他们实际和客户进行交流的过程中,我就发现他们在介绍业务的时候仅仅有条,最终表述出来的思路总结之后也差不多是上面几种。当然这也是老猫所能想到的一些提升抽象思维的一些方式。希望对大家有所帮助,当然如果大家还有比较好的思路也欢迎在评论区留言,欢迎大家一起讨论。

抛开技术不谈,老猫始终觉得,其实很多行业咱们的思考方式应该是相通的。包括抽象思维的方式甚至思考问题的方式以及逻辑思维方式。今天的分享就到这里了。欢迎持续关注老猫。

【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
https://www.cnblogs.com/cnb-yuchen/p/18032072
出自【
进步*于辰的博客

纯文字阐述,内容比较干。并且,由于考虑到时间长了恐有所遗漏,便即兴记录,并没有对内容进行筛选、排序。因此,大家在阅读时可以直接 Ctrl + F 进行检索。

1、细节积累

  1. 1000 == new Integer(1000)
  2. "CS" + ne w String("DN") = "CSDN"
  3. protected 的限制范围是同体系(继承关系)、同包。
  4. 构造方法无返回值,
    void
    属于一种返回值类型。
  5. final、static、private 修饰的方法不能被重写。
  6. 封装的主要作用:对外隐藏内部实现细节、增强程序的安全性。
  7. boolean、byte 类型变量都占一个字节。
  8. 每个类都有各自的常量池,需要时从JVM总常量池中分配;
  9. 装饰模式的原理类似继承,作用是实现读写的扩展。
  10. 表示“空指针”是
    null
    ,不是
    NULL
  11. 进入同步代码块、
    wait()
    、读取输入流、降低优先级这些方法都可使线程停止。
  12. 接口的方法只能由
    public

    abstract

    default

    static
    修饰,不能是
    private
    ,因为接口不能私有化。
  13. 形参可以声明为
    final
    ,只是方法内不能修改。
  14. $文数$指正序与倒序相同的数,如:
    12321
  15. Scanner 类的
    next()
    获取输入以
    “ ”
    (空格)结尾,
    nextLine()
    以“
    \n
    ”(回车)结尾。
  16. 无论程序多复杂,运行时都、且之会生成一个JVM进程。
  17. default
    的三个使用场景:
    switch
    、默认方法、自定义注解注解元素的默认值。
  18. float 和 double 都是小数,java规定:
    1.0
    隐式为
    1.0d
    ,其中的
    d
    是 double 的标志。因此,
    float f1 = 1.0
    这条语句编译报错,因为 double 所占字节数大于 float。这就是给
    float
    类型变量赋初值时要加
    f
    的原因。
  19. 不能作为
    switch
    参数的类型有:float、double、long、boolean 和复杂表达式。
  20. java源文件(后缀是
    .java
    )编译时默认使用操作系统所设定的编码进行编译。这就是为什么使用记事本编写 java 源代码可以正常编译并在 JVM 运行的原因。
  21. 原始 / 基本数据类型:short、int、long、char、byte、float、double、boolean。
  22. 若仅实例化子类,由于
    this
    代表的是当前实例,故当在父类中使用
    this
    时,
    this
    代表的是子类实例,而非父类实例。
  23. 实例化时,会先从方法区中检查构造方法是否相符(相同),再初始化成员变量和成员方法。
  24. 在多层 for 循环中,有时
    for
    前面有一个
    outer:/inner:
    ,作用是便于控制循环,这只是一个标识,不固定。
  25. boolean 不能为
    null
    的原因:(1)、boolean 只有
    true/false
    两种取值;(2)、
    null
    表示空指针,而 boolean 是基本数据类型。
  26. JVM 方法区用于存放静态资源和类信息,多线程共享(线程安全)。当多线程同时使用一个类时,若此类未加载,则只有一个线程去加载类,其他线程等待。

2、一些类的使用细节

2.1 Object

  1. 因为 Object 类型不能作比较(
    obj1 > obj2
    语法不允许),故不能通过 Object 作为“上转类型”来比较不同包装类,需使用 Comparable 类代替 Object。(暂不知原因)
  2. int

    int[]

    Integer

    Integer[]
    都可上转为 Object,
    Integer[]
    也可上转为
    Object[]
    ,但
    int[]
    不可上转为
    Object[]
    。(暂不知原因)

2.2 String

为了标记字符串为 unicode 字符,在字符串末尾默认存在一个空格,此空格不存在于字符序列(
value
)中(即不包含在
length
中),但使用
subString()
截取字符串时,开始索引可以是
length
,能得到一个空格。

2.3 String与字符串缓冲区的区别

String是常量,不可变;字符串缓冲区(StringBuffer/StringBuilder)支持可变的字符串。当需要对字符串进行频繁操作时,两者的性能差异很大。

String 赋值的步骤:

String → StringBuffer/StringBuilder → append() → toString() → String

引用 String 类的
API
中的一张图:
在这里插入图片描述

2.4 基本数据类型、包装类与String三者间的转换关系

参考笔记一,P40.7。

在这里插入图片描述

2.5 Package

参考笔记二,P29.6。

  1. Java属跨平台语言,与操作系统无关,Package`是用来组织源文件的一种虚拟文件系统。
  2. import
    的作用是引入,而不是拷贝,目的是告诉编译器编译时应去哪读取外部文件。
  3. Java的包机制与 IDE 无关。
  4. 同一包下的类可不使用
    import
    而直接调用(不是指实例化时用全类名)。

3、关于比较

3.1
==

==
是运算符,只能用于基本数据类型之间、以及相同包装类之间的比较。

3.2
equals()

所有包装类都重写了 Object 类的
equals()
,但无论
equals()
底层调用的是
toString()

intValue()
、还是
longValue()
,结果都是获取值,与地址无关。

当使用
equals()
比较包装类时,多用于相同包装类之间。之所以不用于不同包装类,是因为所有包装类重写的
equals()
底层都封装了类型判断,若是不同包装类,则直接返回
false
,即无法比较。

4、自动装箱与自动拆箱

所有包装类都具备自动装箱和自动拆箱机制,只是 Double、Float、Boolean 这三个包装类变量的值始终是对象。其中,Double、Float 的常量未存放在 JVM 方法区常量池的原因是浮点数无限。

如果大家想进一步了解自动装箱和自动拆箱机制,可参考博文《
[Java]基本数据类型与引用类型赋值的底层分析的小结
》的第4.2项。

6、一个关于
static
容易混淆的细节

大家先看个示例。

int a;

public static void main(String[] args) throws Exception {
	C c1 = new C();// C 是类名
    int i = 10;
    while (i-- > 0) {
        new Thread(() -> {
            int n = 10000;
            while (n-- > 0)
                c1.a++;
        }).start();
    }
    Thread t1 = new Thread(() -> {});
    t1.start();
    t1.join();
    System.out.println(c1.a);
}

请问
a
的打印结果是多少,
100000
?不是,因为存在多线程并发访问。那如果把
a
改成类变量,结果是
100000
吗?仍然不是,依旧存在多线程并发访问。

不是由
static
修饰吗,为什么不是
00000

?这就是容易混淆的地方。

  • 类变量属于类,为所有对象共享;
  • 并发问题指线程对同一个数据的更改对其他线程不可见而导致的数据不一致问题。

所以,类变量不是为所有线程共享。

换言之,
a
必须进行同步处理(
synchronized
)。至于
a
定义为类变量、还是成员变量,看具体需求,与多线程无关。(注意下面这一点)

我突发奇想。(上面的线程在跑着)

public static void main(String[] args) throws Exception {
    System.out.println(new C().a);
}

请问能获取到
a
的实时数据吗?当然不能,为什么?因为是两个 JVM。

18、关于数组定义

定义数组的4种格式:

1、int[] arr1 = new int[5];------------A
2、int arr2[] = new int[5];
3、int[] arr3 = {1, 2, 3};-------------B
4、int[] arr4 = new int[]{1, 2, 3};----C // [] 内不能指定元素个数

特例:

public static void main(String[] args) throws Exception {
    print({1, 2, 3});// 编译报错
    int[] arr = {1, 2, 3};
    print(arr);// 打印:[1, 2, 3]
}

public static void print(int[] arr) {
    System.out.println(Arrays.toString(arr));
}

若形参类型为数组,调用时不能进行数组初始化。

19、实现多接口时的细节

1
、方法 A 与 B 参数列表、返回值类型、方法名都相同时。

interface AnimalService {
    void print();----------------------A
}

interface LifeService {
    void print();----------------------B
}

class Person implements AnimalService, LifeService {
    @Override
    public void print() {--------------C // 重写A
    }
}

C 重写 A 还是 B,取决于类 Person 实现接口的顺序,故 C 重写A。

2
、方法 A 与 B 仅返回值类型不同时。

interface AnimalService {
    void print();----------------------A
}

interface LifeService {
    int print();----------------------B
}

class Person implements AnimalService, LifeService {
    @Override
    public void print() {--------------C // 编译报错
    }
    或
    @Override
    public intC print() {--------------D // 编译报错
    }
}

A 与 B 仅返回值类型不同,这种情形不允许,故 C、D 都编译报错。

3
、方法 A 与 B 完全不同时。

interface AnimalService {
    void print();----------------------A
}

interface LifeService {
    int print(int age);----------------------B
}

class Person implements AnimalService, LifeService {
    @Override
    public void print() {--------------C // 重写A
    }
    
    @Override
    public int print(int age) {--------D // 重写B
    }
}

这就是普遍实现多接口的情况,此时,C、D 也属于重载。

21、关于时间类和时间处理

21.1 Date、Calendar

Date 类倾向于
获取
时间,Calendar 类倾向于
处理
时间。

示例:获取30分钟后的时间。

Date client = new Date();// 当未指定参数时,获取当前时间
System.out.println(client);// 打印:Wed Apr 12 19:48:10 CST 2023

Calendar handler = Calendar.getInstance();// Calendar是抽象类,故需要通过调用getInstance()创建实例
handler.setTime(client);// 设置初始时间
handler.add(Calendar.MINUTE, 30);// 将初始时间增加30分钟
System.out.println(handler.getTime());// 打印:Wed Apr 12 20:18:10 CST 2023

21.2 SimpleDateFormat

普通的时间格式:
yyyy-MM-dd HH:mm:ss

有时候会是“
hh
”,区别是:前者是24小时制。

22、我误解的一个基础

问题:子类修改父类成员,通过反射获取修改前后父类成员、值未改变。

示例1

待反射类。

class Platform {
    String name;
    public void initail() {
        name = "csdn";
    }
}

测试代码。

Platform p1 = new Platform();
Class z1 = p1.getClass();
Field f1 = z1.getDeclaredField("name");
System.out.println(f1.get(p1));// 打印:null
p1.initail();
System.out.println(f1.get(p1));// 打印:csdn

示例2。
待反射类。

class Platform {
    String name;
}
class CSDN extends Platform {
    public void initail() {
        name = "csdn";
    }
}

测试代码。

Platform p1 = new Platform();
Class z1 = p1.getClass();
Field f1 = z1.getDeclaredField("name");
System.out.println(f1.get(p1));// 打印:null

CSDN c1 = new CSDN();
c1.initail();
System.out.println(f1.get(p1));// 打印:null------A
System.out.println(p1.name);// 打印:null---------B
System.out.println(c1.name);// 打印:csdn

这2个示例的区别在哪?前者是通过父类自身实例修改其成员变量,而后者是通过子类实例修改父类成员变量。

明明已经调用
c1.initail()
,为何A、B两处的
name
仍为
null

关于子类或父类初始化,详述可参考博文《
[Java]知识点
》的第5.4项。

原因很简单:假设
c1
的父类实例是
p0
(当然,实际上 Platform 类仅进行了实例初始化,未实例化),
p0

p1
是两个实例

上述只是一个基础知识,为何我觉得可作为一个细节?
下述代码是我研究 ArrayList
<E>
类的迭代器
Itr
和子迭代器
ListItr
时的一个示例。

ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);

Iterator<Integer> it = list.iterator();

Class z1 = Class.forName("java.util.ArrayList$Itr");
Field f1 = z1.getDeclaredField("expectedModCount");
f1.setAccessible(true);
System.out.println(f1.get(it));// 打印:3------A

ListIterator<Integer> lit = list.listIterator();
lit.add(4);
System.out.println(f1.get(it));// 打印:3------B
lit.add(5);
System.out.println(f1.get(it));// 打印:3------C

it.next();// 抛出:ConcurrentModificationException

关于
expectedModCount
,见
ArrayList
<E>

的第7.1项。

由于之前调用了3次
add()
,故A处的值为
3
。同理,B、C两处的值应分别为
4

5
,可实际都是
3

PS
:我被潜意识误导了,即原因所在。

23、大家可能无意中避过的一个坑

相信大家都写过这么一段代码:

String str;
if (str != null && str.equals("")) {}

并且,可能自己领悟,或者他人指点,一般会这么写:

String str;
if (str != null && "".equals(str)) {}

因为这样可以避免
空指针异常

其实,这两种写法并没有什么区别,用第二种纯粹是个人习惯或者是一种规范,我也是如此。

曾经我有一个疑惑:“第一种写法中同样会执行
str.equals()
,它是如何规避空指针异常的?”
由于这看起来只是一个细节,而且不影响敲代码,也就没关注。然而今天看源码,就碰到了这么一段判断:

if (useCaches &&
    reflectionData != null &&
    (rd = reflectionData.get()) != null &&
    rd.redefinedCount == classRedefinedCount) {}

四个 boolean,这若是无法确定每个 boolean,还如何解析?这就让我想起了开头说的例子。其实,我们并不需要确认所有的 boolean,听我细细道来。。。

这就要涉及运算符
&&

||
的运算规则了,为了研究这个问题,翻了一些资料。果然,这两种运算符实在太基础了,没遇到我需要的,只能自己
debug
了。。。

看下述测试。
1
、测试
&&

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
&&
结果为 true,要求每一段都为 true。从左往右逐个执行,若前面为 false,结果则为 false,后面就不会再执行;若前面为 true,则继续判断后面。(这就是开头我说那两种写法并没有区别的原因)

2
、测试
||

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
||
结果为 true,要求其中一段为 true 即可。从左往右逐个执行,若前面为 true,结果则为 true,后面就不会再执行;若前面为 false,则继续判断后面。

PS
:看到这6张不断播放的图片以及这绕口的说明,我自己都有点晕。。。大家可以根据图片自行理解。

补充一点:

String str;
if ("".equals(str)) {}

请问这样可以避免空指针异常吗?答案是不能,因为下述代码会抛出空指针异常:

Object anObject = null;
if(anObject instanceof String) {}

可能大家不明白我的意思,如果大家感兴趣,可以去看看
equals()
的源码。

直通车 ->
String类
的第2.14项。

24、关于程序与数据库之间数据类型转换说明(基于JDBC)

24.1 数据存储(DML)

  1. String → DateTime,需使用类java.sql.Timestamp。

24.2 数据查询(select)

  1. DateTime → String,先通过
    rs.getTimestamp()
    (rs是结果集ResultSet对象)获取Timestamp对象,再使用SimpleDateFormat类将其转为String。

最后

如果大家想要了解一些Java知识点,可查阅博文《
[Java]知识点
》。

本文持续更新中。。。

题目:


丢到PE里, 无壳,64bit

丢到IDA里,shift+F12,查看字符串,找到一个很可疑的字符串

跟进去看看,找到目标函数,我另外搜索了一下,没有mian函数,sub_400F8E应该就是解题的关键函数

有部分变量我修改的名字,为了方便理解

1 __int64 __fastcall sub_400F8E(__int64 a1, int a2, int a3, int a4, int a5, inta6)2 {3   int v6; //edx
4   int v7; //ecx
5   int v8; //r8d
6   int v9; //r9d
7   int v10; //ecx
8   int v11; //r8d
9   int v12; //r9d
10   char v14; //[rsp+0h] [rbp-C0h]
11   char v15; //[rsp+0h] [rbp-C0h]
12   char input[136]; //[rsp+10h] [rbp-B0h] BYREF
13   int v17; //[rsp+98h] [rbp-28h]
14   char v18; //[rsp+9Fh] [rbp-21h]
15   int v19; //[rsp+A0h] [rbp-20h]
16   unsigned __int8 input_1; //[rsp+A6h] [rbp-1Ah]
17   char key; //[rsp+A7h] [rbp-19h]
18   int v22; //[rsp+A8h] [rbp-18h]
19   int v23; //[rsp+ACh] [rbp-14h]
20   int v24; //[rsp+B0h] [rbp-10h]
21   int v25; //[rsp+B4h] [rbp-Ch]
22   _BOOL4 bool_type; //[rsp+B8h] [rbp-8h]
23   int i; //[rsp+BCh] [rbp-4h]
24 
25   sub_407470((unsigned int)"Give me the password:", a2, a3, a4, a5, a6, a2);26   sub_4075A0((unsigned int)"%s", (unsigned int)input, v6, v7, v8, v9, v14);27   for ( i = 0; input[i]; ++i )28 ;29   bool_type = i == 22;                          //下面要打印Congras,那if里面的bool_type必须为130                                                 //i=22
31   v25 = 10;32   do
33 {34     v10 = (int)sub_406D90() % 22;               //v10的范围0~21
35     v22 =v10;36     v24 = 0;37     key =byte_6B4270[v10];38     input_1 =input[v10];39     v19 = v10 + 1;40     v23 = 0;41     while ( v23 <v19 )42 {43       ++v23;44       v24 = 1828812941 * v24 + 12345;45 }46     v18 = v24 ^input_1;47     if ( key != ((unsigned __int8)v24 ^input_1) )48       bool_type = 0;49     --v25;50 }51   while( v25 );52   if( bool_type )53     v17 = sub_407470((unsigned int)"Congras\n", (unsigned int)input, v24, v10, v11, v12, v15);54   else
55     v17 = sub_407470((unsigned int)"Oh no!\n", (unsigned int)input, v24, v10, v11, v12, v15);56   return0LL;57 }
  • sub_407470((unsigned int)"Give me the password: "
    , a2, a3, a4, a5, a6, a2); sub_4075A0((unsigned int)"%s", (unsigned int
    )input, v6, v7, v8, v9, v14);
    我一开始猜测是输出输入用的,预防万一后面查了资料,就是根据C语言函数可变参数的特性反汇编出来的,就是普通输出输入函数
    至于为什么五个变量就第一个我修改了名称————是看后面代码,通过个人理解,推出是输入量。
  • v10 = (int)sub_406D90() % 22;
    可以得知v10的范围是0~21,`sub_406D90`点进去跟踪,我没有得到什么有用的东西,大佬说`v10`是一个随机数,范围也的确是0 ~ 21,但是不是顺序来取值的——就算不知道`v10`的具体数值,但是肯定是0~21中的一个,写逆向脚本的时候遍历也是可以得出的。

  • key = byte_6B4270[v10];
    双击byte_6B4270得到一串东西,应该是关键字符串了,结合上面可以理解为:`v10`是一个随机的下标,根据下标从`byte_6B4270`中随机抽取一个放到`key`里面
  • input_1 = input[v10];
    在输入中随机抽取一个放到`input_1`里面,方便后面的异或操作
  • while ( v23 < v19 )
    {
    ++v23;
    v24 = 1828812941 * v24 + 12345;
    }v18 = v24 ^ input_1
    这就是变换的关键部分了。经过加法乘法异或一系列操作,得到`v18`

  • if ( key != ((unsigned __int8)v24 ^ input_1) )
    这个if的作用就是检查变换后得出的`v18`与`key`是否相等


解题脚本

1 key = [95,242,94,139,78,14,163,170,199,147,129,61,95,116,163,9,145,43,73,40,147,103]2 flag = ""
3 
4 for j in range(22):5     v23 =06     v24 =07     v19 = j + 1
8     while v23 <v19:9         v23 += 1
10         v24 = 1828812941 * v24 + 12345
11     flag += chr((v24 ^ key[j])&0xff)12 
13 print(flag)

flag:
flag{d826e6926098ef46}

标签:PostgreSQL.Druid.Mybatis.Plus;

一、简介

PostgreSQL是一个功能强大的开源数据库系统,具有可靠性、稳定性、数据一致性等特点,且可以运行在所有主流操作系统上,包括Linux、Unix、Windows等。

通过官方文档可以找到大量描述如何安装和使用PostgreSQL的信息。

环境搭建,基于
Centos7
部署的
PostgreSQL-14
版本,官方文档中提供
yum
安装的方式,配置的话可以参考源码仓库中的其他版本「见文尾」,这里不赘述。

# 1、RPM仓库
sudo yum install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm

# 2、安装PostgreSQL
sudo yum install -y postgresql14-server

# 3、初始化选项
sudo /usr/pgsql-14/bin/postgresql-14-setup initdb
sudo systemctl enable postgresql-14
sudo systemctl start postgresql-14

# 4、查看版本
psql --version
psql (PostgreSQL) 14.11

二、工程搭建

1、工程结构

2、依赖管理

Druid连接池使用的是
1.2.18
版本;使用
mybatis-plus
组件的
3.5.3.1
版本;PostgreSQL本地环境是
14.11
版本,这里依赖包使用
42.6.2
版本;

<!-- Postgresql -->
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>${postgresql.version}</version>
</dependency>
<!-- Druid组件 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-3-starter</artifactId>
    <version>${druid-spring-boot.version}</version>
</dependency>
<!-- MybatisPlus组件 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>${mybatis-plus.version}</version>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>${mybatis-plus.version}</version>
</dependency>

三、PostgreSQL配置

1、数据库配置

有关于
Druid
连接池的可配置参数还有很多,可以参考源码中的描述或者官方案例,此处只提供部分常见的参数配置;

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      # 数据库
      url: jdbc:postgresql://127.0.0.1:5432/pg-data-14
      username: postgres
      password: postgres
      driver-class-name: org.postgresql.Driver
      # 连接池-初始化大小
      initial-size: 10
      # 连接池-最大连接数
      max-active: 100
      # 最大等待时间
      max-wait: 60000
      # 连接池-最小空闲数
      min-idle: 10
      # 检测空闲连接
      test-while-idle: true
      # 最小空闲时间
      min-evictable-idle-time-millis: 300000

2、逆向工程类

逆向工程新版本的API语法和之前有变化,但是整体的逻辑还是差不多。其它的SQL脚本和基础案例,以及相关单元测试不再赘述,参考源码仓库即可。

public class GeneratorMybatisPlus {

    private static final String jdbcUrl = "数据库地址";
    private static final String outDir = "存放路径";

    public static void main(String[] args) {
        // 数据源配置
        DataSourceConfig dataSourceConfig = new DataSourceConfig.Builder
                (jdbcUrl,"postgres","postgres")
                .build();

        // 代码生成器
        AutoGenerator autoGenerator = new AutoGenerator(dataSourceConfig);

        // 全局配置
        GlobalConfig globalConfig = new GlobalConfig.Builder()
                .outputDir(outDir).disableOpenDir().author("知了一笑") // .enableSwagger()
                .build();

        // 分包配置
        PackageConfig packageConfig = new PackageConfig.Builder()
                .parent("com.boot.pgsql.generator").controller("controller")
                .service("dao").serviceImpl("dao.impl").mapper("mapper").entity("entity")
                .build();

        // 策略配置
        StrategyConfig strategyConfig = new StrategyConfig.Builder()
                .addInclude("user_info","sys_user")
                .addTablePrefix("")
                .entityBuilder().enableLombok()
                .naming(NamingStrategy.underline_to_camel)
                .columnNaming(NamingStrategy.underline_to_camel)
                .controllerBuilder().formatFileName("%sController")
                .entityBuilder().formatFileName("%s")
                .serviceBuilder().formatServiceFileName("%sDao").formatServiceImplFileName("%sDaoImpl")
                .mapperBuilder().formatMapperFileName("%sMapper").formatXmlFileName("%sMapper")
                .build();

        autoGenerator.global(globalConfig);
        autoGenerator.packageInfo(packageConfig);
        autoGenerator.strategy(strategyConfig);

        // 执行
        autoGenerator.execute();
    }
}

四、参考源码

文档仓库:
https://gitee.com/cicadasmile/butte-java-note

源码仓库:
https://gitee.com/cicadasmile/butte-spring-parent

PostgreSQL配置参考:
https://gitee.com/cicadasmile/butte-java-note/blob/master/doc/database/postgresql/P01、PostgreSQL环境搭建.md

Mybatis三种逆向工程:
https://gitee.com/cicadasmile/butte-java-note/blob/master/doc/frame/tool/T01、Mybatis三种逆向工程.md

1、概述

在《
Kubernetes客户端认证(一)—— 基于CA证书的双向认证方式
》和《
Kubernetes客户端认证(二)—— 基于ServiceAccount的JWTToken认证
》两篇博文中详细介绍了Kubernetes客户端认证方式,包括以证书方式访问的
普通用户或进程

运维人员及 kubectl、 kubelet 等进程);以 ServiceAccount 方式(JWTToken)访问的 Kubernetes 的
内部服务进程
,它是给运行在 Pod 里的进程用的,它为 Pod 里的进程提供了必要的身份证明,但它并不是给 Kubernetes 集群的用户(系统管理员、 运维人员、租户用户等)用的,对于Kubernetes集群的用户可以通过证书或者接入外部用户系统来提供身份证明。而其中最简单的方式便是为用户签发客户端证书,签发客户端证书有两种方式,
一种是基于CA根证书签发证书,另一个种是发起 CSR(Certificate Signing Requests)请求。
在《
Kubernetes客户端认证(一)—— 基于CA证书的双向认证方式
》这篇博文的3.2章节详细介绍了基于CA根证书签发证书,这种方式因为需要基于根CA证书、根CA私钥进行证书签发,一般需要在集群的master节点上进行配置,但是生产环境下一般情况下我们都没办法进入到集群的master节点机器,所以这种方式用的不多。但是一般情况我们都能拿到对应的一个kubeconfig文件用来访问k8s集群,基于kubeconfig 文件我们也可以进行证书的批准和授权,这就是k8s CSR机制,通过使用CSR方式签发客户端证书,这也是k8s推荐方式,本文主要讲解如果通过k8s CSR机制来为用户签发客户端证书。

2、CertificateSigningRequest(CSR)机制

CertificateSigningRequest(CSR)资源用来向指定的签名者申请证书签名, 在最终签名之前,申请可能被批准,也可能被拒绝。(特性状态: Kubernetes v1.19 [stable])

2.1 请求签名流程

CertificateSigningRequest 资源类型允许客户端基于签名请求申请发放 X.509 证书。 CertificateSigningRequest 对象在 spec.request 字段中包含一个 PEM 编码的 PKCS#10签名请求。CertificateSigningRequest 使用 spec.signerName 字段标示签名者(请求的接收方)。 注意,spec.signerName 在 certificates.k8s.io/v1 之后的 API 版本是必填项。 在 Kubernetes v1.22 和以后的版本,客户可以可选地设置 spec.expirationSeconds 字段来为颁发的证书设定一个特定的有效期。该字段的最小有效值是 600,也就是 10 分钟。

创建完成的 CertificateSigningRequest,要先通过批准,然后才能签名。 根据所选的签名者,CertificateSigningRequest 可能会被控制器自动批准。 否则,就必须人工批准, 人工批准可以使用 REST API(或 go 客户端),也可以执行 kubectl certificate approve 命令。 同样,CertificateSigningRequest 也可能被驳回, 这就相当于通知了指定的签名者,这个证书不能签名。
对于已批准的证书,下一步是签名。 对应的签名控制器首先验证签名条件是否满足,然后才创建证书。 签名控制器然后更新 CertificateSigningRequest, 将新证书保存到现有 CertificateSigningRequest 对象的 status.certificate 字段中。 此时,字段 status.certificate 要么为空,要么包含一个用 PEM 编码的 X.509 证书。 直到签名完成前,CertificateSigningRequest 的字段 status.certificate 都为空。
一旦 status.certificate 字段完成填充,请求既算完成, 客户端现在可以从 CertificateSigningRequest 资源中获取已签名的证书的 PEM 数据。 当然如果不满足签名条件,签名者可以拒签。
为了减少集群中遗留的过时的 CertificateSigningRequest 资源的数量, 一个垃圾收集控制器将会周期性地运行。 此垃圾收集器会清除在一段时间内没有改变过状态的 CertificateSigningRequests:

  • 已批准的请求:1 小时后自动删除
  • 已拒绝的请求:1 小时后自动删除
  • 已失败的请求:1 小时后自动删除
  • 挂起的请求:24 小时后自动删除
  • 所有请求:在颁发的证书过期后自动删除

2.2 Kubernetes 签名者

Kubernetes 提供了内置的签名者,每个签名者都有一个众所周知的 signerName:

  • kubernetes.io/kube-apiserver-client:签名的证书将被 API 服务器视为客户证书。 kube-controller-manager 不会自动批准它。
  • kubernetes.io/kube-apiserver-client-kubelet: 签名的证书将被 kube-apiserver 视为客户证书。 kube-controller-manager 可以自动批准它。
  • kubernetes.io/kubelet-serving: 签名服务证书,该服务证书被 API 服务器视为有效的 kubelet 服务证书, 但没有其他保证。kube-controller-manager 不会自动批准它。
  • kubernetes.io/legacy-unknown: 不保证信任。Kubernetes 的一些第三方发行版可能会使用它签署的客户端证书。

kube-controller-manager 为每个内置签名者实现了控制平面签名。 注意:所有这些故障仅在 kube-controller-manager 日志中报告。

3、通过 CSR 签发客户端证书实例

1)生成用户私钥

openssl genrsa -out zmc.key 2048

2)生成证书请求文件

// 基于用户私钥生成用户证书签名请求文件zmc.csr,其中CN代表k8s用户
openssl req -new -key zmc.key -subj "/CN=zmc" -out zmc.csr

3) 通过 kubectl 创建一个 CertificateSigningRequest 并将其提交到 Kubernetes 集群

cat <<EOF | kubectl apply -f -
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
  name: zmc
spec:
  request: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTUlJQ1V6Q0NBVHNDQVFBd0RqRU1NQW9HQTFVRUF3d0RlbTFqTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQwpBUThBTUlJQkNnS0NBUUVBbWFLRWY5ODBIRkhneG9WdXVBL29oNHlvU1JPNG8vbEU1N1J4UmduT1ZlR0tDbXZoCjRSOWZ3bk9IRzZaUjFVdkd4RzdKY0duaFJFMlFlUFVvcXpVYkVxWnFtc0V4NmpmTHgycTRqd3RNRm9GMDFnaHMKY2RwU0p1WTNEeFlKdFhtbUhKM2h3Q1N1MEZPT1NNMHU2V3JWZ0FFVkRCUno2a0RRcE1lcjlscmQwenBIRTdkUgpJSFNYbWVuVTVFTHVuTGh3a0E5TUhuczAweHhkcEhiR2QrTUFTVEU4LzcrNzR5T3hkQmJZQVhRK04rV2pZQ1NWCm5JT1dVUktWSFpCVDJIKzZxMjFvNkVxK2JlczB2MkhBWmZPTjZhOVVoU2FpZjRQRm9vTXlyS21xSFRkMFdrZlkKY1FZZFVzR0tBUFI3YVZrbmx4cWFxVCtEbXNqM0pBVFdZVjZBV3dJREFRQUJvQUF3RFFZSktvWklodmNOQVFFTApCUUFEZ2dFQkFIOFhoZjJEeVlMRldkQmJtazMwY3FHMkI1Qlh5dEFZWU0vOHQ1Z2UvZjhlMUtEeWQ4aXNQbitXCll1ak5mMzFrNGllRGpZb2NPaDFKZFlTRUkrTmgwdkFiVXVWaU94OU1RY3RUZjBXb0hHTklwRlZ3SFVsNTE2R3MKazRMMEgyVXM5WnlNMUJXSGxVaG13MDJncGs5VjV2RDc3QjN6dnh3aUs4UnJyZlA2YWVScGtnbzdOcFg0bEVMWApRSVY0VzBUWExsTjF2Si9UaGVncHhRZVhPZHZMbjJxd084aEgyVmRSRnQzODNWS0RTNFR0UFdWNGw5WTJGSkxVCk01NlhsdEFhcFd2aDBnV3U4RWVjbzhJd0tSY2xvYzU2RUlob05OVzBHN2lUeDgxVW1LUWlPRXl3RVZVNllPaHAKa25wbXhreWFGUXo4QTdIeG1ON2ZIaTdwdUNCcGZDUT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUgUkVRVUVTVC0tLS0tCg==
  signerName: kubernetes.io/kube-apiserver-client
  #expirationSeconds: 86400  # one day
  usages:
  - client auth
EOF
  • request 字段是 CSR 文件内容的 base64 编码值。 要得到该值,可以执行命令
cat zmc.csr | base64 | tr -d "\n"
  • expirationSeconds 证书过期时间(k8s 1.22版本之前没有这个字段,默认的过期时间是十年左右)
  • usage 字段必须是 ‘client auth’,代表密钥的用途

注意:下面的 csr 代表的是 certificatesigningrequests(是k8s里面的一种资源),注意与上面的 certificate 区分开来。

查看CertificateSigningRequest资源状态

4)批准 CertificateSigningRequest

kubectl certificate approve zmc

查看CertificateSigningRequest资源状态

5)从 CertificateSigningRequest 导出颁发的证书

kubectl get csr zmc -o jsonpath='{.status.certificate}'| base64 -d > zmc.crt

当前k8s集群版本是1.21,可以看到默认的过期时间是十年左右。

验证新签发用户证书也是由kuberntests集群ca证书签发的。

6)使用签发的用户证书访问k8s集群资源

配置用户信息到到对应的 kubeconfig 上下文中。

将客户端证书文件 zmc.crt 和客户端密钥文件 zmc.key 设置为名为 zmc 的用户凭证,并将这些证书和密钥嵌入到 kubeconfig 文件中
kubectl config set-credentials zmc --client-certificate=zmc.crt  --client-key=zmc.key  --embed-certs=true

查看kubeconfig最新配置。

kubectl config view

为当前集群和用户zmc配置kubeconfi上下文。

kubectl config set-context zmc@cluster.local --cluster=cluster.local --user=zmc

切换上下文为当前新创建的用户。

kubectl config use-context zmc@cluster.local

再次查看kubeconfig最新配置。

使用签发的用户证书访问k8s集群资源。

注意: 如果访问资源没有权限就为用户当前k8s集群维护对应资源的RBAC权限,这块详细步骤可以参见《
Kubernetes客户端认证(一)—— 基于CA证书的双向认证方式
》和《
Kubernetes客户端认证(二)—— 基于ServiceAccount的JWTToken认证
》这两篇博文,本文不再赘余。

4、总结

本文详细讲解了Kubernetes使用CertificateSigningRequest方式签发客户端证书详细步骤,基于kubectl和k8s集群交互为示例,当然也可以通过client-go与kube-apiserver交互管理CertificateSigningRequest资源签发客户端证书,并且对于可信客户端证书颁发请求还可以通过开发自定义控制器自动进行证书审批工作。

参考:
K8S集群安全机制

参考:
证书和证书签名请求
(详细步骤参加k8s官方文档)