探索黑客技术攻防,实战研究与安全创新

导航菜单

自己动手打造 APK安装器

为了方便安装Android软件,APK安装器这类软件在网上出现了,大家用的最多的可能就属 91手机助手自带的安装器了。今天我们使用 C++动手来编写一个这样的工具,在开始之前,我们有必要了解一下APK文件是怎样的方式安装到用户手机中的。细分起来,Android程序的安装有下面四种方式:

1.系统程序安装:开机时安装,这类安装没有安装界面

2.通过Market安装:直接通过Market进行网络安装,这类安装没有安装界面

3.ADB工具安装:使用ADB工具进行安装,这类安装没有安装界面

4.手机自带安装:通过SD卡里的APK文件安装,这类安装有安装界面这四种方法,仔细想想,我们能下手的就是通过 ADB工具来安装了,ADB(Android调试桥)是AndroidSDK中为开发人员提供的一款强大的程序调试测试工具,集成了安装,卸载,调试,设备管理等多个功能,详细的adb命令参数自己可以运行adb得到。现在我们只关心需要用到的功能。当 Android手机设备使用   USB调试模式连上计算机后,可以运行adb.exe devices来列出连接上的设备,如图1所示为开启一个Android模拟器后的,在设备已连接的情况下,可以直接运行 adb install APK file安装一个 APK程序或运行adb -s serial number install APK file来对指定的设备进行软件安装,后者是在连接多个设备的情况下才用的,我们这里为了简单就不考虑了,安装效果。在经过一小段时间的等待后,提示Success说明安装成功,不过在实际安装的过程中,有时候adb返回后没有Success提示但程序也安装成功了,这让人很郁闷,因为这样就无法捕获APK是否已经正确安装了,为了简化,我直接返回成功了。

APK程序的卸载也很简单,输入adb  uninstall package就可以了,这里的package是软件的包名,每个APK程序安装后有个APK程序名,也有个包名,我们可以通过获取APK文件里面的信息来获取这个包名。卸载效果。

如果输入的package在手机中不存在的话,会返回如图4所示的结果。

这样就可以通过查看返回的数据里面有无Failure来判断APK是否成功卸载。到这里,我们可以完成手机是否已经正常连接、安装、卸载等功能了,我们继续看看如何获取   APK文件本身的信息,我们通常见到的APK安装器都可以显示APK文件的软件名、包名、文件大小、最低系统版本、权限等数据。这些数据的获取可以通过运行AAPT.exe来获取,AAPT是Android SDK中提供的另外一个工具,它提供了对APK进行信息列举、文件添加、文件删除及资源打包到APK等功能。我们要用到的是它的dump命令。

我们只需运行aapt d badging APK file就可以获取一个APK的详细信息。

这里的“package:name=”就是软件的包名,“versionName”就是软件的版本,“application: label”就是软件对外显示的名称,“icon=”就是软件显示的图标,“uses-permission:”就是软件用到的权限,这些权限有一部分是流氓及病毒软件需要用到的,在安装 APK之前,我们有必要查看它所需的权限来对它的危险度进行粗略的判断,“sdkVersion:”就是软件运行允许的Android最低版本,这里的数字7表示是Android2.1系统,不同版本的Android都有相应数字版本号,截止到最新的Android4.0版本它们的对应关系。

还有一种情况,当APK文件名无效或非法的APK文件的时候,aaptdbadgingAPKfile会返回错误信息。

这可能是 AAPT处理上的一个 BUG,我们将文件名改成普通英文名后即可正常返回   APK文件信息,所以,大家在安装APK程序的时候,请将APK文件名改成英文并且不要包含空格。本来AAPT与ADB这两个工具在Android源码中都可以找到,但是提取所需相关功能出来还绝非易事,研究了一下果断的放弃了,不过从技术研究的角度出发,我觉得大家还是可以深入研究的。整个APK安装器的思路就是通过CreateProcess调用AAPT与ADB进行相应的操作,而返回数据的获取我用到了管道,使用CreatePipe创建读句柄,在父进程中通过ReadFile来读取数据。

介绍到这里,我们需要的信息都已经获取了,下面是代码时间。APK文件的操作我封装进了CAPKHeaper类,类的声明如下:


