In the end, learn python from scratch (18). If you want to become an APP reverse engineer, what technical points do you need to master?

As the last article of learning python from zero, let's briefly review the content

1. Programming syntax

2. Machine Learning

3. Full stack development

4. Data analysis

5. Cultivation of reptile engineers

Python resources suitable for zero-based learning and advanced people :
① Tencent certified python complete project practical tutorial notes PDF
② More than a dozen major factories python interview topics PDF
③ Python full set of video tutorials (zero-based-advanced JS reverse)
④ Hundreds A project + source code + notes
⑤ programming grammar - machine learning - full stack development - data analysis - reptiles - APP reverse and other full set of projects + documents

APP reverse engineering

1. Java syntax programming

1. Java environment construction

As an APP reverse engineer, you need to build a Java development environment for Java syntax programming. The following are the detailed steps to build the Java environment:

Download the Java Development Kit (JDK) :
Download the version of the JDK for your operating system. Select the version suitable for your operating system and system architecture, and download the installation file.

Install JDKs :

Execute the downloaded JDK installation file and follow the instructions of the installation wizard to install it. During the installation process, you can customize the installation path or use the default path.

Configure environment variables (Windows system) :

  • Open "Control Panel" -> "System and Security" -> "System", click "Advanced System Settings" on the left.
  • In the dialog box that opens, click the "Environment Variables" button.
  • In the User Variables section, click the "New" button and add the following two environment variables:
    • Variable name: JAVA_HOME, variable value: JDK installation path (for example: C:\Program Files\Java\jdk-11.0.12)
    • Variable name: PATH, variable value: %JAVA_HOME%\bin
  • Click "OK" to save the settings.

Configure environment variables (macOS and Linux systems) :

  • Open a terminal and edit the ~/.bash_profile file, you can use any text editor.
  • Add the following lines to the end of the file:
export JAVA_HOME=/Library/Java/JavaVirtualMachines/{jdk版本}/Contents/Home
export PATH=$JAVA_HOME/bin:$PATH

Replace {jdk version} with the folder name of your installed JDK version (eg: jdk-11.0.12).

  • Save the file and execute the following command to make the configuration take effect:
    source ~/.bash_profile

Verify installation :
Open a terminal or command line interface, and enter the following command to check whether the Java environment is successfully installed and configured:

java -version

If you see version information similar to the following output, the Java environment has been successfully built:

java version "11.0.12" 2021-07-20 LTS
Java(TM) SE Runtime Environment 18.9 (build 11.0.12+8-LTS-237)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.12+8-LTS-237, mixed mode)

Now that you have successfully built the Java development environment, you can start to use the Java language to perform APP reverse engineering programming tasks. You can use Java development tools (such as Eclipse, IntelliJ IDEA, etc.) to write and run Java programs.

2. Java basic syntax and data types

When doing APP reverse engineering, it is very important to understand the basic syntax and data types of Java. The following is a detailed explanation of Java's basic syntax and data types:

Identifier : In Java, an identifier is a name used to name a variable, class, method, etc. Identifiers must begin with a letter, underscore, or dollar sign and may be followed by a combination of letters, numbers, underscore, or dollar sign.

