安卓逆向 MagicImageViewer技巧分享

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





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

安卓逆向 MagicImageViewer技巧分享

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




反编译


反编译main activity


直接把APK丢到Jadx中,根据反编译情况来看没有加壳。幸好以前做过几天的APP,这代码看着很亲切的感觉。
        
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 ,跳到函数代码处,如下:
    
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的这个函数验证了这个。

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





反编译 so


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

安卓逆向 MagicImageViewer技巧分享

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

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

Java_re_sdnisc2018_sdnisc_1apk2_MagicImageUtils_decrypt 000069E0
Java_re_sdnisc2018_sdnisc_1apk2_MainActivity_getKey 000067E0

双击函数直接来到函数代码处,因为牵扯到JavaVM JNIEnv的类,反汇编代码很难看清逻辑,直接F5吧。

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

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文件,所以卡住了。

if ( (v9 & 1) == 0 )
v5 = (char *)v14 + 1;


柳暗花明


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

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

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

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

伪代码如下:

_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了。

_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。

安卓逆向 MagicImageViewer技巧分享





故事还没有结束


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

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

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

出乎意料呀getKey如此清爽(设置几个数据类型,对几个函数调用强制Force call )。

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。

#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);
}

编译运行

C:UsersdebuggerDesktopCTFbugkuMagicImageViewer>gcc -o keygen.exe ctf.c keygen.c
C:UsersdebuggerDesktopCTFbugkuMagicImageViewer>keygen.exe
key: XaE3*2#@!qV^v+_.
write 'decrypt.png' OK!





验证


安卓逆向 MagicImageViewer技巧分享



安卓逆向 MagicImageViewer技巧分享


看雪ID:zhenwo

https://bbs.kanxue.com/user-home-396123.htm

*本文为看雪论坛优秀文章,由 zhenwo 原创,转载请注明来自看雪社区

安卓逆向 MagicImageViewer技巧分享


# 往期推荐

1、区块链智能合约逆向-合约创建-调用执行流程分析

2、在Windows平台使用VS2022的MSVC编译LLVM16

3、神挡杀神——揭开世界第一手游保护nProtect的神秘面纱

4、为什么在ASLR机制下DLL文件在不同进程中加载的基址相同

5、2022QWB final RDP

6、华为杯研究生国赛 adv_lua


安卓逆向 MagicImageViewer技巧分享


安卓逆向 MagicImageViewer技巧分享

球分享

安卓逆向 MagicImageViewer技巧分享

球点赞

安卓逆向 MagicImageViewer技巧分享

球在看

原文始发于微信公众号(看雪学苑):安卓逆向 MagicImageViewer技巧分享

版权声明:admin 发表于 2024年2月21日 下午6:04。
转载请注明:安卓逆向 MagicImageViewer技巧分享 | CTF导航

相关文章