hans7 发表于 2022-7-23 18:30

【CTF 安卓逆向】MagicImageViewer——png结构+算法逆向

本帖最后由 hans7 于 2022-7-23 18:45 编辑

看到一道结合了png头和安卓逆向的题,觉得有点意思,在此记录。[传送门](https://www.52pojie.cn/thread-820158-1-1.html)

APK文件可以在传送门处下载,也可以QQ找我要。

本文52pojie:https://www.52pojie.cn/thread-1665541-1-1.html

本文juejin:https://juejin.cn/post/7123515777465974821/

首先打开JEB,看到MainActivity:

```java
    protected void onCreate(Bundle arg4) {
      super.onCreate(arg4);
      this.setContentView(0x7F09001B);// layout:activity_main
      TextView v4 = (TextView)this.findViewById(0x7F070062);// id:sample_text
      v4.setText(this.stringFromJNI());
      ((Button)this.findViewById(0x7F070031)).setOnClickListener(new View.OnClickListener() {// id:decrypt_button
            @Override// android.view.View$OnClickListener
            public void onClick(View arg5) {
                TextView v5 = (TextView)MainActivity.this.findViewById(0x7F070062);// id:sample_text
                String v0 = ((EditText)MainActivity.this.findViewById(0x7F070003)).getText().toString();// id:Key_text
                if(v0.length() == 16) {
                  String v0_1 = MainActivity.this.getKey(v0);
                  ImageView v1 = (ImageView)MainActivity.this.findViewById(0x7F070046);// id:img
                  Bitmap v0_2 = MagicImageUtils.readMagicImage(MainActivity.this, "png/encrypt_png.dat", v0_1);
                  if(v0_2 != null) {
                        v5.setText("Congratulations!");
                        MainActivity.this.showMsgToast("Congratulations!");
                        v1.setImageBitmap(v0_2);
                        return;
                  }

                  MainActivity.this.showMsgToast("Something wrong!");
                  return;
                }

                MainActivity.this.showMsgToast("Something wrong!");
            }
      });
      ImageView v0 = (ImageView)this.findViewById(0x7F070046);// id:img
      Bitmap v1 = MagicImageUtils.readImage(this, "png/bg.png");
      if(v1 != null) {
            v4.setText(" ");
            v0.setImageBitmap(v1);
            return;
      }

      this.showMsgToast("Something wrong!");
    }
```

这里`v0`是长为16的输入串,`getKey(v0)`是一个native层的方法,`MagicImageUtils.readMagicImage`是Java层的方法。

我们先看`getKey`。我们选择最熟悉的架构:`x86`,在JEB中导出`so`文件,然后用IDA打开。

```c
int __cdecl Java_re_sdnisc2018_sdnisc_1apk2_MainActivity_getKey(JNIEnv *a1, int a2, int a3)
{
int v3; // eax@1
int v4; // eax@1
char *v5; // edi@1
JNIEnv *v6; // esi@1
int v7; // eax@1
char *v8; // ecx@1
unsigned int i; // esi@2
char v10; // bl@2
int v11; // esi@7
int result; // eax@9
void *v13; // @1
jsize v14; // @1
char v15; // @0
signed int v16; // @1
signed int v17; // @1
void *v18; // @1
int v19; // @1

v19 = _stack_chk_guard;
v3 = operator new(0x20u);
*(_WORD *)(v3 + 28) = 'or';
*(_DWORD *)(v3 + 24) = 'eZ.y';
*(_DWORD *)(v3 + 20) = 'B_81';
*(_DWORD *)(v3 + 16) = '02_c';
*(_DWORD *)(v3 + 12) = 'sind';
*(_DWORD *)(v3 + 8) = 's_ot';
*(_DWORD *)(v3 + 4) = '_emo';
*(_DWORD *)v3 = 'cleW';
v13 = (void *)v3;
*(_BYTE *)(v3 + 30) = 0;
v4 = operator new(0x20u);
v5 = (char *)v4;
v6 = a1;
v18 = (void *)v4;
v16 = 33;
v17 = 16;
*(_DWORD *)(v4 + 12) = '    ';
*(_DWORD *)(v4 + 8) = '    ';
*(_DWORD *)(v4 + 4) = '    ';
*(_DWORD *)v4 = '    ';
*(_BYTE *)(v4 + 16) = 0;
v14 = (*a1)->GetStringLength(a1, (jstring)a3);
v7 = (int)(*v6)->GetStringChars(v6, (jstring)a3, 0);
if ( v14 > 0 )
{
    i = 0;
    v10 = 33;
    do
    {
      v8 = (char *)&v16 + 1;
      if ( !(v10 & 1) )
      v5 = (char *)&v16 + 1;
      v5 = *(_BYTE *)(v7 + 2 * i) ^ *((_BYTE *)v13 + i + -30 * (i / 30));
      ++i;
      v10 = v15;
      v5 = (char *)v18;
    }
    while ( v14 != i );
    v6 = a1;
}
v11 = ((int (__fastcall *)(char *, int))(*v6)->NewStringUTF)(v8, v7);
if ( v15 & 1 )
    operator delete(v18);
operator delete(v13);
result = _stack_chk_guard;
if ( _stack_chk_guard == v19 )
    result = v11;
return result;
}
```

`v4`是eax,eax存储返回值,而`v5`始终保证`v5 = (char *)v4`,因此`v5`是输出串。`v7`是长为16的输入串,`v13`是常量串`Welcome_to_sdnisc_2018_By.Zero`,长为30(注意**小端序**)。因此`v5 = *(_BYTE *)(v7) ^ *((_BYTE *)v13)`。

这个`2*i`比较怪,是什么呢?我们查一下JNI的`GetStringChars`方法,由参考链接1可知它返回的是Unicode格式的`char*`,所以2字节算一个字符。

接下来看`MagicImageUtils.readMagicImage`:

```java
package re.sdnisc2018.sdnisc_apk2;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;

public class MagicImageUtils {
    static {
      System.loadLibrary("native-lib");
    }

    public static native int decrypt(int arg0, char arg1) {
    }
   
    public static Bitmap readImage(Context arg6, String arg7) {}//略

    public static Bitmap readMagicImage(Context arg5, String arg6, String arg7) {
      ArrayList v0 = new ArrayList();
      try {
            InputStream v5_1 = arg5.getAssets().open(arg6);
            int v2;
            for(v2 = 0; true; ++v2) {
                int v3 = v5_1.read();
                if(v3 <= -1) {
                  break;
                }

                v0.add(Byte.valueOf(((byte)MagicImageUtils.decrypt(v3, arg7.charAt(v2 % 16)))));
            }

            byte[] v5_2 = new byte;
            Iterator v7 = v0.iterator();
            int v2_1;
            for(v2_1 = 0; v7.hasNext(); ++v2_1) {
                Object v3_1 = v7.next();
                v5_2 = (byte)(((Byte)v3_1));
            }

            Bitmap v5_3 = BitmapFactory.decodeByteArray(v5_2, 0, v0.size());
            System.out.println(v5_3);
            return v5_3;
      }
      catch(IOException v5) {
            v5.printStackTrace();
            return null;
      }
    }
}
```

其中`decrypt`函数:

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

这里是要把加密的数据解密为答案png,过程很容易看出是`png = (dat - 1) ^ _key ^ 0x61`。约定`_key[]`是`getKey()`的返回值,注意`_key[]`长度仍为16。

有一个MISC的基础知识,png格式的头部是固定的,因此我们认为`png[]`的前16个字节已知,这样就很容易解出`_key[]`了。于是我们就得到了`png[]`。至此,我们发现:

1. 即使没看懂`getKey`也能过这题。
2. 作者怕我们看不懂Java处理图片的操作,贴心地提供了一个正常图片的处理函数`MagicImageUtils.readImage`。
3. 有没有发现,题目MagicImageViewer是一语双关?我们通过已知的png头,即magic number,拿到整个png。

当然我们可以更进一步,求出输入串。`v5[]`就是`_key[]`,而上面已经分析出`_key = v7 ^ v13`,而`_key`上一步已经求出,故`v7`,即输入串,可以求出。

### 代码

1. `b`函数是为了处理`dat = 0`的情况,如果有更优雅的方法,教教我。
2. `goal`是所求png的前16个字节。

```python
goal = b'\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52'
key_s = 'Welcome_to_sdnisc_2018_By.Zero'
ans = ''


def b(v):
    return v if v >= 0 else 0xff


with open('encrypt_png.dat', 'rb') as f:
    dat = f.read(16)
    for i in range(16):
      v = b(dat - 1) ^ 0x61 ^ goal ^ ord(key_s)
      ans += chr(v)
print(ans)


def get_byte(v):
    return v.to_bytes(1, byteorder='little', signed=False)


# 写入png
with open('encrypt_png.dat', 'rb') as f:
    dat = f.read()
    _key = b''
    for i in range(16):
      _key += get_byte(b(dat - 1) ^ 0x61 ^ goal)
    with open('ans.png', 'wb') as g:
      for i in range(len(dat)):
            g.write(get_byte(b(dat - 1) ^ 0x61 ^ _key))
```

### 参考链接

1. https://www.cnblogs.com/lijunamneg/archive/2012/12/22/2828891.html

wuyf 发表于 2022-7-24 17:46

小白又学到了!!谢谢分享{:1_893:}

LeoXiong 发表于 2022-7-25 08:57

感谢分享,学习学习

咔c君 发表于 2022-7-25 12:50

不错学习了

baicha1 发表于 2022-7-25 17:55

tql!向大佬学习

kanxue2018 发表于 2022-7-26 00:20

感谢分享,好资料好好学习

CHE1027 发表于 2022-7-26 00:53

一天一学

a8987216 发表于 2022-7-26 08:35

本帖最后由 a8987216 于 2022-7-26 08:36 编辑

分析的很透彻啊,给大佬点赞{:301_993:}

GuiXiaoQi 发表于 2022-7-26 11:46

先点赞后学习

redyc 发表于 2023-7-25 16:47

多谢楼主分享,分析得很详细
页: [1]
查看完整版本: 【CTF 安卓逆向】MagicImageViewer——png结构+算法逆向