Comments : Comments are used to add comments or explanations to the code. In Java, there are three types of annotations:

  • Single-line comment : start with a double slash (//), and the comment content is at the end of the line.
  • Multi-line comments : start with a slash and an asterisk (/), end with an asterisk and a slash (/), and can span multiple lines.
  • Documentation comments : Start with a slash followed by two asterisks (/**) and end with an asterisk followed by a slash (*/), used to generate documentation.

Keywords : Java has some reserved keywords which are used to denote a specific meaning or functionality. Some commonly used keywords include class, public, private, static, void, etc.

Data types : There are two types of data types in Java:

  • Basic data types: including integer types (byte, short, int, long), floating point types (float, double), character types (char), and Boolean types (boolean).
  • Reference data types: including classes, interfaces, arrays, etc.

Variables : In Java, variables are used to store data. When declaring a variable, you need to specify the data type, and then you can assign a value to the variable. Variables can be primitive data types or reference data types.

Operators : There are many types of operators in Java, including arithmetic operators (+, -, *, /, %), assignment operators (=, +=, -=, etc.), comparison operators (==, !=, >, <, etc.), logical operators (&&, ||, !, etc.), etc.

Control Flow Statements : Java provides several control flow statements that are used to control the execution flow of a program.

  • Conditional statement: if statement, switch statement.
  • Loop statements: for loop, while loop, do-while loop.
  • Branch statement: break statement, continue statement, return statement.

Array : An array is a data structure that can hold multiple elements of the same type. In Java, the size of an array is specified at creation time and cannot be changed. Elements in an array can be accessed by index.

These are the main contents of Java's basic syntax and data types. Familiarity with these concepts can help you understand and write Java code, including the analysis and modification of Java code when performing APP reverse engineering.

3.java control process

The Java control flow refers to the selection of different execution paths according to different conditions or situations during the execution of the program. The control flow in Java mainly includes conditional statements and loop statements.

1. Conditional statement

Conditional statements are used to select different execution paths based on different conditions. Conditional statements in Java include if statement, if-else statement, if-else if statement, and switch statement.

if statement :

The if statement is used to determine whether a condition is true, and if it is true, a block of code is executed.

Grammar format:

if (条件) {
    // 执行代码块
}

Sample code:

int a = 10;
if (a > 5) {
    System.out.println("a大于5");
}

if-else statement :

The if-else statement is used to judge whether a condition is true, if it is true, a block of code is executed, otherwise another block of code is executed.

Grammar format:

if (条件) {
    // 执行代码块1
} else {
    // 执行代码块2
}

Sample code:

int a = 3;
if (a > 5) {
    System.out.println("a大于5");
} else {
    System.out.println("a小于等于5");
}

if-else if statement :

The if-else if statement is used to judge multiple conditions. If the first condition is true, the first code block is executed, otherwise the second condition is judged, and so on.

Grammar format:

if (条件1) {
    // 执行代码块1
} else if (条件2) {
    // 执行代码块2
} else {
    // 执行代码块3
}

Sample code:

int a = 3;
if (a > 5) {
    System.out.println("a大于5");
} else if (a > 0) {
    System.out.println("a大于0,小于等于5");
} else {
    System.out.println("a小于等于0");
}

switch statement :

The switch statement is used to select different execution paths according to different conditions, similar to the if-else if statement, but the switch statement can only judge integer, character and enumeration types.

Grammar format:

switch (表达式) {
    case 值1:
        // 执行代码块1
        break;
    case 值2:
        // 执行代码块2
        break;
    ...
    default:
        // 执行代码块n
        break;
}

Sample code:

int a = 2;
switch (a) {
    case 1:
        System.out.println("a等于1");
        break;
    case 2:
        System.out.println("a等于2");
        break;
    default:
        System.out.println("a不等于1或2");
        break;
}

2. Loop statement

Loop statements are used to repeatedly execute a block of code. Loop statements in Java include for loops, while loops, and do-while loops.

for loop :

The for loop is used to repeatedly execute a block of code, and the number of loops can be specified.

Grammar format:

for (初始化; 条件; 更新) {
    // 执行代码块
}

Sample code:

for (int i = 0; i < 5; i++) {
    System.out.println("i的值为:" + i);
}

while loop :

The while loop is used to repeatedly execute a block of code, as long as the condition is true.

Grammar format:

while (条件) {
    // 执行代码块
}

Sample code:

int i = 0;
while (i < 5) {
    System.out.println("i的值为:" + i);
    i++;
}

do-while loop:

The do-while loop is used to repeatedly execute a block of code, first execute the block of code once, and then judge whether the condition is true, if it is true, continue to execute, otherwise exit the loop.

Grammar format:

do {
    // 执行代码块
} while (条件);

Sample code:

int i = 0;
do {
    System.out.println("i的值为:" + i);
    i++;
} while (i < 5);

Four, java data type

Java is a strongly typed language, which means that the data types of variables must be specified when writing code. Data types in Java can be divided into two categories: primitive data types and reference data types.

1. Basic data types

The basic data types in Java include:

  • Integer: byte, short, int, long
  • Floating point type: float, double
  • Character type: char
  • Boolean: boolean

These data types have different value ranges and storage space sizes, as follows:

2. Reference data type

Reference data types in Java include:

  • kind
  • interface
  • array

A variable of reference data type stores a reference to an object, not the object itself. Objects themselves are stored in heap memory, while references are stored in stack memory.

3. Automatic type conversion and forced type conversion

In Java, if two data types are different, automatic type conversion or cast can be done.

Automatic type conversion means that when a value of one data type is assigned to a variable of another data type, Java will automatically convert it to the target type. For example, when assigning a value of type int to a variable of type double, Java will automatically convert the int type to double type.

Casting is the conversion of one data type to another data type. For example, when converting a value of type double to type int, the mandatory type conversion symbol "()" needs to be used.

4. String type

The string type in Java is a reference data type, but Java provides a special syntax to create a string, that is, enclose a piece of text in double quotes. For example:

String str = "Hello, world!";

The string type also provides some commonly used methods, such as:

  • length(): returns the length of the string
  • charAt(int index): returns the character at the specified position
  • substring(int beginIndex, int endIndex): returns the substring within the specified range
  • equals(String str): compares whether two strings are equal

5. Array type

An array in Java is a reference data type that can store multiple values ​​of the same type. Arrays are declared as follows:

Data type [] array name = new data type [array length];

For example, to declare an array of type int with length 5:

int[] arr = new int[5];

The access method of the array is to access by subscript, and the subscript starts from 0. For example, to access the first element in an array:

int first = arr[0];

Arrays also provide some commonly used methods, such as:

  • length: returns the length of the array
  • sort: sort the array
  • toString: Convert the array to a string

Five, java data structure

Java is an object-oriented programming language, so it provides many data structures to process and organize data. The following are commonly used data structures in Java:

  • Array (Array) : An array is a collection of data elements of the same type. They are stored contiguously in memory and can be accessed through an index. Arrays in Java can be one-dimensional or multi-dimensional.

  • Collection (Collection) : A collection is a container for a group of objects that can dynamically increase or decrease elements. Collection frameworks in Java include List, Set, and Map.

  • List (List) : A list is an ordered collection that can contain repeated elements. ArrayList and LinkedList in Java are commonly used list implementations.

  • Set (Set) : A set is a collection that does not allow duplicate elements. HashSet and TreeSet in Java are commonly used set implementations.

  • Mapping (Map) : A map is a collection of key-value pairs. HashMap and TreeMap in Java are commonly used mapping implementations.

  • Stack : A stack is a last-in-first-out (LIFO) data structure. The Stack class in Java implements the basic operations of the stack.

  • Queue : A queue is a first-in-first-out (FIFO) data structure. The LinkedList class in Java implements the basic operations of the queue.

  • Tree (Tree) : A tree is a hierarchical structure, each node can have multiple child nodes. TreeSet and TreeMap in Java are tree-based implementations of sets and maps.

  • Graph : A graph is a data structure composed of nodes and edges. There is no built-in graph implementation in Java, but third-party libraries can be used to implement it.

The above are commonly used data structures in Java. Understanding their characteristics and usage can help us better process and organize data.

Six, java object-oriented

As an APP reverse engineer, understanding Java's object-oriented programming is crucial. Object-Oriented Programming (OOP for short) is a programming paradigm that uses objects in programs as basic units and completes tasks through interactions between objects. Here is a detailed explanation of Object Oriented Programming in Java:

  • Classes and Objects : Classes are blueprints or templates that define objects, and objects are instances of classes. The class contains the properties (fields/member variables) and behaviors (methods/member functions) of the object. By creating an instance (object) of a class, we can call methods defined in the class to manipulate the object.

  • Encapsulation : Encapsulation is a concept that combines data and methods in order to hide the specific implementation details of the data and provide a common interface to access the data. Through accessor (getter) and modifier (setter) methods, you can control access to and modification of the internal data of the object.

  • Inheritance : Inheritance means that a class (subclass/derived class) can inherit the properties and methods of another class (parent class/base class). Subclasses can use the methods in the parent class, and can add new properties and methods to them, or modify the methods of the parent class. Inheritance facilitates code reuse and extension.

  • Polymorphism : Polymorphism means that an object can appear in multiple forms. Through polymorphism, you can use the reference variable of the parent class type to refer to the instance object of the subclass. Polymorphism allows calling the same method to produce different behaviors on different objects, providing code flexibility and scalability.

  • Abstract class : An abstract class refers to a class that cannot be instantiated. It defines a set of abstract methods that subclasses must implement to instantiate. An abstract class provides a template or constraint for defining the general characteristics of a class.

  • Interface : An interface is a pure abstract class that contains only method declarations but no method implementations. A class can implement one or more interfaces and thus obtain the methods defined in the interfaces. Interfaces provide a behavioral contract for polymorphism and code decoupling.

  • Constructor : A constructor is a special method used to create and initialize an object. It has the same name as the class and has no return type. Through the constructor, you can set the initial state and properties of the object.

  • Member variables and local variables : Member variables are variables defined in a class and can be accessed by all methods of the class. A local variable is a variable defined in a method and can only be accessed within the method.

By understanding and applying the concepts of object-oriented programming, you can better organize and design your code so that it is easier to understand, extend, and maintain. This is crucial for reverse engineering apps and writing high-quality Java code.

Seven, java inheritance relationship chain

In the Java language, the inheritance relationship is one of the important concepts of object-oriented programming. Inheritance relationships form a class hierarchy, known as an inheritance relationship chain. Let me explain in detail the related concepts of Java inheritance relationship chain:

  • Class (Class) : A class is the basic programming unit in Java, which is used to define the properties and methods of objects. A class can be the parent or superclass of another class and can be inherited by other classes.

  • Parent class (Superclass) and subclass (Subclass) : In the inheritance relationship, the parent class refers to the inherited class, and the subclass refers to the class that inherits the parent class. A parent class is also called a superclass or a base class, and a subclass is also called a derived class.

  • Inheritance : By using the keyword extends, a class can inherit the properties and methods of another class. Subclasses inherit the characteristics of the parent class, including fields, methods, and constructors. Inheritance enables code reuse and extension.

  • Single Inheritance (Single Inheritance) : Classes in Java only support single inheritance, that is, a class can only have one direct parent class. This is to avoid the complexity and conflict issues that multiple inheritance may cause.

  • Multilevel Inheritance (Multilevel Inheritance) : Multilevel inheritance means that the inheritance relationship can form multiple levels. A class can be a subclass of another class and at the same time be the parent class of other classes, forming a chain of inheritance relationships.

  • Override : Subclasses can override existing methods in the parent class by defining the same method name and parameter list. When calling this method of a subclass object, the method defined in the subclass will be executed instead of the method in the superclass.

  • Call the parent class method (Superclass Method Invocation) : Subclasses can use the super keyword to call the method of the parent class. This is typically used when a subclass wants to use the superclass's implementation in an overridden method.

  • Abstract Class (Abstract Class) : An abstract class is a class that cannot be instantiated, and it defines a set of abstract methods. An abstract class can be inherited by other classes as a parent class, and subclasses must implement the abstract methods in the abstract class.

  • Interface (Interface) : An interface is a pure abstract class that only contains method declarations but no method implementations. A class can implement one or more interfaces and thus obtain the methods defined in the interfaces.

Through the inheritance relationship chain, we can establish a hierarchical structure between different classes, so as to realize code reuse and extension. Subclasses can inherit the properties and methods of the parent class, and can add new properties and methods, or override the methods of the parent class. This makes the code more organized and easier to understand and maintain. As an APP reverse engineer, understanding and applying the inheritance relationship chain will help you analyze and modify Java code.

Eight, the concept of java package

Package (Package) in Java is a mechanism for organizing classes and interfaces, which organizes related classes and interfaces together for better management and use. A package can be thought of as a folder that contains a set of related classes and interfaces.

Packages in Java have the following functions:

  • Avoid naming conflicts : Packages in Java can avoid naming conflicts because different packages can have the same class name.

  • Organize classes and interfaces : Packages in Java can organize related classes and interfaces together for easy management and use.

  • Access Control : Packages in Java can use access modifiers to control access to classes and interfaces.

  • Provide namespaces : Packages in Java provide namespaces to avoid naming conflicts between different classes and interfaces.

The naming rule for packages in Java is to use lowercase letters and use dots (.) to separate multiple words, for example: com.example.mypackage.

In Java, the syntax of using a package is to use the package statement at the beginning of the class to declare the package to which it belongs, for example:

package com.example.mypackage;

public class MyClass {
    // 类的代码
}

When using classes in other packages, you need to use the import statement to import the class, for example:

import java.util.ArrayList;

public class MyClass {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<String>();
        // 使用ArrayList类
    }
}

