1. 程式人生 > >Apk加殼實現

Apk加殼實現

前幾天在網上看到一篇不錯的介紹關於apk加殼的介紹,Android中的Apk的加固(加殼)原理解析和實現,針對裡面關於資源載入這塊自己研究了下,給出了一個方案,下面結合那篇文章的內容做一下apk加殼流程介紹 

一、將目標apk加密放進殼apk的classes.dex裡面,程式碼如下

package com.example;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.zip.Adler32;


public class MyClass {
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        try {
            File payloadSrcFile = new File("C:\\Users\\Desktop\\force\\ForceApkObj.apk");   //需要加殼的程式
            System.out.println("apk size:"+payloadSrcFile.length());
            File unShellDexFile = new File("C:\\Users\\Desktop\\force\\ForceApkObj.dex");    //解客dex
            byte[] payloadArray = encrpt(readFileBytes(payloadSrcFile));//以二進位制形式讀出apk,並進行加密處理//對源Apk進行加密操作
            byte[] unShellDexArray = readFileBytes(unShellDexFile);//以二進位制形式讀出dex
            int payloadLen = payloadArray.length;
            int unShellDexLen= unShellDexArray.length;
            int totalLen= payloadLen + unShellDexLen +4;//多出4位元組是存放長度的。
            byte[] newdex = newbyte[totalLen]; // 申請了新的長度
            //新增解殼程式碼
            System.arraycopy(unShellDexArray, 0, newdex, 0, unShellDexLen);//先拷貝dex內容
            //新增加密後的解殼資料
            System.arraycopy(payloadArray, 0, newdex, unShellDexLen, payloadLen);//再在dex內容後面拷貝apk的內容
            //新增解殼資料長度
            System.arraycopy(intToByte(payloadLen),0, newdex, totalLen-4, 4);//最後4為長度
            //修改DEXfile size檔案頭
            fixFileSizeHeader(newdex);
            //修改DEXSHA1 檔案頭
            fixSHA1Header(newdex);
            //修改DEXCheckSum檔案頭
            fixCheckSumHeader(newdex);

            String str = "C:\\Users\\jalen_yang\\Desktop\\force\\classes.dex";
            File file = new File(str);
            if (!file.exists()){
                file.createNewFile();
            }

            FileOutputStreamlocalFileOutputStream = new FileOutputStream(str);
           localFileOutputStream.write(newdex);
           localFileOutputStream.flush();
           localFileOutputStream.close();


        } catch (Exceptione) {
            e.printStackTrace();
        }
    }

    //直接返回資料,讀者可以新增自己加密方法
    private static byte[] encrpt(byte[]srcdata){
        for(int i = 0;i<srcdata.length;i++){
            srcdata[i] = (byte)(0xFF ^ srcdata[i]);
        }
        return srcdata;
    }

    /**
     * 修改dex頭,CheckSum 校驗碼
     * @param dexBytes
     */
    private static void fixCheckSumHeader(byte[]dexBytes) {
        Adler32 adler = new Adler32();
        adler.update(dexBytes, 12, dexBytes.length - 12);//從12到檔案末尾計算校驗碼
        long value = adler.getValue();
        int va = (int) value;
        byte[]newcs = intToByte(va);
        //高位在前,低位在前掉個個
        byte[] recs = newbyte[4];
        for (int i = 0; i < 4; i++){
            recs[i] = newcs[newcs.length - 1 - i];
            System.out.println(Integer.toHexString(newcs[i]));
        }
        System.arraycopy(recs, 0, dexBytes, 8, 4);//效驗碼賦值(8-11)
        System.out.println(Long.toHexString(value));
        System.out.println();
    }


    /**
     * int 轉byte[]
     * @param number
     * @return
     */
    public static byte[] intToByte(intnumber) {
        byte[] b =new byte[4];
        for (int i = 3; i >= 0; i--){
            b[i] = (byte) (number % 256);
            number >>= 8;
        }
        return b;
    }

    /**
     * 修改dex頭 sha1值
     * @param dexBytes
     * @throws NoSuchAlgorithmException
     */
    private static void fixSHA1Header(byte[]dexBytes)
            throws NoSuchAlgorithmException{
        MessageDigest md = MessageDigest.getInstance("SHA-1");
        md.update(dexBytes, 32, dexBytes.length - 32);//從32為到結束計算sha--1
        byte[] newdt = md.digest();
        System.arraycopy(newdt, 0, dexBytes, 12, 20);//修改sha-1值(12-31)
        //輸出sha-1值,可有可無
        String hexstr = "";
        for (int i = 0; i < newdt.length; i++){
            hexstr += Integer.toString((newdt[i]& 0xff) + 0x100, 16)
                    .substring(1);
        }
        System.out.println(hexstr);
    }

    /**
     * 修改dex頭 file_size值
     * @param dexBytes
     */
    private static void fixFileSizeHeader(byte[]dexBytes) {
        //新檔案長度
        byte[] newfs = intToByte(dexBytes.length);
        System.out.println(Integer.toHexString(dexBytes.length));
        byte[]refs = new byte[4];
        //高位在前,低位在前掉個個
        for (int i = 0; i < 4; i++){
            refs[i] = newfs[newfs.length - 1 - i];
            System.out.println(Integer.toHexString(newfs[i]));
        }
        System.arraycopy(refs, 0, dexBytes, 32, 4);//修改(32-35)
    }


    /**
     * 以二進位制讀出檔案內容
     * @param file
     * @return
     * @throws IOException
     */
    private static byte[] readFileBytes(File file) throws IOException{
        byte[]arrayOfByte = new byte[1024];
        ByteArrayOutputStreamlocalByteArrayOutputStream = new ByteArrayOutputStream();
        FileInputStream fis = new FileInputStream(file);
        while (true) {
            int i =fis.read(arrayOfByte);
            if (i !=-1) {
               localByteArrayOutputStream.write(arrayOfByte, 0, i);
            } else {
                return localByteArrayOutputStream.toByteArray();
            }
        }
    }
}

 

