第七届山东省大学生网络安全技能大赛. 安卓逆向 MagicImageViewer
闲来无事刷CTF,今天逛论坛看到了一个CTF《无用的CE》,没有bin文件,只有一个bugku的连接;去首页看了下不少CTF的题目,故注册了账号,每个题下载都要币,幸好初始账号有30个,研究了下,有的题目下载需要3 - 5个币不等,提交flag还能赚1 - 2个币,所以专挑差价多的搞,先下了三个试试水。前两个难度不大,感觉这个有些技巧故分享之。
观
看后缀是apk,丢到夜神模拟器看看。

随便写个code,点击DECRYPT按钮会Toast提示 Something wrong!
反编译
反编译main activity
直接把APK丢到Jadx中,根据反编译情况来看没有加壳。幸好以前做过几天的APP,这代码看着很亲切的感觉。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | 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 ,跳到函数代码处,如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | 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的这个函数验证了这个。
1 2 3 | static { System.loadLibrary("native-lib"); } |
反编译 so
通过jadx可以清楚的看到资源lib下有多个版本的libnative-lib.so文件

直接保存下来,然后挑个自己熟悉的来搞,先从x86的下手。
直接拖入IDA中开始分析,通过导出表很快找到了想要的函数
1 2 | Java_re_sdnisc2018_sdnisc_1apk2_MagicImageUtils_decrypt 000069E0 Java_re_sdnisc2018_sdnisc_1apk2_MainActivity_getKey 000067E0 |
双击函数直接来到函数代码处,因为牵扯到JavaVM JNIEnv的类,反汇编代码很难看清逻辑,直接F5吧。
简单重定义了几个数据类型后,代码看起来舒服多了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | 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文件,所以卡住了。
1 2 | if ( (v9 & 1) == 0 ) v5 = (char *)v14 + 1; |
柳暗花明
回头看了下decrypt倒是很简单。
把流程多看了几遍,突然灵机一动,解密的是png,至少前4字节是固定的。这让我突然想起了,破译希特勒电报的故事,电报的开头固定的格式“希特勒万岁”。
通过分析bg.png,网上下png和其他三个png发现,不仅仅是magic是一样的甚至前16字节完全一样,出乎意料呀,刚开始还以只会是前4字节是一样的。
这样知道亦或算法,处理后的数据和原始数据,很容易推算数key[16]。
伪代码如下
1 2 3 4 5 6 | _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了。
1 2 3 4 5 6 7 8 9 10 | _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

故事还没有结束
一直认为是数据类型影响了IDA对getKey的正常还原。
找了几篇IDA反汇编的so的文章,学了不少技巧,如导入structs,导入JNI.h头文件,以及Type Lib 增加android相关的库,来丰富IDA的还原能力。
又尝试着看看其他几个反编译F5还原的代码都一样吗,接着对x64的so进行了反编译和F5
出乎意料呀getKey如此清爽(设置几个数据类型,对几个函数调用强制Force call )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | #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); } |
编译运行
1 2 3 4 | C:\Users\debugger\Desktop\CTF\bugku\MagicImageViewer>gcc -o keygen.exe ctf.c keygen.cC:\Users\debugger\Desktop\CTF\bugku\MagicImageViewer>keygen.exekey: XaE3*2#@!qV^v+_.write 'decrypt.png' OK! |
验证