class CAPKHelper
{
protected:
mapstring,string m_mapPermission;
string m_adbPath;
string m_aaptPath;
string m_apkName;
string m_softName;
string m_PkgName;
string m_Version;
string m_OSVersion;
string m_FileSize;
string m_Permission;
string m_iconFile;
public:
CAPKHelper(const char *aaptPath,constchar *adbPath, const char*apkFile);
~CAPKHelper(void);
protected:
const string PrePare(APKFTyte flag);  //准备命令字符串
const string DoCommand(const string cmd); //执行命令并返回结果
void InitPermissionMap();
public:
void AdbPath(const char* val);
void AaptPath(const char* val);
const char* ApkName() const;
void ApkName(const char * val);
const char* AdbPath() const;
const char* AaptPath() const;
const char* Permission() const
;
const char* AppName() const;
const char* PkgName() const;
const char* Version() const;
const char* OSVersion() const;
const char* FileSize() const;
void GetInfo();
BOOL GetDevices();
BOOL Install(BOOL bSDCard);
BOOL UnInstall();
void UnzipIcoFile(string outPath);
};
设备的检查使用GetDevices()方法,信息的获取使用GetInfo()方法,安装与卸载则分别是BOOL Install(BOOL bSDCard)与BOOL  UnInstall(),看看GetDevices()的代码如下:
BOOL CAPKHelper::GetDevices()
{
string str;
str = DoCommand(PrePare(APKF_DEVICES));
string::size_type posbegin;
posbegin = str.find(devices);
if (posbegin == string::npos)
return false;
else
{
posbegin = posbegin + devices.size();
string strDevice = ;
strDevice.assign(str, posbegin, str.size()-posbegin);
posbegin = strDevice.find(device);
if(posbegin == string::npos)
return false;
}
return true;
}
代码首先通过PrePare(APKF_DEVICES)来获取需要执行的命令字符串,不同的命令类型用了一个枚举量来表示:
typedef enum tagApkFType{//flags for PrePare
APKF_INFO,
APKF_DEVICES,
APKF_INSTALL,
APKF_INSTALLSDCARD,
APKF_UNINSTALL
}  APKFTyte;
获取命令后通过DoCommand()来执行并返回执行结果,然后对结果进行处理。接下来看看PrePare的代码:
const string CAPKHelper::PrePare( APKFTyte flag )
{
string cmd = ;
switch (flag)
{
case APKF_INFO:
if(m_apkName.empty() || m_aaptPath.empty()) return ;
cmd = m_aaptPath +  d badging \ + m_apkName + ;
break;
case APKF_DEVICES:
if(m_adbPath.empty()) return ;
cmd = m_adbPath +  devices ;
;
break;
case  APKF_INSTALL:
if(m_apkName.empty() || m_adbPath.empty()) return ;
cmd = m_adbPath +  install \ + m_apkName + ;
break;
case APKF_INSTALLSDCARD:
if(m_apkName.empty() || m_adbPath.empty()) return ;
cmd = m_adbPath +  install -s \ + m_apkName + ;
break;
case  APKF_UNINSTALL:
if(m_appName.empty() || m_adbPath.empty()) return ;
cmd = m_adbPath +  uninstall  + m_PkgName;
break;
default:
return ;
break;
}
return cmd;
}
这个方法很简单,就不多讲了,类的执行核心在DoCommand中,代码如下:
const string CAPKHelper::DoCommand( const string cmd )
{
STARTUPINFO si;
SECURITY_ATTRIBUTES sa;
PROCESS_INFORMATION pi;
string str=;
char ReadBuf[1024] = {0};
DWORD ReadNum;
HANDLE hRead; //管道读句柄
HANDLE hWrite; //管道写句柄
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.bInheritHandle = true;
sa.lpSecurityDescriptor = NULL;
BOOL bRet = CreatePipe(hRead, hWrite, sa, 0); //创建匿名管道
if(!bRet) return ;
GetStartupInfo(si);
si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
si.hStdOutput = hWrite;
si.hStdError = hWrite;
si.hStdInput = hRead;
bRet = CreateProcess(NULL, (char *)cmd.c_str(), NULL, NULL,
true, CREATE_NEW_CONSOLE, NULL, NULL, si, pi);
if (!bRet)
{
CloseHandle(hRead);
CloseHandle(hWrite);
return ;
}
CloseHandle(hWrite);
WaitForSingleObject(pi.hProcess, 3000); //等待执行返回
while(ReadFile(hRead, ReadBuf, sizeof(ReadBuf)-1, ReadNum, NULL))
{
ReadBuf[ReadNum] = \0;
str = str + ReadBuf;
}
str = utf2ansi(str.c_str(), str.size());
CloseHandle(hRead);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return str;
}