二、     殼apk動態載入目標apk,注意的是目前發現android studio2.2開始編譯的debug apk裡面會出現兩個dex,然後導致動態載入失敗,這是因為studio2.2開始預設打開了 Instant Run選項,所以測試動態載入時不要開啟這個開關,2.2開始需要自己手動去Settings裡面關掉

package com.example.reforceapk;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import android.app.Application;
import android.app.Instrumentation;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.content.res.Resources.Theme;
import android.os.Bundle;
import android.util.ArrayMap;
import android.util.Log;
import dalvik.system.DexClassLoader;

public class ProxyApplication extends Application{
   private static final String appkey = "APPLICATION_CLASS_NAME";
   private String apkFileName;
   private String odexPath;
   private String libPath;

   //這是context 賦值
   @Override
   protected void attachBaseContext(Context base) {
      super.attachBaseContext(base);
      try {
         //建立兩個資料夾payload_odex,payload_lib 私有的,可寫的檔案目錄
         File odex = this.getDir("payload_odex", MODE_PRIVATE);
         File libs = this.getDir("payload_lib", MODE_PRIVATE);
         odexPath = odex.getAbsolutePath();
         libPath = libs.getAbsolutePath();
         apkFileName = odex.getAbsolutePath() + "/payload.apk";
         File dexFile = new File(apkFileName);
         Log.i("demo", "apk size:"+dexFile.length());
         if (!dexFile.exists())
         {
            dexFile.createNewFile();  //在payload_odex資料夾內,建立payload.apk
            // 讀取程式classes.dex檔案
            byte[] dexdata = this.readDexFileFromApk();

            // 分離出解殼後的apk檔案已用於動態載入
            this.splitPayLoadFromDex(dexdata);
         }
         // 配置動態載入環境
         Object currentActivityThread = RefInvoke.invokeStaticMethod(
               "android.app.ActivityThread", "currentActivityThread",
               new Class[] {}, new Object[] {});//獲取主執行緒物件 http://blog.csdn.net/myarrow/article/details/14223493
         String packageName = this.getPackageName();//當前apk的包名
         //下面兩句不是太理解
         ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect(
               "android.app.ActivityThread", currentActivityThread,
               "mPackages");
         WeakReference wr = (WeakReference) mPackages.get(packageName);
         //建立被加殼apk的DexClassLoader物件  載入apk內的類和原生代碼(c/c++程式碼)
         DexClassLoader dLoader = new DexClassLoader(apkFileName, odexPath,
               libPath, (ClassLoader) RefInvoke.getFieldOjbect(
               "android.app.LoadedApk", wr.get(), "mClassLoader"));
         //base.getClassLoader(); 是不是就等同於 (ClassLoader) RefInvoke.getFieldOjbect()? 有空驗證下//?
         //把當前程序的DexClassLoader 設定成了被加殼apk的DexClassLoader  ----有點c++中程序環境的意思~~
         RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader",
               wr.get(), dLoader);

         Log.i("demo","classloader:"+dLoader);

         try{
            Object actObj = dLoader.loadClass("com.example.forceapkobj.MainActivity");
            Log.i("demo", "actObj:"+actObj);
         }catch(Exception e){
            Log.i("demo", "activity:"+Log.getStackTraceString(e));
         }


      } catch (Exception e) {
         Log.i("demo", "error:"+Log.getStackTraceString(e));
         e.printStackTrace();
      }
   }

   @Override
   public void onCreate() {
      {
         loadResources(apkFileName);

         Log.i("demo", "onCreate");
         // 如果源應用配置有Appliction物件,則替換為源應用Applicaiton,以便不影響源程式邏輯。
         String appClassName = null;
         try {
            ApplicationInfo ai = this.getPackageManager()
                  .getApplicationInfo(this.getPackageName(),
                        PackageManager.GET_META_DATA);
            Bundle bundle = ai.metaData;
            if (bundle != null && bundle.containsKey("APPLICATION_CLASS_NAME")) {
               appClassName = bundle.getString("APPLICATION_CLASS_NAME");//className 是配置在xml檔案中的。
            } else {
               Log.i("demo", "have no application class name");
               return;
            }
         } catch (NameNotFoundException e) {
            Log.i("demo", "error:"+Log.getStackTraceString(e));
            e.printStackTrace();
         }
         //有值的話呼叫該Applicaiton
         Object currentActivityThread = RefInvoke.invokeStaticMethod(
               "android.app.ActivityThread", "currentActivityThread",
               new Class[] {}, new Object[] {});
         Object mBoundApplication = RefInvoke.getFieldOjbect(
               "android.app.ActivityThread", currentActivityThread,
               "mBoundApplication");
         Object loadedApkInfo = RefInvoke.getFieldOjbect(
               "android.app.ActivityThread$AppBindData",
               mBoundApplication, "info");
         //把當前程序的mApplication 設定成了null
         RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication",
               loadedApkInfo, null);
         Object oldApplication = RefInvoke.getFieldOjbect(
               "android.app.ActivityThread", currentActivityThread,
               "mInitialApplication");
         //http://www.codeceo.com/article/android-context.html
         ArrayList<Application> mAllApplications = (ArrayList<Application>) RefInvoke
               .getFieldOjbect("android.app.ActivityThread",
                     currentActivityThread, "mAllApplications");
         mAllApplications.remove(oldApplication);//刪除oldApplication

         ApplicationInfo appinfo_In_LoadedApk = (ApplicationInfo) RefInvoke
               .getFieldOjbect("android.app.LoadedApk", loadedApkInfo,
                     "mApplicationInfo");
         ApplicationInfo appinfo_In_AppBindData = (ApplicationInfo) RefInvoke
               .getFieldOjbect("android.app.ActivityThread$AppBindData",
                     mBoundApplication, "appInfo");
         appinfo_In_LoadedApk.className = appClassName;
         appinfo_In_AppBindData.className = appClassName;
         Application app = (Application) RefInvoke.invokeMethod(
               "android.app.LoadedApk", "makeApplication", loadedApkInfo,
               new Class[] { boolean.class, Instrumentation.class },
               new Object[] { false, null });//執行 makeApplication(false,null)
         RefInvoke.setFieldOjbect("android.app.ActivityThread",
               "mInitialApplication", currentActivityThread, app);


         ArrayMap mProviderMap = (ArrayMap) RefInvoke.getFieldOjbect(
               "android.app.ActivityThread", currentActivityThread,
               "mProviderMap");
         Iterator it = mProviderMap.values().iterator();
         while (it.hasNext()) {
            Object providerClientRecord = it.next();
            Object localProvider = RefInvoke.getFieldOjbect(
                  "android.app.ActivityThread$ProviderClientRecord",
                  providerClientRecord, "mLocalProvider");
            RefInvoke.setFieldOjbect("android.content.ContentProvider",
                  "mContext", localProvider, app);
         }

         Log.i("demo", "app:"+app);

         app.onCreate();
      }
   }

   /**
    * 釋放被加殼的apk檔案,so檔案
    * @param data
    * @throws IOException
    */
   private void splitPayLoadFromDex(byte[] apkdata) throws IOException {
      int ablen = apkdata.length;
      //取被加殼apk的長度   這裡的長度取值,對應加殼時長度的賦值都可以做些簡化
      byte[] dexlen = new byte[4];
      System.arraycopy(apkdata, ablen - 4, dexlen, 0, 4);
      ByteArrayInputStream bais = new ByteArrayInputStream(dexlen);
      DataInputStream in = new DataInputStream(bais);
      int readInt = in.readInt();
      System.out.println(Integer.toHexString(readInt));
      byte[] newdex = new byte[readInt];
      //把被加殼apk內容拷貝到newdex中
      System.arraycopy(apkdata, ablen - 4 - readInt, newdex, 0, readInt);
      //這裡應該加上對於apk的解密操作,若加殼是加密處理的話
      //?

      //對源程式Apk進行解密
      newdex = decrypt(newdex);

      //寫入apk檔案
      File file = new File(apkFileName);
      try {
         FileOutputStream localFileOutputStream = new FileOutputStream(file);
         localFileOutputStream.write(newdex);
         localFileOutputStream.close();
      } catch (IOException localIOException) {
         throw new RuntimeException(localIOException);
      }

      //分析被加殼的apk檔案
      ZipInputStream localZipInputStream = new ZipInputStream(
            new BufferedInputStream(new FileInputStream(file)));
      while (true) {
         ZipEntry localZipEntry = localZipInputStream.getNextEntry();//不瞭解這個是否也遍歷子目錄,看樣子應該是遍歷的
         if (localZipEntry == null) {
            localZipInputStream.close();
            break;
         }
         //取出被加殼apk用到的so檔案,放到 libPath中(data/data/包名/payload_lib)
         String name = localZipEntry.getName();
         if (name.startsWith("lib/") && name.endsWith(".so")) {
            File storeFile = new File(libPath + "/"
                  + name.substring(name.lastIndexOf('/')));
            storeFile.createNewFile();
            FileOutputStream fos = new FileOutputStream(storeFile);
            byte[] arrayOfByte = new byte[1024];
            while (true) {
               int i = localZipInputStream.read(arrayOfByte);
               if (i == -1)
                  break;
               fos.write(arrayOfByte, 0, i);
            }
            fos.flush();
            fos.close();
         }
         localZipInputStream.closeEntry();
      }
      localZipInputStream.close();


   }

   /**
    * 從apk包裡面獲取dex檔案內容(byte)
    * @return
    * @throws IOException
    */
   private byte[] readDexFileFromApk() throws IOException {
      ByteArrayOutputStream dexByteArrayOutputStream = new ByteArrayOutputStream();
      ZipInputStream localZipInputStream = new ZipInputStream(
            new BufferedInputStream(new FileInputStream(
                  this.getApplicationInfo().sourceDir)));
      while (true) {
         ZipEntry localZipEntry = localZipInputStream.getNextEntry();
         if (localZipEntry == null) {
            localZipInputStream.close();
            break;
         }
         if (localZipEntry.getName().equals("classes.dex")) {
            byte[] arrayOfByte = new byte[1024];
            while (true) {
               int i = localZipInputStream.read(arrayOfByte);
               if (i == -1)
                  break;
               dexByteArrayOutputStream.write(arrayOfByte, 0, i);
            }
         }
         localZipInputStream.closeEntry();
      }
      localZipInputStream.close();
      return dexByteArrayOutputStream.toByteArray();
   }


   // //直接返回資料,讀者可以新增自己解密方法
   private byte[] decrypt(byte[] srcdata) {
      for(int i=0;i<srcdata.length;i++){
         srcdata[i] = (byte)(0xFF ^ srcdata[i]);
      }
      return srcdata;
   }


   //以下是載入資源
   protected AssetManager mAssetManager;//資源管理器
   protected Resources mResources;//資源
   protected Theme mTheme;//主題

   protected void loadResources(String dexPath) {
      try {
         AssetManager assetManager = AssetManager.class.newInstance();
         Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
         addAssetPath.invoke(assetManager, dexPath);
         mAssetManager = assetManager;
         try {
            Field mAssets = Resources.class
                  .getDeclaredField("mAssets");
            mAssets.setAccessible(true);
            mAssets.set(super.getResources(), assetManager);
            Log.i("demo", "mAssets  exist, is "+mAssets);
         } catch (Throwable ignore) {
            Log.i("demo", "mAssets don't exist ,search mResourcesImpl:");
            Field mResourcesImpl = Resources.class
                  .getDeclaredField("mResourcesImpl");
            mResourcesImpl.setAccessible(true);
            Object resourceImpl = mResourcesImpl.get(super.getResources());
            Log.i("demo", "mResourcesImpl  exist, is "+resourceImpl);
            Field implAssets = resourceImpl.getClass()
                  .getDeclaredField("mAssets");
            implAssets.setAccessible(true);
            implAssets.set(resourceImpl, assetManager);
         }

      } catch (Exception e) {
         Log.i("demo", "loadResource error:"+Log.getStackTraceString(e));
         e.printStackTrace();
      }
     }
}

