Contents

Stripping the bottoms of mobile games - Cocos2dx and LuaJIT complete decryption

Hehehe~ I have been enhanced in my love, after all, it is人生第一篇精华帖 , Let’s celebrate~ Two days of nights are not in vain 😄 Well… This morning, I was tweeted again by my love official,链接在这里 .

In order to satisfy the desire to collect, I want to win all the vertical paintings of a certain card game. In the process of reverse engineering, some fields that have not been explored by the predecessors have been opened up, and there is no corresponding information in either Chinese or English. This article proves that even if the Lua source code is compiled, it is not absolutely safe. The small calculations of the manufacturers may be that there are no mature tools and it is not easy to crack. But for security researchers who understand the principles, this is not a problem. On the contrary, for game developers who do not understand the principles and focus on rapid development of reusable code, there is no mature protection system. Whether they make their own wheels or use other people’s wheels, the threat will be greater.

Since I don’t know what the impact will be, I am worried that the plagiarism will come faster, so I selectively hide some source code and details here, but the ideas are not deleted at all here, and it is right to be my own notes and comments on the game. A wake-up call for manufacturers. Let’s start~

First, a decrypted sample picture.

/2017/luajit-decompile/00.png

The children’s shoes that have been played may be recognized at a glance, hehehe. The name of the game is Yaomeng Zhanji, and I don’t have much comment on the game itself. The version analyzed in this article is the version 20170922 downloaded from the official website, which is the latest official version as of the release of this article.

Compile and modify Luajit-lang-toolkit

After unpacking, I found that all the png resources are encrypted and cannot be opened (hey, but the audio has not been encrypted…but no one will watch it). Combined with lib, it is found that the game is written by cocos2x and Lua. Although Lua is notoriously rebellious, but after checking the file header, it is found that it is the compiled LuaJIT… or the latest version 2.1. Find an item after some manipulation luajit-lang-toolkit (hereinafter referred to as LLT), but it is a little problem to use. After make, the luajit-x program is generated in the src folder, which is the same as the command line tool parameters of luajit.

$ luajit-x -bl main
-- BYTECODE -- main:0-0
0001    TGETV    1   0   0
0002    KSHORT   2   1
...
0020    KSHORT   2   1
0021    ITERN    1   1   2
0022    FORL     0 => -32744

-- BYTECODE -- main:0-0
0001    TDUP     0   0
0002    TGETS    0   0   1  ; "__G__TRACKBACK__"
0003    KPRI     0 500
...
0072    TGETS    0   0  19  ; "LAUNCHERPKG"
0073    TGETV    0   0  26
0074    KSHORT   1  30
0075    ITERN    0   2   2
0076    TSETV    0   0  31
0077    ITERN    0   2   1
0078    UNM      1   0
0079    TSETV    0   0  32
0080    ITERN    0   1   2
0081    FORL     0 => -32685

We can see… nothing… but the disassembly results from the official command line tools are very different.

You see, there is at least one RET!

$ luajit -bl main
-- BYTECODE -- main:0-0
0001    GGET     1   0      ; "print"
0002    KSTR     2   1      ; "----------------------------------------"
0003    CALL     1   1   2
...
0019    GGET     1   0      ; "print"
0020    KSTR     2   1      ; "----------------------------------------"
0021    CALL     1   1   2
0022    RET0     0   1

-- BYTECODE -- main:0-0
...
0043    KSTR     0  18      ; ""
0044    GSET     0  19      ; "LAUNCHERPKG"
0045    GGET     0  13      ; "cc"
0046    TGETS    0   0  20  ; "LuaLoadChunksFromZIP"
0047    KSTR     1  21      ; "lib/"
0048    GGET     2   3      ; "GAME_BIT"
0049    KSTR     3  22      ; "/launcher.zip"
0050    CAT      1   1   3
0051    CALL     0   1   2
0052    GGET     0  23      ; "package"
0053    TGETS    0   0  24  ; "loaded"
0054    GGET     1  19      ; "LAUNCHERPKG"
0055    KSTR     2  25      ; "launcher.launcher"
0056    CAT      1   1   2
0057    KPRI     2   0
...
0073    GGET     0  26      ; "require"
0074    KSTR     1  30      ; "app.MyApp"
0075    CALL     0   2   2
0076    TGETS    0   0  31  ; "new"
0077    CALL     0   2   1
0078    MOV      1   0
0079    TGETS    0   0  32  ; "run"
0080    CALL     0   1   2
0081 => RET0     0   1