It should be noted that packages in Java are organized in the form of folders, so the package name and folder name must be consistent. For example, a class with the package name com.example.mypackage should be stored in a .java file in the com/example/mypackage directory.

NDK Development Topics

1. NDK data types

NDK (Native Development Kit) is a development tool provided by Android, which allows developers to write Android applications in C/C++ language. In NDK, data type is a very important concept. Let's introduce the data type in NDK in detail.

1. Basic data types

In NDK, the basic data types are the same as those in C/C++ language, including int, float, double, char, etc. These data types are used in NDK in the same way as in C/C++ language.

2. Pointer type

The pointer type is also a very important data type in NDK, it can point to any type of data. In NDK, pointer types are declared in the same way as in C/C++ language, for example:

int *p;

3. Structure type

The structure type is also a very common data type in NDK, which can combine multiple different types of data together. In NDK, the declaration method of structure type is the same as that in C/C++ language, for example:

struct Person {
    char name[20];
    int age;
    float height;
};

4. Enumerated type

Enumerated types are also very common data types in NDK, which can group a group of related constants together. In NDK, enumeration types are declared in the same way as in C/C++ language, for example:

enum Color {
    RED,
    GREEN,
    BLUE
};

5. Array type

The array type is also a very common data type in NDK, which can combine multiple data of the same type. In NDK, the declaration method of array type is the same as that in C/C++ language, for example:

int arr[10];

6. Pointer to function type

The pointer type to function is also a very common data type in NDK, it can point to any type of function. In the NDK, the declaration method of the pointer type to the function is the same as that in the C/C++ language, for example:

int (*p)(int, int);

The above are the common data types in NDK. Developers need to master the usage of these data types when using NDK for development.

2. Combination of java reflection and NDK

The combination of Java reflection and NDK can realize some advanced functions, such as calling methods of Java classes or obtaining property values ​​​​of Java classes at the NDK level. The following is a detailed introduction to the implementation of the combination of Java reflection and NDK.

1. Define the method or property that needs to be called at the Java level

First, define the method or property that needs to be called at the Java level, for example:

public class Test {
    private int value;

