# 第七届山东省大学生网络安全技能大赛. 安卓逆向 MagicImageViewer

闲来无事刷CTF，今天逛论坛看到了一个CTF《无用的CE》，没有bin文件，只有一个bugku的连接；去首页看了下不少CTF的题目，故注册了账号，每个题下载都要币，幸好初始账号有30个，研究了下，有的题目下载需要3 - 5个币不等，提交flag还能赚1 - 2个币，所以专挑差价多的搞，先下了三个试试水。前两个难度不大，感觉这个有些技巧故分享之。

## 观

看后缀是apk,丢到夜神模拟器看看。

![](./pictures/nox.png)

随便写个code，点击DECRYPT按钮会Toast提示 Something wrong!


## 反编译


### 反编译main activity

直接把APK丢到Jadx中，根据反编译情况来看没有加壳。幸好以前做过几天的APP，这代码看着很亲切的感觉。


```java

        public native String getKey(String str);

        public native String stringFromJNI();

        public void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        setContentView(R.layout.activity_main);
        TextView textView = (TextView) findViewById(R.id.sample_text);
        textView.setText(stringFromJNI());
        ((Button) findViewById(R.id.decrypt_button)).setOnClickListener(new View.OnClickListener() { // from class: re.sdnisc2018.sdnisc_apk2.MainActivity.1
            @Override // android.view.View.OnClickListener
            public void onClick(View view) {
                TextView textView2 = (TextView) MainActivity.this.findViewById(R.id.sample_text);
                String obj = ((EditText) MainActivity.this.findViewById(R.id.Key_text)).getText().toString();
                if (obj.length() != 16) {
                    MainActivity.this.showMsgToast("Something wrong!");
                    return;
                }
                String key = MainActivity.this.getKey(obj);
                ImageView imageView = (ImageView) MainActivity.this.findViewById(R.id.img);
                Bitmap readMagicImage = MagicImageUtils.readMagicImage(MainActivity.this, "png/encrypt_png.dat", key);
                if (readMagicImage == null) {
                    MainActivity.this.showMsgToast("Something wrong!");
                    return;
                }
                textView2.setText("Congratulations!");
                MainActivity.this.showMsgToast("Congratulations!");
                imageView.setImageBitmap(readMagicImage);
            }
        });
        ImageView imageView = (ImageView) findViewById(R.id.img);
        Bitmap readImage = MagicImageUtils.readImage(this, "png/bg.png");
        if (readImage != null) {
            textView.setText(" ");
            imageView.setImageBitmap(readImage);
            return;
        }
        showMsgToast("Something wrong!");
    }

```
代码解读：

* decrypt_button点击事件：从Key_text控件读取字符串，判断长度，长度不等于16就提示 “Something wrong!”
* 通过长度检查后，key会经过getKey()函数转换
* readMagicImage 会用key解密 encrypt_png.dat 文件，获取一个bitmap，然后重置imageView的ImageBitmap，来显示新解密后的图片。

通过上面的代码可以看到2个关键函数getKey readMagicImage;

双击getKey，可以看到`public native String getKey(String str);`，根据以前的开发经验应该是调用c/c++的so函数了。

双击readMagicImage ，跳到函数代码处，如下

```java

    public static native int decrypt(int i, char c);

    public static Bitmap readMagicImage(Context context, String str, String str2) {
        Bitmap decodeByteArray;
        ArrayList<Byte> arrayList = new ArrayList();
        Bitmap bitmap = null;
        try {
            InputStream open = context.getAssets().open(str);
            int i = 0;
            while (true) {
                int read = open.read();
                if (read <= -1) {
                    break;
                }
                arrayList.add(Byte.valueOf((byte) decrypt(read, str2.charAt(i % 16))));
                i++;
            }
            byte[] bArr = new byte[arrayList.size()];
            int i2 = 0;
            for (Byte b : arrayList) {
                bArr[i2] = b.byteValue();
                i2++;
            }
            decodeByteArray = BitmapFactory.decodeByteArray(bArr, 0, arrayList.size());
        } catch (IOException e) {
            e = e;
        }
        try {
            System.out.println(decodeByteArray);
            return decodeByteArray;
        } catch (IOException e2) {
            bitmap = decodeByteArray;
            e = e2;
            e.printStackTrace();
            return bitmap;
        }
    }

```

代码解读：

* 打开资源文件，然后依次读取一字节，然后通过decrypt函数和key一起解密数据。
* 解密后的一字节存入arrayList中
* arrayList 转 byte[] 转 bitmap ；返回bitmap

关键函数decrypt

双击decrypt，来到`public static native int decrypt(int i, char c);`,来自so。