It’s fine to assemble directly, but LLT can parse more things, what should I do? I carefully referred to the opcode table 1, and found that many instructions are between the instruction table and the correct instruction (such as the first line TGETV and GGET) is only two instructions away. I looked through LLT’s issues and mentioned version 2.1, and someone even wrote a patch2 for him. I just understood - because compared with 2.0, two instructions are inserted in the middle of the instruction table of 2.1, so the positions of the following instructions in the table are moved backward accordingly.

I perfected this patch and added the updated instruction table to the reading and writing process. The modified version is located in我的Github middle v2.1 branch. Running the modified version outputs just fine.

$ luajit-x -bxg main
1b 4c 4a 02             | Header LuaJIT 2.0 BC
02                      | Flags: BCDUMP_F_STRIP
                        | .. prototype ..
b6 01                   | prototype length 182
00                      | prototype flags None
01                      | parameters number 1
05                      | framesize 5
00 08 00 16             | size uv: 0 kgc: 8 kn: 0 bc: 23
                        | .. bytecode ..
36 01 00 00             | 0001    GGET     1   0      ; "print"
27 02 01 00             | 0002    KSTR     2   1      ; "---------------
                        | -------------------------"
42 01 02 01             | 0003    CALL     1   1   2
...
36 01 00 00             | 0019    GGET     1   0      ; "print"
27 02 01 00             | 0020    KSTR     2   1      ; "---------------
                        | -------------------------"
42 01 02 01             | 0021    CALL     1   1   2
4b 00 01 00             | 0022    RET0     0   1
                        | .. uv ..
                        | .. kgc ..
05                      | kgc: ""
0e 74 72 61 63 65 62 61 | kgc: "traceback"
63 6b                   |
0a 64 65 62 75 67       | kgc: "debug"
06 0a                   | kgc: "\
...
15 5f 5f 47 5f 5f 54 52 | kgc: "__G__TRACKBACK__"
41 43 4b 42 41 43 4b 5f |
5f                      |
00                      | kgc: <function: main:0>
                        | .. knum ..
00                      | eof

This tool can list the hexdump of the file and the stack frame of the subroutine, local variables and other information, which is my favorite place. Enter the reverse part below.

First, disassemble and save all bytecodes to the src directory. run on the command line

ls | xargs -I% sh -c 'echo "Processing %" && luajit-x -bxg "%" > "src/%.lasm"'

View PNG Header Discovery Typeface img.libla, look for this string in files other than png

$ grep libla . -R | grep -iv png
Binary file ./lib/armeabi/libcocos2dlua.so matches

So it seems that the decryption process should be in the libcocos2dlua.so middle. First drag it into IDA to analyze it. Well, I reluctantly admit that it’s the opposite (it’s okay to use it later in the Lua part).

Analyze libcocos2dlua

Search in this so file img.libla Navigate to the function cocos2d::Image::isMpi(uchar const*,int). I went to the cocos2d-x official documentation to check, but there is no Image class. It feels unlikely. I found the corresponding CCImage class in the document of version 2.x, and found that 3.4 is the last version with this class. search in string 3. Find out if there is a specific version, and there is an unexpected discovery.

.rodata:0119FB8B 0000004D C E:\\MyWorkSpace\\ACGProj\\cocosEngine\\Quick-Cocos2dx-361\\/cocos/./math/Vec3.cpp

Hey~ It turned out to be a frame. I found this framework 3 on GitHub, and the author said that he chose the “recognized most stable version” 3.3. ~~ In fact, I am too lazy to learn new APIs. ~~ A random search can find many interesting encryption methods 45. We happily started homework with source code. But after a simple search, I found that there is no such function in the source code. Let’s go back to IDA and look at the place where this function is called to determine the file type.

signed int __fastcall cocos2d::Image::detectFormat(cocos2d::Image *this, const unsigned __int8 *a2, int a3)
{
  cocos2d::Image *v4; // [sp+4h] [bp-14h]@1
  int v5; // [sp+8h] [bp-10h]@1
  unsigned __int8 *v6; // [sp+Ch] [bp-Ch]@1
  signed int v7; // [sp+14h] [bp-4h]@2

  // ...省略...
  else if ( cocos2d::Image::isATITC(v4, v6, v5) & 1 )
  {
    v7 = 7;
  }
  else if ( cocos2d::Image::isMpi(v4, v6, v5) & 1 )
  {
    v7 = 10;
  }
  else
  {
    cocos2d::log("cocos2d: can't detect image format", &GLOBAL_OFFSET_TABLE_);
    v7 = 11;
  }
  return v7;
}