    public Test(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}

2. Use reflection to get the Class object of the Java class

At the NDK level, you need to use reflection to obtain the Class object of the Java class, for example:

jclass clazz = env->FindClass("com/example/Test");

3. Get the constructor of the Java class

Use reflection to get the constructor of a Java class, for example:

jmethodID constructor = env->GetMethodID(clazz, "<init>", "(I)V");

4. Create Java objects

Create a Java object using reflection, for example:

jobject obj = env->NewObject(clazz, constructor, 10);

5. Get the method of Java class

Use reflection to get methods of Java classes, for example:

jmethodID getValueMethod = env->GetMethodID(clazz, "getValue", "()I");
jmethodID setValueMethod = env->GetMethodID(clazz, "setValue", "(I)V");

6. Call the method of the Java class

Use reflection to call a method of a Java class, for example:

int value = env->CallIntMethod(obj, getValueMethod);
env->CallVoidMethod(obj, setValueMethod, 20);

7. Get the attribute value of the Java class

Use reflection to get the property value of a Java class, for example:

jfieldID valueField = env->GetFieldID(clazz, "value", "I");
int value = env->GetIntField(obj, valueField);

8. Set the attribute value of the Java class

Use reflection to set property values ​​of Java classes, for example:

env->SetIntField(obj, valueField, 30);

3. JNI calls java function objects and accesses java methods and classes

1. JNI calls the java function object

In JNI, methods in Java can be called through the JNIEnv object. The JNIEnv object is a pointer to the JNI environment, which provides a set of functions that can be used to access Java objects and call Java methods.

Before calling a Java method, you need to obtain the ID of the Java method. The ID of a Java method can be obtained by calling the GetMethodID function of the JNIEnv object. The parameters of the GetMethodID function include the object of the Java class, the name of the Java method, and the signature of the Java method.

After obtaining the ID of the Java method, you can call the Java method by calling the CallXXXMethod function of the JNIEnv object. Among them, XXX represents the return value type of the Java method, which can be Int, Boolean, Object, etc.

The following is a sample code of JNI calling a Java method:

JNIEXPORT void JNICALL Java_com_example_test_TestJNI_callJavaMethod(JNIEnv *env, jobject obj) {
    jclass clazz = env->GetObjectClass(obj);
    jmethodID methodId = env->GetMethodID(clazz, "javaMethod", "()V");
    env->CallVoidMethod(obj, methodId);
}

In the above code, first get the object of the Java class through the GetObjectClass function, then get the ID of the Java method through the GetMethodID function, and finally call the Java method through the CallVoidMethod function.

It should be noted that when JNI calls a Java method, it is necessary to ensure that the access permission of the Java method is public. Otherwise, an IllegalAccessException will be thrown when calling the Java method.

2. Access java methods and classes

As an APP reverse engineer, it is very important to master the concepts of Java methods and classes. Below I will explain in detail about methods and classes in Java:

1. Method (Method) :

  • A method is a reusable block of code that performs a specific action or completes a specific task.
  • A method consists of a method name, a parameter list (optional), a return type, and a method body.
  • The method name is used to uniquely identify the method, the parameter list is the value passed to the method, and the return type specifies the type of value returned by the method.
  • A method can accept parameters and return a value, or it can be a no-return method with no parameters.
  • Methods are executed by invocation (using the method name and parameter list).

2. Class (Class) :

  • A class is a template or blueprint for creating an object, which defines its properties and behavior.
  • Class is the basic programming unit in Java, and all objects are instantiated from classes.
  • A class consists of fields (member variables) and methods (member functions).
  • Fields are variables in a class that store state information about an object.
  • Methods are behaviors defined in a class that manipulate the state of an object.

3. Object (Object) :

  • Objects are instances of classes, created by using the keyword new and the class's constructor.
  • Objects have the properties and behaviors defined by the class, and the fields and methods of the object can be accessed and manipulated.
  • Objects can exist independently, have a unique state, and interact with other objects.

4. Construction method (Constructor) :

  • A constructor is a special method used to create and initialize an object.
  • The constructor has the same name as the class, but has no return type.
  • The constructor is called when an object is created using the new keyword and is used to initialize the state of the object.
  • Multiple constructors can be defined in Java, distinguished by different parameter lists.

5. Static Method :

  • Static methods are class-level methods that can be called without creating an instance of the class.
  • Static methods are decorated with the keyword static, and can only access static member variables and call other static methods.
  • Static methods are typically used for utility functions and utility methods that don't need to depend on a specific object.

6. Encapsulation :

  • Encapsulation is the combination of data and methods in a single unit for data hiding and access control.
  • Encapsulation restricts access to fields and methods of a class by using access modifiers such as private, public, etc.

7. Inheritance :

  • Inheritance is an object-oriented concept that allows a class to inherit properties and methods from another class.
  • Inheritance is achieved by using the keyword extends, and subclasses inherit the characteristics of the parent class and can add their own functions.
  • Inheritance provides a mechanism for code reuse and extension, and implements a hierarchical structure between classes.

8. Polymorphism :

  • Polymorphism is an important concept in object-oriented programming that allows the use of an object to appear in many different ways.
  • Polymorphism is achieved by using reference variables of the superclass type to refer to objects of the subclass.
  • Polymorphism provides flexibility and scalability. Through dynamic binding of methods, which method to call can be decided at runtime.

Mastering the concept of methods and classes is the key to reverse engineering APP. Knowing how to create and use methods, how to define and instantiate classes, and how to use concepts such as encapsulation, inheritance, and polymorphism will enable you to better analyze and modify Java code.

Three, Hook framework topic

Frida Hook

Frida is a powerful dynamic analysis tool that can be used in APP reverse engineering, security testing, vulnerability mining, etc. This article will explain in detail from six directions: Frida development and debugging environment construction, Frida structure array-object, Frida and unpacking, Frida Hook shell and plug-in dex, Frida compiling source code, Frida detection and anti-debugging.

1. Frida development and debugging environment setup

1. Install Frida

Frida supports multiple platforms such as Windows, macOS, Linux, etc. You can download the installation package for the corresponding platform from the official website for installation. After the installation is complete, you can enter the frida -version command on the command line to check whether Frida is installed successfully.

2. Install Frida-Server

Frida-Server is the core component of Frida, which runs on the target device and communicates with the Frida client. When using Frida for APP reverse engineering, Frida-Server needs to be installed on the target device. For the installation method of Frida-Server, please refer to the official documentation.

3. Install Frida-Tools

Frida-Tools is a set of tools for Frida, including frida-ps, frida-trace, frida-discover, etc. These tools can help us more conveniently use Frida for dynamic analysis. You can enter the pip install frida-tools command on the command line to install Frida-Tools.

2. Frida constructs an array-object

When using Frida for APP reverse engineering, it is often necessary to construct some arrays and objects for data manipulation. Some commonly used construction methods are introduced below.

1. Construct an array

Arrays can be constructed using the Java.array() method provided by Frida. For example, to construct an int array of length 3:

var arr = Java.array("int", [1, 2, 3]);

2. Construct objects

Objects can be constructed using the Java.use() method provided by Frida. For example, to construct a java.lang.String object:

var str = Java.use("java.lang.String").$new("hello");

3. Frida and shelling

When performing APP reverse engineering, it is often necessary to unpack the APP for better analysis. Here are some common unpacking methods.

1. Use Frida-Objection for shelling

Frida-Objection is a Frida-based mobile device security testing framework, which can be used for APP shelling, data leakage detection, SSL Pinning bypass, etc. Frida-Objection can be installed by entering the pip install objection command on the command line.

The steps to unpack using Frida-Objection are as follows :