這邊重點解釋一下殼apk動態載入資源這一塊,首先解釋一下apk載入資源這塊的大概流程:

1、 Resources物件包含一個AssetManager物件,在7.0以前這個變數直接是Resources的一個屬性,因此要替換AssetManager就要找它的mAssets成員變數,7.0開始Resources物件不再有mAssets屬性,取代的是mResourcesImpl屬性,這個屬性其實就是一個Resources的實現類,它裡面包含了一個AssetManager屬性,所以在7.0上面就要多一個步驟來替換AssetManager了,也就是上面程式碼中那樣

2、 android系統載入apk資源時主要通過AssetManager來解析Resources.arsc檔案,AssetManager

c++類的成員變數mResources指向的是一個ResTable物件,這個ResTable就是解析後的資源索引表,每次載入一個apk就會將這個apkResources.arsc解析後放到這個ResTable裡面,以及字串資源池變數裡面

3、 在android5.0以前AssetManager對於相同的package id,比如0x7f,搜尋資源時是按照逆序,也就是從後往前,第一個找到的就是返回的物件,因此要先載入當前應用的資源,再載入patch的資源,才能實現覆蓋,不過如果找到最後一個發現不存在,就會拋異常,因為android系統不允許新增資源,只允許覆蓋已有資源;android 5.0以後就不一樣了,搜尋相同package id,按照從前往後,因此你要先載入patch資源,再載入當前應用資源,這樣你就得重新建立一個AssetManager了