这段代码是典型的 WIN32代码,首先通过 CreatePipe()创建管道,设置子进程的读写句柄,然后创建子进程并获取返回信息,在这里需要注意的是AAPT返回的是UTF-8编码的字符串,如果APK软件名为中文的话我们直接获取的话在程序中就会显示为乱码,所以需要加上这行代码:str = utf2ansi(str.c_str(), str.size()),它的作用就是将UTF8格式编码的字符串转换为ANSI编码:


std::string utf2ansi(LPCSTR pszSrc, int  nLen)
{
int nSize = MultiByteToWideChar(CP_UTF8, 0, (LPCSTR)pszSrc, nLen, 0, 0);
if(nSize = 0) return  NULL;
WCHAR *pwsz = new  WCHAR[nSize+1];
if( NULL == pwsz) return  NULL;
MultiByteToWideChar(CP_UTF8, 0,(LPCSTR)pszSrc, nLen, pwsz, nSize);
pwsz[nSize] = 0;
char *psz = new char[nSize+1];
WideCharToMultiByte(CP_ACP, 0, pwsz, nSize, psz, nSize, NULL, NULL);
string str =  psz;
delete  pwsz;
delete  psz;
return  str;
}


GetInfo()、Install()、Uninstall()对返回的数据进行分析并给出最终的执行结果,Install()与 Uninstall()代码很简单,就不列举了,大家可以看已查看源码,具体的说说GetInfo(),它的处理还是相对较难的,GetInfo()通过DoCommand()接收到类似图6的数据并进行提取,然后设置到类的相应成员当中,为了使字符串操作简化,我使用了基于STL模板类中的string类,这个类很强大,提供了字符串除正则表达式外的一切操作,这使得GetInfo()的代码量更少更美观,截取一段处理代码如下:


void CAPKHelper::GetInfo()
{
string str;
str = DoCommand(PrePare(APKF_INFO));
string::size_type posbegin;
string::size_type posend;
posbegin = str.find(errorstr);
if (posbegin != string::npos)
{
//APK文件是否有效
m_softName = ;
m_PkgName = ;
m_iconFile = ;
m_Version = ;
m_OSVersion = ;
m_FileSize = ;
m_Permission = ;
return;
}
posbegin = str.find(softName);
if (posbegin == string::npos)
m_softName = ;
else
{
posbegin = posbegin + softName.size();
posend = str.find(\, posbegin);
m_softName.assign(str, posbegin, posend-posbegin);  //获取APK的软件对外显示名称
}
posbegin = str.find(pkgName);
if (posbegin == string::npos)
m_PkgName = ;
else
{
posbegin = posbegin + pkgName.size();
posend = str.find(\, posbegin);
m_PkgName.assign(str, posbegin, posend-posbegin);  //获取 APK的软件包名称
}
......
......
HANDLE hApk = CreateFile(m_apkName.c_str(), GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL );
if(hApk == INVALID_HANDLE_VALUE)
m_FileSize = ;
else
{
DWORD dwSize = GetFileSize(hApk, NULL);
DOUBLE fSize = 0;
string strend = ;
if (dwSize = 1024*1024)
{
fSize = dwSize * 1.0 / 1024 / 1024;
strend = MB;
}
else if (dwSize = 1024)
{
fSize = dwSize * 1.0 / 1024;
strend = KB;
}
else
{
fSize = dwSize;
strend = 字节;
}
char buf[16] = {0};
m_FileSize = _gcvt(fSize, 3, buf);
m_FileSize += strend;
CloseHandle(hApk);
}
posbegin = str.find(iconfile);
if (posbegin == string::npos)
m_OSVersion = ;
else
{
}
posbegin = posbegin + iconfile.size();
posend = str.find(\, posbegin);
m_iconFile.assign(str, posbegin, posend-posbegin);
}