  • 1) Start Frida-Server

Start Frida-Server on the target device.

  • 2) Start Frida-Objection

Enter the objection explore command on the command line to start Frida-Objection.

  • 3) Enter APP

Enter the android jailbreak command in the command line of Frida-Objection to enter the APP.

  • 4) Shelling

Enter the dump -h command on the Frida-Objection command line to unpack.

2. Use Frida for shelling

You can use the Java.perform() method provided by Frida to perform shelling operations. Here is an example of a simple unpacking script:

Java.perform(function () {
    var BaseDexClassLoader = Java.use("dalvik.system.BaseDexClassLoader");
    var DexFile = Java.use("dalvik.system.DexFile");
    var PathClassLoader = Java.use("dalvik.system.PathClassLoader");
    var DexClassLoader = Java.use("dalvik.system.DexClassLoader");
    var File = Java.use("java.io.File");
    var System = Java.use("java.lang.System");
    var Runtime = Java.use("java.lang.Runtime");
    var Process = Java.use("android.os.Process");
    var String = Java.use("java.lang.String");
    var Arrays = Java.use("java.util.Arrays");

    var classLoader = Java.classFactory.loader;
    var dexFile = DexFile.$new("/data/app/com.example.app-1/base.apk");
    var dexElements = BaseDexClassLoader.getDeclaredField("dexElements");
    dexElements.setAccessible(true);
    var elements = dexElements.get(classLoader);
    var newElements = Arrays.copyOf(elements, elements.length);
    var dex = DexFile.$new(File.$new("/data/data/com.example.app/files/1.dex"));
    var newElement = DexClassLoader.$new("/data/data/com.example.app/files", null, null, classLoader).findClass("com.example.app.MainActivity").getDeclaredMethod("main", String.arrayOf(String));
    newElements[newElements.length - 1] = newElement.invoke(null, null);
    dexElements.set(classLoader, newElements);

    System.loadLibrary("native-lib");
    var handle = Runtime.getRuntime().exec("su");
    var outputStream = handle.getOutputStream();
    outputStream.write(("kill " + Process.myPid() + "\n").getBytes());
    outputStream.flush();
    outputStream.close();
});

4. Frida Hook shell and plug-in dex

When performing APP reverse engineering, it is often necessary to hook some shells and plug-in dex for better analysis. Some commonly used Hook methods are introduced below.

1.Hook shell

You can use the Java.use() method provided by Frida to hook the shell. The following is an example of a simple Hook shell script:

Java.perform(function () {
    var BaseDexClassLoader = Java.use("dalvik.system.BaseDexClassLoader");
    var DexFile = Java.use("dalvik.system.DexFile");
    var PathClassLoader = Java.use("dalvik.system.PathClassLoader");
    var DexClassLoader = Java.use("dalvik.system.DexClassLoader");
    var File = Java.use("java.io.File");
    var System = Java.use("java.lang.System");
    var Runtime = Java.use("java.lang.Runtime");
    var Process = Java.use("android.os.Process");
    var String = Java.use("java.lang.String");
    var Arrays = Java.use("java.util.Arrays");

    var classLoader = Java.classFactory.loader;
    var dexFile = DexFile.$new("/data/app/com.example.app-1/base.apk");
    var dexElements = BaseDexClassLoader.getDeclaredField("dexElements");
    dexElements.setAccessible(true);
    var elements = dexElements.get(classLoader);
    var newElements = Arrays.copyOf(elements, elements.length);
    var dex = DexFile.$new(File.$new("/data/data/com.example.app/files/1.dex"));
    var newElement = DexClassLoader.$new("/data/data/com.example.app/files", null, null, classLoader).findClass("com.example.app.MainActivity").getDeclaredMethod("main", String.arrayOf(String));
    newElements[newElements.length - 1] = newElement.invoke(null, null);
    dexElements.set(classLoader, newElements);

    System.loadLibrary("native-lib");
    var handle = Runtime.getRuntime().exec("su");
    var outputStream = handle.getOutputStream();
    outputStream.write(("kill " + Process.myPid() + "\n").getBytes());
    outputStream.flush();
    outputStream.close();
});

2. Hook plugin dex

You can use the Java.use() method provided by Frida to hook the plugin dex. The following is an example of a simple Hook plugin dex script:

Java.perform(function () {
    var classLoader = Java.classFactory.loader;
    var dexFile = Java.use("dalvik.system.DexFile").$new("/data/data/com.example.app/files/1.dex");
    var dexElements = Java.cast(classLoader, Java.use("dalvik.system.BaseDexClassLoader")).pathList.dexElements;
    var newElements = Java.array("dalvik.system.DexFile$Element", [dexFile.entries()[0]]);
    var elements = Java.array("dalvik.system.DexFile$Element", dexElements);
    var allElements = Java.array("dalvik.system.DexFile$Element", [newElements[0], elements[0]]);
    Java.cast(classLoader, Java.use("dalvik.system.BaseDexClassLoader")).pathList.dexElements = allElements;
});

Five, Frida compiled source code

When performing APP reverse engineering, it is often necessary to compile some source code for better analysis. Some commonly used compilation methods are introduced below.