4、 我們平常看到的R.java裡面的每個id都是由三部分組成的。分別是:mpackage、type、configurelist,mpackage代表的是資源包,由一個位元組表示,比如系統資源包、當前應用資源包、第三方資源包等等,預設情況下系統資源包用0x01表示,當前應用資源包用0x7f,在這兩個值範圍內的都是合法的id,否則是不合法的。Type代表的是資源型別,同樣是一個位元組,比如layout、drawable、string等等;最後兩個位元組代表的是次序,也就是偏移,用來定位具體的資源位置。所以搜尋資源時首先分解id成上面三個型別,然後在資源索引表裡搜尋,注意的是相同的mpackage可能存在多個組,也就是上面提到的覆蓋資源問題,每個package對應的是一個PakcageGroup,而這個PakcageGroup包含多個Package,這裡的Package不是包名,也和mpackage不同,可以看看5.0以前的系統原始碼:

ssize_tResTable::getResource(uint32_t resID, Res_value* outValue, bool mayBeBag, 
        uint32_t* outSpecFlags,ResTable_config* outConfig) const 
{ 
    ...... 
 
    const ssize_t p =getResourcePackageIndex(resID); 
    const int t = Res_GETTYPE(resID); 
    const int e = Res_GETENTRY(resID); 
 
    ...... 
 
    const Res_value* bestValue = NULL; 
    const Package* bestPackage = NULL; 
    ResTable_config bestItem; 
    memset(&bestItem, 0, sizeof(bestItem));// make the compiler shut up 
 
    if (outSpecFlags != NULL) *outSpecFlags =0; 
 
    // Look through all resource packages,starting with the most 
    // recently added. 
    const PackageGroup* const grp =mPackageGroups[p]; 
    ...... 
 
    size_t ip = grp->packages.size(); 
    while (ip > 0) { 
        ip--; 
        int T = t; 
        int E = e; 
 
        const Package* const package =grp->packages[ip]; 
        if (package->header->resourceIDMap){ 
            uint32_t overlayResID = 0x0; 
            status_t retval =idmapLookup(package->header->resourceIDMap, 
                                         package->header->resourceIDMapSize, 
                                          resID, &overlayResID); 
            if (retval == NO_ERROR &&overlayResID != 0x0) { 
                // for this loop iteration,this is the type and entry we really want 
                ...... 
                T =Res_GETTYPE(overlayResID); 
                E =Res_GETENTRY(overlayResID); 
            } else { 
                // resource not present inoverlay package, continue with the next package 
                continue; 
            } 
        } 
 
        const ResTable_type* type; 
        const ResTable_entry* entry; 
        const Type* typeClass; 
        ssize_t offset = getEntry(package, T,E, &mParams, &type, &entry, &typeClass); 
        if (offset <= 0) { 
            // No {entry, appropriate config}pair found in package. If this 
            // package is an overlay package(ip != 0), this simply means the 
            // overlay package did not specifya default. 
            // Non-overlay packages are stillrequired to provide a default. 
            if (offset < 0 && ip ==0) { 
                ...... 
                return offset; 
            } 
            continue; 
        } 
 
        if((dtohs(entry->flags)&entry->FLAG_COMPLEX) != 0) { 
            ...... 
            continue; 
        } 
 
        ...... 
 
        const Res_value* item = 
            (const Res_value*)(((constuint8_t*)type) + offset); 
        ResTable_config thisConfig; 
       thisConfig.copyFromDtoH(type->config); 
 
        if (outSpecFlags != NULL) { 
            if (typeClass->typeSpecFlags !=NULL) { 
                *outSpecFlags |=dtohl(typeClass->typeSpecFlags[E]); 
            } else { 
                *outSpecFlags = -1; 
            } 
        } 
 
       if (bestPackage != NULL && 
           (bestItem.isMoreSpecificThan(thisConfig) || bestItem.diff(thisConfig) ==0)) { 
            // Discard thisConfig not only ifbestItem is more specific, but also if the two configs 
            // are identical (diff == 0), oroverlay packages will not take effect. 
            continue; 
        } 
 
        bestItem = thisConfig; 
        bestValue = item; 
        bestPackage = package; 
    } 
 
    ...... 
 
    if (bestValue) { 
        outValue->size =dtohs(bestValue->size); 
        outValue->res0 =bestValue->res0; 
        outValue->dataType =bestValue->dataType; 
        outValue->data =dtohl(bestValue->data); 
        if (outConfig != NULL) { 
            *outConfig = bestItem; 
        } 
        ...... 
        returnbestPackage->header->index; 
    } 
 
    return BAD_VALUE; 
}