通过google，得知android会使用 `System.loadLibrary` 加载so文件。

MainActivity的这个函数验证了这个。
```java
static {
        System.loadLibrary("native-lib");
    }
```

## 反编译 so

通过jadx可以清楚的看到资源lib下有多个版本的libnative-lib.so文件

![](./pictures/lib.png)

直接保存下来，然后挑个自己熟悉的来搞，先从x86的下手。

直接拖入IDA中开始分析，通过导出表很快找到了想要的函数

```
Java_re_sdnisc2018_sdnisc_1apk2_MagicImageUtils_decrypt	000069E0	
Java_re_sdnisc2018_sdnisc_1apk2_MainActivity_getKey	000067E0	
```
双击函数直接来到函数代码处，因为牵扯到JavaVM JNIEnv的类，反汇编代码很难看清逻辑，直接F5吧。

简单重定义了几个数据类型后，代码看起来舒服多了。

```c
int __cdecl Java_re_sdnisc2018_sdnisc_1apk2_MainActivity_getKey(int a1, JNIEnv *a2, int a3, int a4)
{
  char *v4; // eax
  char *v5; // edi
  JNIEnv *v6; // esi
  const jchar *v7; // eax
  unsigned int v8; // esi
  char v9; // bl
  int v10; // esi
  char *v12; // [esp-34h] [ebp-34h]
  int v13; // [esp-30h] [ebp-30h]
  _DWORD v14[2]; // [esp-28h] [ebp-28h] BYREF
  char *v15; // [esp-20h] [ebp-20h]

  v4 = (char *)new(0x20u);
  strcpy(v4, "Welcome_to_sdnisc_2018_By.Zero");
  v12 = v4;
  v5 = (char *)new(0x20u);
  v6 = a2;
  v15 = v5;
  v14[0] = 33;
  v14[1] = 16;
  strcpy(v5, "                ");
  v13 = (*a2)->GetStringLength(a2, (jstring)a4);
  v7 = (*v6)->GetStringChars(v6, (jstring)a4, 0);
  if ( v13 > 0 )
  {
    v8 = 0;
    v9 = 33;
    do
    {
      if ( (v9 & 1) == 0 )
        v5 = (char *)v14 + 1;
      v5[v8] = LOBYTE(v7[v8]) ^ v12[v8 + -30 * (v8 / 0x1E)];
      ++v8;
      v9 = v14[0];
      v5 = v15;
    }
    while ( v13 != v8 );
    if ( (v14[0] & 1) == 0 )
      v5 = (char *)v14 + 1;
    v6 = a2;
  }
  v10 = (int)(*v6)->NewStringUTF(v6, v5);
  if ( (v14[0] & 1) != 0 )
    operator delete(v15);
  operator delete(v12);
  return v10;
}


int __cdecl Java_re_sdnisc2018_sdnisc_1apk2_MagicImageUtils_decrypt(int a1, int a2, int a3, int a4, unsigned __int16 a5)
{
  return (a4 - 1) ^ a5 ^ 0x61;
}


```

### 困惑

看了好久的getKey函数，有意些代码实在是搞不明白。又因为不会调试分析so文件，所以卡住了。
```c
if ( (v9 & 1) == 0 )
    v5 = (char *)v14 + 1;
```

### 柳暗花明

回头看了下decrypt倒是很简单。

把流程多看了几遍，突然灵机一动，解密的是png，至少前4字节是固定的。这让我突然想起了，破译希特勒电报的故事，电报的开头固定的格式“希特勒万岁”。

通过分析bg.png，网上下png和其他三个png发现，不仅仅是magic是一样的甚至前16字节完全一样，出乎意料呀，刚开始还以只会是前4字节是一样的。

这样知道亦或算法，处理后的数据和原始数据，很容易推算数key[16]。

伪代码如下
```c 
    _dat_magic = ctf_read_file(ENCRYPT_PIC,16);
    _png_magic = ctf_read_file(NORMAL_PIC,16);
    for (_i = 0; _i < 16; _i++)
    {
        _xorKey[_i] = _png_magic[_i] ^ (_dat_magic[_i] - 1) ^ 0x61;
    }
```

计算出xorKey后，就可以解密ENCRYPT_PNG.data了。

```c
    _pngSize = ctf_get_file_size(ENCRYPT_PIC);
    _pngBuffer = ctf_read_file(ENCRYPT_PIC,_pngSize);
  
    for ( _i = 0; _i < _pngSize; _i++)
    {
         _pngBuffer[_i] =  ( _pngBuffer[_i] -1) ^  _xorKey[_i % 16] ^ 0x61;
    }
    ctf_write_file(DECRYPT_PIC,_pngBuffer,_pngSize);
   
    free(_pngBuffer);

```