1. Compile with dex2jar

dex2jar is a tool for converting dex files into jar files, which can be used to convert APP's dex files into readable jar files. You can enter the dex2jar.sh command on the command line to compile with dex2jar.

2. Use apktool to compile

apktool is a tool for decompiling and repackaging APK, which can be used to decompile and repackage APP. You can enter the apktool d command on the command line to decompile the APP, and enter the apktool b command to repackage the APP.

Six, Frida detection anti-debugging

When performing APP reverse engineering, it is often necessary to detect whether the APP has been anti-debugged. Some commonly used detection methods are introduced below.

1. Hook anti-debugging method

You can use the Java.use() method provided by Frida to hook the anti-debugging method. The following is a simple Hook anti-debugging method script example:

Java.perform(function () {
    var Debug = Java.use("android.os.Debug");
    Debug.isDebuggerConnected.implementation = function () {
        return false;
    };
});

2. Hook anti-debugging variables

You can use the Java.use() method provided by Frida to hook anti-debugging variables. Below is an example of a simple Hook anti-debugging variable script:

Java.perform(function () {
    var ActivityThread = Java.use("android.app.ActivityThread");
    ActivityThread.currentApplication.implementation = function () {
        var app = this.currentApplication();
        var packageName = app.getPackageName();
        if (packageName.indexOf("com.example.app") != -1) {
            return null;
        } else {
            return app;
        }
    };
});

The above is the detailed explanation of the Frida HOOk topic, and I hope it will be helpful to APP reverse engineering enthusiasts.

APP shell processing

1. Explanation of the principle of APP reinforcement

APP reinforcement refers to the security reinforcement processing of mobile applications to improve their protection capabilities and prevent malicious behaviors such as reverse engineering, tampering, and data leakage by attackers. The following is a detailed explanation of the principle of APP reinforcement:

  • Code obfuscation : Code obfuscation is to modify and optimize the source code to make the code structure and logic complex and difficult to understand, thus increasing the difficulty of reverse analysis of the application by the attacker. By renaming variables and function names, adding redundant code, modifying control flow, etc., it is difficult for attackers to understand the meaning and execution flow of the code, thereby reducing the success rate of reverse engineering.

  • Encryption protection : Encryption protection is to encrypt sensitive data or key codes to prevent attackers from directly obtaining and modifying data. Common encryption methods include encrypting character strings, configuration files, algorithms, and network communications, and using symmetric or asymmetric encryption algorithms to protect data confidentiality and integrity.

  • Secure storage : Secure storage refers to storing sensitive data of applications (such as user credentials, keys, certificates, etc.) in encrypted containers to prevent them from being obtained by malicious applications or attackers. Hardening tools can provide secure storage, encrypt sensitive data, and use keys to protect data security.

  • Anti-debugging and anti-dynamic analysis : In order to prevent attackers from debugging and dynamically analyzing applications to obtain key information, hardening tools can use anti-debugging and anti-dynamic analysis techniques. For example, methods such as detecting the existence of debuggers, monitoring changes in the operating environment, and preventing the injection of dynamically loaded libraries can prevent applications from being analyzed and modified by attackers.

  • Security authentication and anti-tampering : In order to ensure the legitimacy and integrity of the application, the hardening tool can perform security authentication through digital signature, digest verification, and code integrity verification. These technologies prevent applications from being tampered with, implanted with malicious code, or replaced by malicious applications.

  • Runtime protection : Aiming at the security issues during the running of the application, hardening tools can provide a runtime protection mechanism. For example, detect and prevent runtime attacks such as memory overflow, buffer overflow, code injection, and illegal calls of dynamically loaded libraries, so as to improve the security of applications.

It should be noted that although APP reinforcement can improve the security of the application, it cannot completely eliminate all security risks. Attackers may use advanced reverse engineering techniques and vulnerabilities to attack hardened applications. Therefore, developers and security teams should use a combination of security technologies and strategies to achieve comprehensive application security protection.

2. How to identify whether the APP is packed

To identify whether an APP is packed, you can consider the following methods:

  • Static analysis : By statically analyzing the APP and checking the file structure and content of the APP, you can preliminarily judge whether there is a case of packing. Packing usually inserts a piece of code into the original binary for unpacking, so try to find similar signatures to the unpacking code.

  • Dynamic debugging : Use debugging tools to dynamically debug the APP, observe its behavior and running process, and obtain more information to determine whether to pack. For example, detect whether there is unpacking behavior, view the unpacking code in memory, monitor the dynamic library loaded by the application, etc.

  • Decompilation analysis : Decompile the APP to restore the source code or similar source code. You can judge whether it has been packed by analyzing the decompiled code. Packing tools usually insert some special instructions and function calls into the code, or rewrite and reconstruct the code, all of which can be judged through decompilation analysis.

  • Use special tools : There are some tools specially designed to detect and identify packers to assist in judgment. For example, tools such as AndroGuard, Frida, and IDA Pro all have certain capabilities to analyze and identify packing situations.

It should be noted that packing technology continues to develop and evolve, and new packing methods may bypass some conventional identification methods. Therefore, identifying packers is an ongoing challenge that requires a combination of multiple methods and tools, and does not rely on a single basis for judgment.

Three, frida-dump shelling skills

Frida-dump is a tool for unpacking (dump) packed applications developed based on the Frida framework. It can dynamically inject the application at runtime, bypass the hardening protection measures, and extract the decrypted memory image of the application. The following is a detailed explanation of the Frida-dump unpacking technique:

  • Frida environment setup : First, you need to install the Frida framework on the computer and configure the operating environment. For details, please refer to the installation and configuration guide provided by the official Frida documentation.

  • Locating hardening points : Before using Frida-dump, you need to understand the hardening scheme used by the target application. For different hardening schemes, it may be necessary to use specific Frida scripts to bypass protection mechanisms. For common hardening schemes such as DEX encryption, function decryption, etc., the location of hardening points can be confirmed through static analysis or debugging.

  • Writing Frida scripts : Depending on the hardening scheme of the target application, a custom Frida script can be written to locate and bypass hardening points while the application is running. Frida scripts are written in JavaScript and can use the API provided by Frida for code injection and dynamic debugging.

  • Dynamic injection and unpacking : use the written Frida script, run the target application, and inject the script into the application process. The script will be automatically executed when the application is running, and the decrypted memory area will be located by monitoring memory access, and exported to the local file system to complete the unpacking operation.