And this is the case near the place where this function is called.

int __fastcall cocos2d::Image::initWithImageData(cocos2d::Image *this, const unsigned __int8 *a2, int a3)
{
  // ... 解压被压缩的图片数据
        v6 = cocos2d::Image::detectFormat(v8, (const unsigned __int8 *)v11, (int)v10);
    }
    *((_DWORD *)v8 + 10) = v6;
    if ( v6 > 0xA )
    {
      // ... 未知格式错误处理
    }
    else
    {
      switch ( v6 )
      {
        case 1u:
          v12 = cocos2d::Image::initWithPngData(v8, (const unsigned __int8 *)v11, (int)v10) & 1;
          break;
        case 0xAu:
          v12 = cocos2d::Image::initWithMPIData(v8, (const unsigned __int8 *)v11, (int)v10) & 1;
          break;
        case 0u:
          v12 = cocos2d::Image::initWithJpgData(v8, (const unsigned __int8 *)v11, (int)v10) & 1;
          break;
        case 2u:
	//...

So our purpose is clear - the developer changed the cocos2d::Image::initWithImageData added its own file format, and in the detectFormat Called when the format is recognized isMpi Determine and specify whether the input file is your own private file format, and then write a initWithMPIData And a decoding or decryption operation is performed in it. All we have to do is parse or use the reverse process of this function to decrypt the image resource.

Bypass resource encryption with direct calls

At first I tried to simulate running with unicorn-engine. But later found that the standard library functions such as malloc were called in the function, which was very troublesome to deal with. I also passed the IDA debugging option. Combining with the image processing process of cocos2dx in CCImage.cpp, we can see that the original RGBA data is stored in the memory, even if the memory dump is made, it will be useless. After two days of tossing, I decided to use NDK to write a wrapper by myself. Using the exported characteristics of this function, I obtained this function from so and called it to decrypt it for us.

Let’s start with the existing code to judge the structure of the Image class. Combine itCCImage.h and CCImage.cpp middle cocos2d::Image::initWithPngData The corresponding relationship with its disassembly in IDA is organized as follows.

class CC_DLL Image : public Ref
{
	static const int MIPMAP_MAX = 16;
    unsigned char *_data;  // *(this_ + 20)
    ssize_t _dataLen;  // *(this_ + 24)
    int _width;  // *(this_ + 28)
    int _height; // *(this_ + 32)
    bool _unpack;  // *(this_ + 36) 来自初始化函数,实际并未读写
    Format _fileType;  // *(this_ + 40)
    Texture2D::PixelFormat _renderFormat;  // *(this_ + 44)
    MipmapInfo _mipmaps[MIPMAP_MAX];   // *(this_ + 48) pointer to mipmap images
    int _numberOfMipmaps;  // *(this_ + 176)

    // false if we cann't auto detect the image is premultiplied or not.
    bool _hasPremultipliedAlpha;  //  *(this_ + 180) 在一个判断后可能读写
    std::string _filePath;

It should be noted that IDA must be opened here Show casts Show casts, e.g. *((_DWORD *)this + 6) = 4 * v5; The offset of the member variable actually assigned in this sentence is not 6 bytes but 6*DWORD=24 bytes.

We write test cases for this object accordingly

class Image {
public:
    int __stub1[5];
    unsigned char *_data;  // *(this_ + 20)  wr
    int _dataLen;  // *(this_ + 24) w
    int _width;  // *(this_ + 28)  wr
    int _height; // *(this_ + 32)  wr
    int __stub2;
    int _fileType;  // *(this_ + 40)
    int _renderFormat;  // *(this_ + 44)  w (read in ::hasAlpha)
    char __stub3[132];  // 180-44-4
    bool _hasPremultipliedAlpha;  //  *(this_ + 180) possible w
};

// ...
printf("Image layout:\n"
           "_data: %d\n"
           "_datalen: %d\n"
           "_width: %d\n"
           "_height: %d\n"
           "_fileType: %d\n"
           "_renderFormat: %d\n"
           "_hasPremultipliedAlpha: %d\n",
           offsetof(Image, _data),
           offsetof(Image, _dataLen),
           offsetof(Image, _width),
           offsetof(Image, _height),
           offsetof(Image, _fileType),
           offsetof(Image, _renderFormat),
           offsetof(Image, _hasPremultipliedAlpha)
           );

After compiling with NDK and running on armv7 device, the results are as follows (if compiling encounters the problem of lack of standard library, just specify the GNU C++ runtime 6 in Application.mk)

Image layout:
_data: 20
_datalen: 24
_width: 28
_height: 32
_fileType: 40
_renderFormat: 44
_hasPremultipliedAlpha: 180

bingo~ Since the member variables inside are all ints or pointers, there is not much need to consider the issue of alignment7. If you have any questions, please refer to the ARM documentation in the resources below.

Then it’s programming. The C++ class member function is mangle into a messy string at compile time, which can be obtained in IDA or objdump, take it as it is and use dlsym to find the function pointer, and call it with our forged Image class as the first parameter of this pointer That’s it. As for how to save the content of the Image object as png, this is left as a question for everyone to think about, and the method is similar. We all refuse to reach out to the party.

Note, it is not written here but I have* Analyze in advance * Through the read and write status of the Image class by the function we want to call, all operations on member variables are written first and then read, and there is no uninitialized situation. If you want to call other functions in a similar way, you should also pay attention to the value of the data member passed in.

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <dlfcn.h>
#include "CCImage.h"

#define assert(cond, err) if (!(cond)) {printf(err);exit(1);}

typedef bool (*initWithMPIData_t)(Image*, char *, ssize_t);
const char* initWithMPIData_mangled = "_ZN7cocos2d5Image15initWithMPIDataEPKhi";

int main(int argc, char** argv) {
    assert(argc > 1, "usage: decrypt [encrypted_png]\n");

    Image *img = new Image();

    FILE *f = fopen(argv[1], "rb");
    assert(f != NULL, "File not exists!");

    fseek(f, 0, SEEK_END);
    long fsize = ftell(f);
    printf("Source file size: %ld\n", fsize);
    fseek(f, 0, SEEK_SET);

    char *buf = (char *)malloc(fsize + 1);
    fread(buf, fsize, 1, f);
    buf[fsize] = 0;
    fclose(f);
    assert(!memcmp(buf, "img.libla", 9), "File already decrypted~~\n");

    void* handle = dlopen("/data/local/tmp/libcocos2dlua.so", RTLD_LAZY);
    assert(handle != NULL, "libcocos2dlua.so missing or corrupted!\n");

    initWithMPIData_t initWithMPIData_ptr = (initWithMPIData_t) dlsym(handle, initWithMPIData_mangled);

    assert(initWithMPIData_ptr != NULL, "Image::initWithMPIData not found\n");
    printf("Image::initWithMPIData found at: %p\n", initWithMPIData_ptr);

    initWithMPIData_ptr(img, buf, fsize);
    printf("Decrypted image %d x %d, raw size %d.\n",
        img->_width, img->_height, img->_dataLen);

	// 留给大家的思考题

    return 0;
}

Push to run on the phone. By the way, 1.out.png and 1.png are exactly the same size. Show that this algorithm is completely reversible. I’m just lazy…

$ ./decrypt assets/res/photo/imgs/normal/1.png
Source file size: 92363
Image::initWithMPIData found at: 0xe8185854
Decrypted image 488 x 720, raw size 1405440.
Saving to assets/res/photo/imgs/normal/1.out.png

OK, and then batch processing with shell scripts can decrypt all the files. The next thing we have to do is to match each picture with his character and see if we can dig out other information by the way.

Exploring LuaJIT

This is a game with the theme of the Three Kingdoms. We search for the keyword “Zhao Yun” and locate the class app.data.db.cardDef. Here I would like to praise LLT again. The kgc and knum information cannot be dumped by official tools. The official document 8 is obscure and difficult to understand (there are still a bunch of TODOs that are simply not finished), and a slightly better teaching material is the source code 9 of LLT’s bytecode parsing process.

Here is an example to explain the binary file format generated by LuaJIT. Look at the hex on the left and the number note on the right

1b 4c 4a 02             | Header LuaJIT 2.0 BC				| 1
02                      | Flags: BCDUMP_F_STRIP				| 2
                        | .. prototype ..
ce b9 16                | prototype length 367822			| 3
02                      | prototype flags PROTO_VARARG		| 4
00                      | parameters number 0				| 5
03                      | framesize 3						| 6
00 99 02 00 85 06       | size uv: 0 kgc: 281 kn: 0 bc: 774	| 7
                        | .. bytecode ..
34 00 00 00             | 0001    TNEW     0   0
35 01 00 00             | 0002    TDUP     1   0
3e 01 01 00             | 0003    TSETB    1   0   1
..... 此处省略770行,都是汇编
4c 00 02 00             | 0773    RET1     0   2
                        | .. uv ..
                        | .. kgc ..
01 00 63                | ktab narray: 0 nhash: 99			| 8
0e 63 61 72 64 53 63 6f | ktabk string: "cardScore"
72 65                   |
03 d0 0f                | ktabk int: 2000
  1. 3 bytes fixed header “\x1bLJ”, the fourth 02 is the bytecode version, 01 for LuaJIT 2.0, 02 It is LuaJIT 2.1. There is currently no official document for version 2.1, so we can only get a glimpse of it from the code. See the reference 2 for the difference.
  2. The definition of flags refers to LLT source code bytecode.lua9
  3. Start a “section” (prototype, equivalent to a function definition) with a variable-length number represented by ULEB128 and specify the length of the section. If 0 is read, it is considered the end of the file.
  4. Refer to the same as above for flag, 1byte
  5. Number of parameters, 1byte
  6. Stack frame size 3 Guess is related to memory allocation, 1byte
  7. Here the four numbers are:
    1. uv: Upvalue comes from the variable of the caller, 1byte
    2. kgc: The number of variables allocated dynamically (guess, the translation is garbage collected?), which can store strings (str*B pointer), arrays (karray) or dictionaries (khash), ULEB128
    3. kn: constant, can be understood as const, ULEB128
    4. bc: bytecode, the number of assembly codes (each instruction is 4bytes fixed length), ULEB128
  8. kgc format: 1 bit for type (string or array/dictionary), if the type is array or dictionary. here is 01 Represents an array.
    1. Array/dictionary (ktab) format, 2bytes following the kgc type
    2. narray: the number of stored array elements (understandable as *args in Python)
    3. nhash: the number of dictionary elements stored (can be understood as Python** kwargs)
    4. Followed by a string of ktabk to count by the above two values. One ktabk for an array element, two ktabk for a dictionary element
    5. The type of ktabk is in the lj_bcdump.h file of LuaJIT BCDUMP_KTAB_XXX in the enum definition. int is 03, the string is 05 Plus the string length. This item type is ULEB128

What I explained is more general, and a more accurate definition needs to refer to the paradigm on the official document.

We can see that the bytecode format is very compact and there is almost no wasted space. And I have to say that it is much easier to write a program to parse raw bytecode than to read the text output by LLT… so I wrote one.

The following script is used to parse the file generated by LuaJIT and extract the constant information into a csv file.

#!/usr/bin/env python3
import sys
from pathlib import Path
import csv


target = Path("app.data.db.cardDef" if len(sys.argv) < 2 else sys.argv[1])


def readULEB128(file):
    result = offset = 0
    while True:
        byte = file.read(1)
        result += (byte[0] & 0x7F) << offset
        offset += 7
        if byte[0] < 0x80:
            return result


def readKtabk(file):
    type_ktab = readULEB128(file)
    assert type_ktab == 3 or type_ktab > 4, \
        "Unsupported ktab type {} at position {}".format(type_ktab, file.tell())
    if type_ktab == 3:
        return readULEB128(file)
    else:
        ret = file.read(type_ktab-5)
        try:
            return ret.decode()
        except UnicodeDecodeError as e:
            print("ERROR at pos", file.tell(), str(e))
        return ret.hex()


def main():
    with target.open('rb') as f:
        f.seek(5)                       # skip header
        length_total = readULEB128(f)   # read proto leng
        print("proto length", length_total)
        prototype_start_pos = f.tell()
        f.seek(4, 1)                    # skip flag, param, frame, uv
        num_vars = readULEB128(f)
        _ = readULEB128(f)              # kn not used
        num_bytecode = readULEB128(f)
        f.seek(num_bytecode*4, 1)   # skip bytecode

        kgcs = []

        for _ in range(num_vars):       # read one kgc per time
            type_kgc = f.read(1)[0]
            assert type_kgc == 1, \
                "Unsupported variable {} at {}".format(type_kgc, f.tell())
            print("== reading new one from", f.tell())

            narray = readULEB128(f)
            nhash = readULEB128(f)
            array = []
            hashes = {}
            for _ in range(narray):
                array.append(readKtabk(f))
            for _ in range(nhash):
                key = readKtabk(f)
                val = readKtabk(f)
                print("Reading key: ", key, "val:", val)
                hashes[key] = val

            kgcs.append(hashes or array)  # a ktab may only contain one

        assert f.tell() - prototype_start_pos == length_total, \
            "What happened?? file corrupted? at {}".format(f.tell())

        assert f.read(1)[0] == 0, "More prototypes following, I'm tired"

        print("Dump success!")

    with open("dumpinfo.csv", 'w') as csvfile:
        csvfile.write('\ufeff')
        writer = csv.DictWriter(csvfile, sorted(kgcs[0].keys()))
        writer.writeheader()
        writer.writerows(kgcs)

        print("dumped into dumpinfo.csv")


if __name__ == '__main__':
    main()

Get a csv in a format like the one below. The header was deleted by me. Too long and ugly. But the following string of information already contains most of the information we may need, including character attack power, life value, selling price, skill ID, voice ID, etc., and even the slogan shouted when zooming in. It also has great potential

0,0,0,0,0,0,0,0,9,7,1,17,430,75,327,327,327,1,1,1,14500,14500,14500,65,0,2000,5,0,6,1250,0,90,240,道为核心   天道无为   道法自然  且善且行,3273,3,60,30,100,150,45,5,327,136,Y01,249,255,481,3272,130,1400,130,梦里繁花,3,75,左慈,0,0,0,0,35,240,5,0,0,3273,23,24,24,0,0,1,1,1,0,0,149,290,5,5,0,0,0,0,0,0,0,0,0,6,6,100,45,0,0,0,350,45,0
15,15,38,25,746,115,60,0,2,8,1,15,260,42,15,15,15,1,1,1,2500,2500,2500,120,0,1,2,1150,7,1300,600,124,232,什么  你说现在不流行用剑了?,0,0,0,0,0,180,60,5,15,150,S03,320,267,743,745,79,1500,140,,0,35,木鹿大王,0,0,0,0,120,246,5,0,0,747,0,13,14,15,15,0,1,1,1,1,110,232,0,4,0,0,0,0,0,0,0,0,0,4,4,500,60,0,0,0,150,60,0
15,5,37,10,744,130,35,0,2,8,1,15,260,42,15,15,15,1,1,1,2500,2500,2500,120,166,1,2,1150,5,1300,600,112,210,什么  你说现在不流行用剑了?,0,0,0,0,0,150,45,4,15,145,S03,290,230,743,743,50,1500,130,,0,45,木鹿大王,0,0,0,0,105,200,4,0,0,747,0,13,14,15,15,0,1,1,1,1,100,210,0,5,0,0,0,0,0,0,0,0,0,4,4,200,45,0,0,0,150,45,0
710,5,39,15,716,115,60,0,9,7,1,17,430,76,710,710,710,1,1,1,50000,50000,50000,150,0,2000,5,0,8,1350,0,237,286,尝尝这南蛮之地的烈火吧 ,0,0,0,0,0,260,65,6,710,165,M02,250,246,495,715,105,1500,145,含光剑 ,7,60,祝融,0,0,0,0,115,210,6,0,0,717,46,50,33,0,0,1,1,1,0,0,139,305,5,3,0,0,0,0,0,0,0,0,0,6,6,250,65,0,0,0,650,65,0
0,0,0,0,0,0,0,0,2,8,1,24,440,95,308,308,308,1,1,1,10000,10000,10000,165,185,400,2,1200,9,1300,1500,151,309,切...(无视)...,0,0,0,0,0,600,70,6,308,160,Y02,439,304,2308,352,91,2900,155,青龙偃月刀,1,60,关羽,0,0,0,0,160,309,6,0,0,352,0,39,17,18,18,0,1,1,1,1,139,312,2,4,0,0,0,0,0,0,0,0,0,4,4,600,70,0,0,0,500,70,0

Search for the file number 2308 of the picture at the beginning of the article, and find the column No. er… 58 of Guan Yu’s row. Of course, if this column has a name, it is called icon, and as the name implies, the column name is called name. Continue to use python to batch rename all files with numbers in the names in the decrypted output directory.

import csv
from pathlib import Path

with open("dumpinfo.csv") as csvfile:
    reader = csv.DictReader(csvfile)
    for row in reader:
        icon = row['icon']
        name = row['name']
        for f in Path().rglob(icon+'.png'):
            f.rename(f.with_name('{}-{}.png'.format(icon, name)))

You’re done!

/2017/luajit-decompile/01.png

This part is finished, but there is no end to learning~ Well, I have learned PIL, and then cut and cut the pictures to make a card collection (escape

Protective measures

  • Packing against static analysis.
  • Do not export sensitive functions.
  • Added encryption function context dependency. (emphasis added)
  • confused. (Is there a mature plan?)

References