使用string类的find方法对特征字符串进行查找,特征字符串在上面讲解ADB与AAPT时有用到,它们分别是:


const string errorstr = ERROR: dump failed;
const string appName = application: label=;
const string pkgName = package: name=;
const string version = versionName=;
const string osversion = sdkVersion:;
const string permission = uses-permission:;
const string iconfile = icon=;
const string devices = List of devices attached;


如果找到相应的特征字符串就提取相应的值并设置相应的成员变量,反之则设为空。权限字符串的处理是比较麻烦的,查找permission会返回一系列以  uses-permission:打头的字符串,而我们程序需要显示的是权限的描述,为此,我将这些权限与之对应的描述创建到了一个STL的Map对象中,mapstring,stringm_mapPermission,而初始化的操作封装到了InitPermissionMap()中,代码如下:


void  CAPKHelper::InitPermissionMap()
{
m_mapPermission.clear();
m_mapPermission.insert(pairstring,string(android.permission.ACCESS_COARSE_LOCATION,允许获取粗略的位置信息));
m_mapPermission.insert(pairstring,string(android.permission.ACCESS_FINE_LOCATION,允许访问GPS));
......
......
m_mapPermission.insert(pairstring,string(android.permission.ACCESS_MOCK_LOCATION,获取模拟定位信息));
m_mapPermission.insert(pairstring,string(android.permission.CHANGE_COMPONENT_ENABLED_STATE,改变组件启用状态));
}


这样访问权限描述就很简单了,代码如下:


string strTmp =  ;
string strInfo =  ;
posbegin = 0;
while((posbegin = str.find(permission, posbegin)) !=  string::npos)
{
posbegin = posbegin +  permission.size();
posend = str.find(\, posbegin);
strTmp.assign(str, posbegin, posend-posbegin);  //获取APk使用到的权限
strInfo =  m_mapPermission[strTmp];
if(!strInfo.empty())
m_Permission = m_Permission + strInfo +  \n;
posbegin = posend; //下一个
}


这个CAPKHeaper类就讲到这里,大家可以在代码中查看类关系图更详细了解它的流程,我们接下来看看主程序的逻辑,先看看程序设计的界面,如图8所示:

图片8.png

图8

程序的编写基于MFC的对话框,我们看看安装按钮的响应:


voidCAPKInstallerDlg::OnBnClickedInstall()