It should be noted that unpacking using Frida-dump is an advanced technique that requires some understanding of the internal structure and hardening mechanisms of mobile applications. Additionally, unpacking may have legal and ethical implications, so it should be used with caution and only for legitimate security assessment and research purposes. In addition, the unpacking operation using unpacking tools such as Frida-dump may also trigger the application's own anti-debugging and anti-cracking mechanism, resulting in unpacking failure or abnormal termination of the application.

smail syntax

Knowing the smali syntax is very important when reverse engineering an app. Below I will introduce in detail the data types in the smali syntax, the constant declaration syntax, the declaration of method functions, the declaration of constructors, the declaration of static code blocks, and the call based on interfaces.

1. Data type:

smali supports the following data types:

  • Z: boolean type
  • B: byte type
  • S: short type
  • C: char type
  • I: int type
  • J: long type
  • F: float type
  • D: double type
    L class name;: reference type
    For example, I means int type, Ljava/lang/String; means the reference type is java.lang.String.

2. Constant declaration syntax:

The constant declaration uses the const instruction, the syntax is as follows:

const 寄存器, 常量值

For example, const v0, 10 means to set register v0 to the integer constant value 10.

3. Declaration of method function:

Method function declarations use the method keyword, and the syntax is as follows:

.method 修饰符 返回类型 方法名(参数列表) [异常列表]

    方法体

.end method

Modifiers include: public, private, protected, abstract, static, etc.
The return type can be any data type, represented by the data types mentioned above.
The parameter list is separated by commas, and each parameter consists of a type and a variable name.

For example,

.method public static add(II)I

    ...

.end method

Represents a public static method named add that accepts two parameters of type int and returns a value of type int.

Fourth, the declaration of the constructor:

The constructor declaration uses the constructor keyword, the syntax is as follows:

.constructor 修饰符(参数列表) [异常列表]

    构造函数体

.end constructor

Constructors and method functions are declared similarly, but without a return type.

For example,

.constructor public <init>()V

    ...

.end constructor

Represents a public parameterless constructor.

5. Declaration of static code block:

Static code blocks are declared using the clinit keyword, and the syntax is as follows:

.clinit

    静态代码块体

.end clinit

Static code blocks are executed when the class is loaded, and will only be executed once.

For example,

.clinit

    # 执行静态代码块的操作

.end clinit

Represents a static code block.

6. Interface-based call:

In smali, use the invoke-interface command to call the method of the interface. The syntax is as follows:

invoke-interface {参数列表}, 接口类型->方法签名

Among them, the parameter list is the parameters passed to the method, the interface type is the interface type to be called, and the method signature is the descriptor of the method to be called.

For example,

invoke-interface {v0}, Ljava/util/Iterator;->next()Ljava/lang/Object;

Indicates calling the next method of the java.util.Iterator interface.

These are detailed introductions about data types, constant declarations, method function declarations, constructor declarations, static code block declarations, and interface-based calls in smali syntax. By understanding these grammar rules, you can better analyze and modify smali code in reverse engineering. Remember, the smali syntax is large and complex and requires constant practice and exploration to become proficient.

APP RPC topic

1. Write frida rpc plug-in

Frida is a powerful dynamic analysis tool that can be used to reverse engineer and debug mobile applications. Among them, RPC (Remote Procedure Call) is an important function of Frida, which allows us to communicate between Frida client and Frida server, so as to achieve more flexible and efficient dynamic analysis.

In this article, we'll describe how to write a Frida RPC plugin to communicate between a Frida client and a Frida server.

1. Create the Frida RPC plugin

First, we need to create a Frida RPC plugin. In Frida, an RPC plugin is a JavaScript file that can be loaded and executed via RPC communication between the Frida client and the Frida server.

Here is an example of a simple Frida RPC plugin:

rpc.exports = {
  add: function (a, b) {
    return a + b;
  },
  sub: function (a, b) {
    return a - b;
  }
};

In this example, we define two functions add and sub, which implement addition and subtraction respectively. These functions can be called through RPC communication between Frida client and Frida server.

2. Load the Frida RPC plugin

Once we have created the Frida RPC plugin, we can load it to communicate between the Frida client and the Frida server. In the Frida client, we can load the Frida RPC plugin with the following code:

const rpc = new frida.Rpc(peer);
const plugin = rpc.use("path/to/plugin.js");

In this example, we first create a Frida RPC object rpc, and then use the use method to load the Frida RPC plugin. The peer parameter specifies the address and port number of the Frida server.

3. Call the Frida RPC plugin

Once we have loaded the Frida RPC plugin, we can call the functions defined in it through the RPC communication between the Frida client and the Frida server. In the Frida client, we can use the following code to call functions in the Frida RPC plugin:

const result = await plugin.add(1, 2);
console.log(result);

In this example, we call the add function in the Frida RPC plugin and pass two parameters 1 and 2. The result of the call will be stored in the result variable and printed to the console.

4. Summary

In this article, we covered how to write a Frida RPC plugin and communicate between a Frida client and a Frida server. By using the Frida RPC plugin, we can achieve more flexible and efficient dynamic analysis to better understand and secure mobile applications.

2. Use frida to remotely call java code

Before using Frida to remotely call Java code, you need to understand some basic concepts and steps.

1. What is Frida?

Frida is a JavaScript-based dynamic code injection tool that can be used to dynamically analyze and modify Android and iOS applications. Frida can modify the behavior of the application at runtime, including function calls, parameter passing, return values, etc.

2. What is RPC?

RPC (Remote Procedure Call) is a remote procedure call protocol that allows clients to call functions on remote servers just like calling local functions. RPC allows applications to communicate on different computers to achieve distributed computing.

3. How to use Frida for RPC?

Frida provides an RPC mechanism that enables JavaScript code to communicate between local and remote devices. Through RPC, you can write JavaScript code on the local device, and then execute the code on the remote device to realize the function of remotely calling Java code.

The following are the detailed steps to use Frida to remotely call Java code:

  • Write JavaScript code on the local device to realize the function of calling Java code remotely. For example, you can use the Java.perform() method to obtain a Java virtual machine instance and call a method of a Java class.