解密后的png，得到图片中的flag

![](./pictures/decrypt.png)

## 故事还没有结束

一直认为是数据类型影响了IDA对getKey的正常还原。

找了几篇IDA反汇编的so的文章，学了不少技巧，如导入structs，导入JNI.h头文件，以及Type Lib 增加android相关的库，来丰富IDA的还原能力。

又尝试着看看其他几个反编译F5还原的代码都一样吗，接着对x64的so进行了反编译和F5

出乎意料呀getKey如此清爽（设置几个数据类型，对几个函数调用强制Force call ）

```c
jstring __fastcall Java_re_sdnisc2018_sdnisc_1apk2_MainActivity_getKey(JNIEnv_ *a1, __int64 a2, __int64 a3)
{
  char *v5; // r12
  int keyLen; // ebp
  jchar *keyString; // rax
  __int64 v8; // r9
  unsigned int v9; // edx
  __int64 i; // rbp
  jstring v11; // rbp
  _BYTE newKey[23]; // [rsp+1h] [rbp-47h] BYREF
  unsigned __int64 v14; // [rsp+18h] [rbp-30h]

  v14 = __readfsqword(0x28u);
  v5 = (char *)operator new(0x20uLL);
  strcpy(v5, "Welcome_to_sdnisc_2018_By.Zero");
  *(_QWORD *)&newKey[15] = 0LL;
  *(_OWORD *)newKey = xmmword_27DC6;
  keyLen = a1->functions->GetStringLength(&a1->functions, (jstring)a3);
  keyString = (jchar *)a1->functions->GetStringChars(&a1->functions, (jstring)a3, 0LL);
  if ( keyLen > 0 )
  {
    v8 = (unsigned int)keyLen;
    v9 = 0;
    for ( i = 0LL; i != v8; ++i )
    {
      newKey[i] = LOBYTE(keyString[i]) ^ v5[(unsigned int)i - 30 * (v9 / 0x1E)];
      ++v9;
    }
  }
  v11 = a1->functions->NewStringUTF(&a1->functions, newKey);
  operator delete(v5);
  return v11;
}

```

既然得到了算法，最后得到keygen.c

```c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "ctf.h"

#define ENCRYPT_PIC "encrypt_png.dat"
#define NORMAL_PIC "bg.png"
#define DECRYPT_PIC "decrypt.png"

void main(){

    unsigned char _xorKey[16],_key[17],* _pngBuffer,*_dat_magic,*_png_magic;
    unsigned int _i,_pngSize;
    char _welcome[] = "Welcome_to_sdnisc_2018_By.Zero";
 
	
    _dat_magic = ctf_read_file(ENCRYPT_PIC,16);
    _png_magic = ctf_read_file(NORMAL_PIC,16);

    for (_i = 0; _i < 16; _i++)
    {
        _key[_i] = _png_magic[_i] ^ (_dat_magic[_i] - 1) ^ 0x61 ^ _welcome[_i];
    }
    _key[16] = 0;
    printf("key: %s \n",_key);

    for (_i = 0; _i < 16; _i++)
    {
        _xorKey[_i] = _png_magic[_i] ^ (_dat_magic[_i] - 1) ^ 0x61;
    }
    // for ( _i = 0; _i < 16; _i++) printf("0x%02x ",_xorKey[_i]);
    
    _pngSize = ctf_get_file_size(ENCRYPT_PIC);
    _pngBuffer = ctf_read_file(ENCRYPT_PIC,_pngSize);
    if (_pngBuffer)
    {
        for ( _i = 0; _i < _pngSize; _i++)
        {
            _pngBuffer[_i] =  ( _pngBuffer[_i] -1) ^  _xorKey[_i % 16] ^ 0x61;
        }
        if(ctf_write_file(DECRYPT_PIC,_pngBuffer,_pngSize) == 1)
        {
            printf("write '%s' OK!\n ",DECRYPT_PIC);
        }else{
            printf("write '%s' FAILED!\n ",DECRYPT_PIC);
        }
        free(_pngBuffer);
    }else{
        printf("read '%s' FAILED!\n ",ENCRYPT_PIC);
    }

    free(_dat_magic);
    free(_png_magic);  
}

```

编译运行

```bash
C:\Users\debugger\Desktop\CTF\bugku\MagicImageViewer>gcc -o keygen.exe ctf.c keygen.c
C:\Users\debugger\Desktop\CTF\bugku\MagicImageViewer>keygen.exe
key: XaE3*2#@!qV^v+_.
write 'decrypt.png' OK!

```

## 验证



![](./pictures/bingo.png)



