{

if(!m_helper-GetDevices())

{

m_syslnkShow.SetWindowText(手机未连接或手机驱动没有安装好,ahref=\http://*******\);

return;

}

if(m_apkPath.empty())

OpenAPK();

if(m_apkPath.empty())return;

m_threadType=THREADF_INSTALL;

unsignedintdwThreadID=0;

HANDLEh=(HANDLE)_beginthreadex(NULL,0,www.weixianmanbu.com ThreadProc,(LPVOID)this,0,dwThreadID);

;

}


线程的创建使用了WIN32的_beginthreadex(),所有对APK的操作都封装进这个ThreadProc()线程中,代码如下:


unsigned__stdcallCAPKInstallerDlg::ThreadProc(LPVOIDlpParam)
{
EnterCriticalSection(g_cs);
CAPKInstallerDlg*pWnd=(CAPKInstallerDlg*)lpParam;
switch(pWnd-m_threadType)
{
caseTHREADF_DEVICES:
pWnd-m_syslnkShow.SetWindowText(正在检查已连接的手机);
if(pWnd-m_helper-GetDevices())
{
pWnd-m_syslnkShow.SetWindowText(手机连接良好);
}
else
{
pWnd-m_syslnkShow.SetWindowText(手机未连接或手机驱动没有安装好,a
href=\http://*******\点击此处/a下载驱动);
}
break;
caseTHREADF_INSTALL:
pWnd-m_btnUninstall.EnableWindow(FALSE);
pWnd-m_syslnkShow.SetWindowText(正在安装软件...);
pWnd-m_helper-Install(pWnd-m_cbPath.GetCurSel());
pWnd-m_syslnkShow.SetWindowText(软件安装完成);
pWnd-m_btnUninstall.EnableWindow(TRUE);
break;
caseTHREADF_UNINSTALL:
pWnd-m_btnInstall.EnableWindow(FALSE);
pWnd-m_syslnkShow.SetWindowText(正在卸载软件...);
if(pWnd-m_helper-UnInstall())
{
pWnd-m_syslnkShow.SetWindowText(软件卸载完成);
}
else
{
pWnd-m_syslnkShow.SetWindowText(软件卸载失败);
}
pWnd-m_btnInstall.EnableWindow(TRUE);
break;
}
LeaveCriticalSection(g_cs);
return1;
}


各种操作信息的显示我使用了syslink控件,使用它的一个好处就是这个控件支持如html的链接一样的显示效果,它们的鼠标点击处理代码统一如下:


voidCAPKInstallerDlg::OnNMClickLinkurl(NMHDR*pNMHDR,LRESULT*pResult)
{
PNMLINKpNMLink=(PNMLINK)pNMHDR;
wstringwstr=pNMLink-item.szUrl;
stringstr=wstring2string(wstr);
::ShellExecute(m_hWnd,open,str.c_str(),NULL,NULL,SW_SHOWNORMAL);
*pResult=0;
}


注意这行:wstringwstr=pNMLink-item.szUrl,syslink点击消息返回的链接地址为WCHAR*类型,这使用我们使用前需要先转换一下:


stringwstring2string(constwstringws)
{
_bstr_tt=ws.c_str();
char*pchar=(char*)t;
stringresult=pchar;
returnresult;
}


说说APK文件图标的获取,在上面图6显示的信息中,就能够知道APK图标的文件名与位置,而这个文件存放在APK这个ZIP包中,AAPT不提供ZIP解压的功能,需要自己来提取APK中的文件,这里为了代码轻便使用了XUNZIP类,获取图标并解压的工作放到了voidUnzipIcoFile(stringoutPath)方法中:


voidCAPKHelper::UnzipIcoFile(stringoutPath)
{
HZIPhZip=OpenZip((void*)(char*)m_apkName.c_str(),0,ZIP_FILENAME);
intindex=-1;
ZIPENTRYze;
ZRESULTbRet=FindZipItem(hZip,m_iconFile.c_str(),TRUE,index,ze);
if(bRet==ZR_OK)
{
UnzipItem(hZip,index,(void*)outPath.c_str(),0,ZIP_FILENAME);
}
CloseZip(hZip);
}


在主程序中处理如下:


charszBuffer[MAX_PATH]={0};
GetTempPath(MAX_PATH,szBuffer);
m_outpngPath=szBuffer;
m_outpngPath+=m_helper-PkgName();
m_outpngPath+=m_helper-Version();
m_outpngPath+=.png;
//APK文件图标释放路径
m_helper-UnzipIcoFile(m_outpngPath.c_str());
ShowPNG();//显示图标


最后调用ShowPNG()来显示图标,一般APK中的图标为PNG,在VC中显示PNG使用GDI+再方便不过了,代码如下:


voidCAPKInstallerDlg::ShowPNG()
{
if(!m_outpngPath.empty())
{
wstringstr=string2wstring(m_outpngPath);
Imageimage(str.c_str());
RECTrc;
m_stcPic.GetWindowRect(rc);
m_pGraphics-DrawImage(image,0,0,rc.right-rc.left,rc.bottom-rc.top);
}
}


但是直接显示PNG后,程序如果被刷新图片就不再显示了,因此我们需要在WM_PAINT消息中做进一步的处理:


voidCAPKInstallerDlg::ShowPNG()
{
if(!m_outpngPath.empty())
{
wstringstr=string2wstring(m_outpngPath);
Imageimage(str.c_str());
RECTrc;
m_stcPic.GetWindowRect(rc);
m_pGraphics-DrawImage(image,0,0,rc.right-rc.left,rc.bottom-rc.top);
}
}


在调用ShowPNG()之前,CDialog::UpdateWindow()这行代码一定要先加上,否则图标照样不会显示,好了,整体上程序代码很简单,一目了然,到这里我们的程序就算是完成了,看看运行效果如下图:


图片9.png

图片10.png

图片11.png

如果文中有任何语言描述或代码上的错误,欢迎大家批评指正,同时也欢迎大家与我一起交流Android技术。