Java.perform(function() {
    var MainActivity = Java.use("com.example.MainActivity");
    MainActivity.sayHello.implementation = function() {
        console.log("Hello from Frida!");
        return this.sayHello();
    }
});
  • Save the JavaScript code as a file, such as rpc.js.

  • Install Frida on the remote device and start the Frida service. The Frida service can be started with the following command:

frida-server -l 0.0.0.0
  • Launch the target application on the remote device and record the application's process ID.

  • Use the Frida command line tool on the local device to connect to the remote device and load the JavaScript code. You can connect to a remote device with the following command:

frida -U -f com.example.app -l rpc.js

Among them, com.example.app is the package name of the target application, and rpc.js is the file name for saving the JavaScript code.

  • Execute JavaScript code on the local device to realize the function of remotely calling Java code. JavaScript code can be executed in the Frida console with the following command:
rpc.exports.sayHello();

Among them, sayHello() is the function name defined in the JavaScript code.

Through the above steps, you can use Frida to remotely call Java code. It should be noted that Frida's RPC mechanism can only communicate between local and remote devices, and cannot communicate between different remote devices.

Three, sekiro framework source code dismantling

The Sekiro framework is a remote call framework based on Netty and SpringBoot, which can realize cross-process and cross-platform remote calls. When using the Sekiro framework, we need to write code for the server and client, where the server needs to implement the SekiroServer interface, and the client needs to implement the SekiroClient interface.

Let's disassemble the source code of the Sekiro framework to understand how it implements remote calls.

1. SekiroServer

SekiroServer is the server interface of the Sekiro framework, which defines the following methods:

public interface SekiroServer {
    void start() throws Exception;
    void stop() throws Exception;
    void registerHandler(String uri, SekiroRequestHandler handler);
}

Among them, start()the method is used to start the server, stop()the method is used to stop the server, and registerHandler()the method is used to register the request handler.

2.SekiroRequestHandler

SekiroRequestHandler is the request handler interface of the Sekiro framework, which defines the following methods:

public interface SekiroRequestHandler {
    void handle(SekiroRequest sekiroRequest, SekiroResponse sekiroResponse);
}

Among them, handle()the method is used to process the request and write the processing result into the response.

3. SekiroClient

SekiroClient is the client interface of the Sekiro framework, which defines the following methods:

public interface SekiroClient {
    void start() throws Exception;
    void stop() throws Exception;
    SekiroResponse invokeSync(String uri, SekiroRequest sekiroRequest) throws Exception;
}

Among them, start()the method is used to start the client, stop()the method is used to stop the client, and invokeSync()the method is used to call the remote service synchronously.

4.SekiroRequest

SekiroRequest is the request object of the Sekiro framework, which contains the requested URI, request parameters and other information.

5. SekiroResponse

SekiroResponse is the response object of the Sekiro framework, which contains the response status code, response data and other information.

6.SekiroServerHandler

SekiroServerHandler is the server-side processor of the Sekiro framework, which inherits Netty's SimpleChannelInboundHandler class to handle client requests.

In SekiroServerHandler, we can see the following code:

@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
    if (msg instanceof FullHttpRequest) {
        FullHttpRequest request = (FullHttpRequest) msg;
        String uri = request.uri();
        if (uri.startsWith("/api/")) {
            handleHttpRequest(ctx, request);
        } else {
            ctx.fireChannelRead(msg);
        }
    } else {
        ctx.fireChannelRead(msg);
    }
}

This code is used to determine whether the URI of the request starts with "/api/", if so, call the handleHttpRequest()method to process the request, otherwise pass the request to the next processor.

In handleHttpRequest()the method, we can see the following code:

SekiroRequest sekiroRequest = SekiroRequestParser.parse(request);
SekiroResponse sekiroResponse = new SekiroResponse();
sekiroResponse.setRequestId(sekiroRequest.getRequestId());
sekiroResponse.setVersion(sekiroRequest.getVersion());
sekiroResponse.setStatusCode(200);
sekiroResponse.setContentType("application/json;charset=UTF-8");
try {
    SekiroRequestHandler handler = handlerMap.get(sekiroRequest.getUri());
    if (handler != null) {
        handler.handle(sekiroRequest, sekiroResponse);
    } else {
        sekiroResponse.setStatusCode(404);
        sekiroResponse.setContent("{\"message\":\"No handler found for uri: " + sekiroRequest.getUri() + "\"}");
    }
} catch (Exception e) {
    sekiroResponse.setStatusCode(500);
    sekiroResponse.setContent("{\"message\":\"" + e.getMessage() + "\"}");
}

This code is used to parse the request, create a response object, and find the corresponding request processor according to the URI of the request. If found, the request processor is called to process the request, otherwise a 404 error is returned.

7.SekiroClientHandler

SekiroClientHandler is the client processor of the Sekiro framework, which inherits Netty's SimpleChannelInboundHandler class to handle server-side responses.

In SekiroClientHandler, we can see the following code:

@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
    if (msg instanceof FullHttpResponse) {
        FullHttpResponse response = (FullHttpResponse) msg;
        String content = response.content().toString(CharsetUtil.UTF_8);
        SekiroResponse sekiroResponse = JSON.parseObject(content, SekiroResponse.class);
        if (sekiroResponse.getStatusCode() == 200) {
            promise.setSuccess(sekiroResponse);
        } else {
            promise.setFailure(new SekiroException(sekiroResponse.getStatusCode(), sekiroResponse.getContent()));
        }
    } else {
        ctx.fireChannelRead(msg);
    }
}

This code is used to determine whether the status code of the response is 200. If yes, convert the response to a SekiroResponse object and set it as the successful result of the Promise. Otherwise, use the response content as the exception information and set it as the failed result of the Promise.

Through the analysis of the above code, we can understand how the Sekiro framework implements remote calls. On the server side, it receives the client request through Netty, finds the corresponding request processor according to the URI of the request, then calls the request processor to process the request, and writes the processing result into the response. On the client side, it sends a request to the server through Netty and waits for a response from the server, then converts the response into a SekiroResponse object and sets it as the success or failure result of the Promise.

Python resources suitable for zero-based learning and advanced people :
① Tencent certified python complete project practical tutorial notes PDF
② More than a dozen major factories python interview topics PDF
③ Python full set of video tutorials (zero-based-advanced JS reverse)
④ Hundreds A project + source code + notes
⑤ programming grammar - machine learning - full stack development - data analysis - reptiles - APP reverse and other full set of projects + documents

Guess you like

Origin blog.csdn.net/ch950401/article/details/132259721