看到一道结合了png头和安卓逆向的题,觉得有点意思,在此记录。传送门
APK文件可以在传送门处下载,也可以QQ找我要。
本文52pojie:https://www.52pojie.cn/thread-1665541-1-1.html
本文juejin:https://juejin.cn/post/7123515777465974821/
首先打开JEB,看到MainActivity:
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打开。
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; // [sp+1Ch] [bp-30h]@1
jsize v14; // [sp+20h] [bp-2Ch]@1
char v15; // [sp+28h] [bp-24h]@0
signed int v16; // [sp+28h] [bp-24h]@1
signed int v17; // [sp+2Ch] [bp-20h]@1
void *v18; // [sp+30h] [bp-1Ch]@1
int v19; // [sp+38h] [bp-14h]@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[i] = *(_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[i] = *(_BYTE *)(v7[2 * i]) ^ *((_BYTE *)v13[i])
。
这个2*i
比较怪,是什么呢?我们查一下JNI的GetStringChars
方法,由参考链接1可知它返回的是Unicode格式的char*
,所以2字节算一个字符。
接下来看MagicImageUtils.readMagicImage
:
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[v0.size()];
Iterator v7 = v0.iterator();
int v2_1;
for(v2_1 = 0; v7.hasNext(); ++v2_1) {
Object v3_1 = v7.next();
v5_2[v2_1] = (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
函数:
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[i] = (dat[i] - 1) ^ _key[i % 16] ^ 0x61
。约定_key[]
是getKey()
的返回值,注意_key[]
长度仍为16。
有一个MISC的基础知识,png格式的头部是固定的,因此我们认为png[]
的前16个字节已知,这样就很容易解出_key[]
了。于是我们就得到了png[]
。至此,我们发现:
- 即使没看懂
getKey
也能过这题。
- 作者怕我们看不懂Java处理图片的操作,贴心地提供了一个正常图片的处理函数
MagicImageUtils.readImage
。
- 有没有发现,题目MagicImageViewer是一语双关?我们通过已知的png头,即magic number,拿到整个png。
当然我们可以更进一步,求出输入串。v5[]
就是_key[]
,而上面已经分析出_key[i] = v7[i] ^ v13[i]
,而_key
上一步已经求出,故v7
,即输入串,可以求出。
代码
b
函数是为了处理dat[i] = 0
的情况,如果有更优雅的方法,教教我。
goal
是所求png的前16个字节。
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[i] - 1) ^ 0x61 ^ goal[i] ^ ord(key_s[i])
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[i] - 1) ^ 0x61 ^ goal[i])
with open('ans.png', 'wb') as g:
for i in range(len(dat)):
g.write(get_byte(b(dat[i] - 1) ^ 0x61 ^ _key[i & 0xf]))
参考链接
- https://www.cnblogs.com/lijunamneg/archive/2012/12/22/2828891.html