最終目標(biāo):
1作彤、保存發(fā)送的微信語(yǔ)音
2、發(fā)送指定的微信語(yǔ)音
思路:
因?yàn)橐猦ook的是微信語(yǔ)音功能,所以首先要知道微信語(yǔ)音功能的流程瑞妇。
查閱資料以及利用Xposed的hook功能,最終知道微信的語(yǔ)音功能是通過(guò)AudioRecord來(lái)實(shí)現(xiàn)的梭冠,一般的流程是這樣的:
// 1辕狰、初始化audioRecord 對(duì)象
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, frequency, channelConfiguration, EncodingBitRate, recBufSize);
// 2、調(diào)用audioRecord的start方法
audioRecord.startRecording();
// 3控漠、讀取audioRecord的錄音數(shù)據(jù)蔓倍,這些操作跟文件的io操作類似
byte data[] = new byte[recBufSize];
String filename = getTempFilename();
String filename = getTempFilename();
FileOutputStream os = null;
try {
os = new FileOutputStream(filename);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
int read = 0;
if(null != os){
while(isRecording){
read = audioRecord.read(data, 0, recBufSize);
if(AudioRecord.ERROR_INVALID_OPERATION != read){
try {
os.write(data);
} catch (IOException e) {
e.printStackTrace();
}
}
}
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
// 4、將AudioRecord錄制到的pcm文件轉(zhuǎn)為amr
copyWaveFile(String inFilename,String outFilename);
// 5盐捷、停止錄制
private void stopRecord(){
if(null != audioRecord){
isRecording = false;
audioRecord.stop();
audioRecord.release();
audioRecord = null;
recordingThread = null;
}
copyWaveFile(getTempFilename(),getFilename());
deleteTempFile();
}
這就是一個(gè)AudioRecord的工作流程偶翅,這里由于我們可以想要保存文件,所以第四步的壓縮為amr文件可以省略碉渡,直接操作pcm文件即可聚谁。
-
現(xiàn)在就有兩個(gè)問(wèn)題了:
- 1、如何保存錄制的語(yǔ)音滞诺?
- 2形导、如何替換發(fā)送指定的語(yǔ)音环疼?
首先,第二個(gè)問(wèn)題是建立在第一個(gè)問(wèn)題所保存的語(yǔ)音的基礎(chǔ)上的朵耕。
現(xiàn)在來(lái)看第一個(gè)問(wèn)題炫隶,因?yàn)槭且4嫖⑿潘l(fā)送的語(yǔ)音,所以我們利用Xposed來(lái)hook在微信調(diào)用AudioRecord的后阎曹,在其after回調(diào)里面進(jìn)行相應(yīng)的保存操作即可伪阶。
這里先hook了AudioRecord的read方法。
// hook read 方法处嫌,當(dāng)發(fā)送指定語(yǔ)音時(shí)需要hook這個(gè)函數(shù)需要在before操作望门,當(dāng)錄入自己的語(yǔ)音文件時(shí)需要在after操作
XposedHelpers.findAndHookMethod("android.media.AudioRecord",
loadPackageParam.classLoader, "read", byte[].class, int.class,
int.class, new ReadMethodHook());
然后在它的afterHookedMethod中保存pcm文件到我們指定的目錄下。因?yàn)檫@個(gè)方法是在一個(gè)while循環(huán)中調(diào)用的锰霜,所以我們通過(guò)一個(gè)map來(lái)維護(hù)每個(gè)AudioRecord的FileOutputStream數(shù)據(jù)流筹误。
// 錄入自己的語(yǔ)音文件時(shí)
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
try {
....
FileOutputStream fileOutputStream ;
// 拿到當(dāng)前的AudioRecord對(duì)象
AudioRecord record = (AudioRecord) param.thisObject;
byte[] buffer = (byte[]) param.args[0];
Integer integer = (Integer) param.args[1];
int offset = integer.intValue();
Integer integer2 = (Integer) param.args[2];
int size = integer2.intValue();
// 創(chuàng)建輸出的臨時(shí)文件流
if (mFosMap.get(record) == null) {
execCommand("chmod 777 /data/local/tmp",true);
String pcmFileName = "myPcmFile";
mNum++;
pcmFileName = pcmFileName + mNum;
File file = new File("/data/local/tmp/" + pcmFileName + ".pcm");
File fileParent = file.getParentFile();
if (!fileParent.exists()) {
fileParent.mkdirs();
}
file.createNewFile();
Log.i(TAG, "pcmFileName: " + pcmFileName);
fileOutputStream = new FileOutputStream("/data/local/tmp/" + pcmFileName + ".pcm");
mFosMap.put(record, fileOutputStream);
mPcmFileMap.put(record, pcmFileName);
} else {
fileOutputStream = mFosMap.get(record);
}
// 獲取當(dāng)前AudioRecord的read方法的返回值
int read = (int) param.getResult();
// read方法還在不斷的執(zhí)行中
if (AudioRecord.ERROR_INVALID_OPERATION != read) {
// 新建一個(gè)byte[] ,用于拿到微信的buffer數(shù)據(jù)
byte[] bytes = new byte[read];
// 將微信的buffer數(shù)據(jù)賦予到自己的byte[]中
for (int i = 0; i < bytes.length; i++) {
bytes[i] = buffer[i + offset];
}
// 將byte[] 寫到臨時(shí)的語(yǔ)音文件中,這樣既可拿到當(dāng)前語(yǔ)音輸入的內(nèi)容
fileOutputStream.write(bytes);
}
}
} catch (Exception e) {
Log.i(TAG, "afterHookedMethod Exception: " + e.getMessage());
e.printStackTrace();
}
}
接著就是在Stop方法中做文件的關(guān)閉以及map資源釋放等處理了癣缅。
// hook stop 方法厨剪,當(dāng)發(fā)送指定語(yǔ)音時(shí)需要hook這個(gè)函數(shù)需要在before操作,當(dāng)錄入自己的語(yǔ)音文件時(shí)需要在after操作
XposedHelpers.findAndHookMethod("android.media.AudioRecord",
loadPackageParam.classLoader, "stop",new StopMethodHook() );
private class StopMethodHook extends XC_MethodHook{
// 錄入自己的語(yǔ)音文件
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
try {
getCurrentModeAndPath();
if (mMode == 1) {
AudioRecord record = (AudioRecord) param.thisObject;
if (mFosMap.get(record) != null) {
// 關(guān)閉文件輸出流友存,清理map
FileOutputStream fos = mFosMap.get(record);
fos.close();
mFosMap.remove(record);
// 修改指定輸出文件的父目錄權(quán)限祷膳。
File file = new File(mRecordPcmFileName);
String parentPath = file.getParent();
execCommand("chmod 777 "+parentPath,true);
// 覆蓋拷貝到指定的文件目錄
String pcmFileName = mPcmFileMap.get(record);
execCommand("chmod 777 /data/local/tmp/" + pcmFileName + ".pcm", true);
execCommand("\\cp /data/local/tmp/" + pcmFileName + ".pcm " + mRecordPcmFileName, true);
Log.i(TAG, "命令 : \\cp /data/local/tmp/" + pcmFileName + ".pcm " + mRecordPcmFileName);
execCommand("chmod 777 " + mRecordPcmFileName, true);
// 刪除臨時(shí)文件
execCommand("rm /data/local/tmp/" + pcmFileName + ".pcm", true);
// 清理map
mPcmFileMap.remove(record);
}
}
} catch (Exception e) {
Log.i(TAG, "AudioRecord # stop 里的 afterHookedMethod出錯(cuò) : " + e.getMessage());
}
}
}
到此我們就是實(shí)現(xiàn)了將發(fā)送的微信語(yǔ)音保存到本地的功能了,第一個(gè)問(wèn)題也就解決了屡立。
接著我們看第二個(gè)問(wèn)題直晨,要想替換微信所發(fā)送的語(yǔ)音,就是讓微信的語(yǔ)音發(fā)不出去膨俐,而發(fā)出去的是我們自己的語(yǔ)音勇皇,所以此時(shí)要在hook方法中的before回調(diào)里面執(zhí)行操作了。
我們需要利用Xposed來(lái)hook AudioRecord的startRecording焚刺,read敛摘,getRecordingState,stop和release方法乳愉。
// hook startRecording 方法兄淫,當(dāng)發(fā)送指定語(yǔ)音時(shí)需要hook這個(gè)函數(shù),將微信的流程打斷,自己維護(hù)整個(gè)發(fā)送過(guò)程
XposedHelpers.findAndHookMethod("android.media.AudioRecord",
loadPackageParam.classLoader, "startRecording",new StartRecordingMethodHook());
// hook getRecordingState 方法蔓姚,當(dāng)發(fā)送指定語(yǔ)音時(shí)需要hook這個(gè)函數(shù)
XposedHelpers.findAndHookMethod("android.media.AudioRecord",
loadPackageParam.classLoader, "getRecordingState",new GetRecordingStateMethodHook());
// hook read 方法捕虽,當(dāng)發(fā)送指定語(yǔ)音時(shí)需要hook這個(gè)函數(shù)需要在before操作,當(dāng)錄入自己的語(yǔ)音文件時(shí)需要在after操作
XposedHelpers.findAndHookMethod("android.media.AudioRecord",
loadPackageParam.classLoader, "read", byte[].class, int.class,
int.class, new ReadMethodHook());
// hook stop 方法坡脐,當(dāng)發(fā)送指定語(yǔ)音時(shí)需要hook這個(gè)函數(shù)需要在before操作泄私,當(dāng)錄入自己的語(yǔ)音文件時(shí)需要在after操作
XposedHelpers.findAndHookMethod("android.media.AudioRecord",
loadPackageParam.classLoader, "stop",new StopMethodHook() );
// hook release 方法,當(dāng)發(fā)送指定語(yǔ)音時(shí)需要hook這個(gè)函數(shù),打斷微信
XposedHelpers.findAndHookMethod("android.media.AudioRecord", loadPackageParam.classLoader,
"release", new ReleaseMethodHook());
注意看before回調(diào)的操作挖滤。
// 當(dāng)發(fā)送指定語(yǔ)音時(shí)需要hook這個(gè)函數(shù)需要在before操作崩溪,當(dāng)錄入自己的語(yǔ)音文件時(shí)需要在after操作
private class ReadMethodHook extends XC_MethodHook{
// 發(fā)送指定語(yǔ)音
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
try {
getCurrentModeAndPath();
if (mMode == 0) {
AudioRecord record = (AudioRecord) param.thisObject;
byte[] buffer = (byte[]) param.args[0];
int off = (int) param.args[1];
int size = (int) param.args[2];
FileInputStream fis;
// 指定發(fā)送的語(yǔ)音文件
if (mFisMap.get(record)==null) {
fis = new FileInputStream(mSendPcmFileName);
mFisMap.put(record,fis);
}else {
fis = mFisMap.get(record);
}
// 創(chuàng)建byte[]數(shù)據(jù),用來(lái)替換微信的buffer
int min = Math.min(buffer.length - off, size);
byte[] bytes = new byte[min];
// 將指定的語(yǔ)音文件讀取到微信的語(yǔ)音文件斩松,實(shí)現(xiàn)替換發(fā)送指定語(yǔ)音
int res = fis.read(bytes);
if (res == -1) {
param.setResult(0);
} else {
for (int i = 0; i < bytes.length; i++) {
buffer[off + i] = bytes[i];
}
param.setResult(res);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 錄入自己的語(yǔ)音文件時(shí)
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
try {
getCurrentModeAndPath();
if (mMode == 1) {
FileOutputStream fileOutputStream ;
// 拿到當(dāng)前的AudioRecord對(duì)象
AudioRecord record = (AudioRecord) param.thisObject;
byte[] buffer = (byte[]) param.args[0];
Integer integer = (Integer) param.args[1];
int offset = integer.intValue();
Integer integer2 = (Integer) param.args[2];
int size = integer2.intValue();
// 創(chuàng)建輸出的臨時(shí)文件流
if (mFosMap.get(record) == null) {
execCommand("chmod 777 /data/local/tmp",true);
String pcmFileName = "myPcmFile";
mNum++;
pcmFileName = pcmFileName + mNum;
File file = new File("/data/local/tmp/" + pcmFileName + ".pcm");
File fileParent = file.getParentFile();
if (!fileParent.exists()) {
fileParent.mkdirs();
}
file.createNewFile();
Log.i(TAG, "pcmFileName: " + pcmFileName);
fileOutputStream = new FileOutputStream("/data/local/tmp/" + pcmFileName + ".pcm");
mFosMap.put(record, fileOutputStream);
mPcmFileMap.put(record, pcmFileName);
} else {
fileOutputStream = mFosMap.get(record);
}
// 獲取當(dāng)前AudioRecord的read方法的返回值
int read = (int) param.getResult();
// read方法還在不斷的執(zhí)行中
if (AudioRecord.ERROR_INVALID_OPERATION != read) {
// 新建一個(gè)byte[] 伶唯,用于拿到微信的buffer數(shù)據(jù)
byte[] bytes = new byte[read];
// 將微信的buffer數(shù)據(jù)賦予到自己的byte[]中
for (int i = 0; i < bytes.length; i++) {
bytes[i] = buffer[i + offset];
}
// 將byte[] 寫到臨時(shí)的語(yǔ)音文件中,這樣既可拿到當(dāng)前語(yǔ)音輸入的內(nèi)容
fileOutputStream.write(bytes);
}
}
} catch (Exception e) {
Log.i(TAG, "afterHookedMethod Exception: " + e.getMessage());
e.printStackTrace();
}
}
}
private class StopMethodHook extends XC_MethodHook{
// 發(fā)送指定語(yǔ)音
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
try {
getCurrentModeAndPath();
if (mMode == 0) {
AudioRecord record = (AudioRecord) param.thisObject;
// 關(guān)閉自己的文件輸入,清理map
if (mFisMap.get(record) != null) {
FileInputStream fis = mFisMap.get(record);
fis.close();
mFisMap.remove(record);
}
// 將錄音狀態(tài)設(shè)置為stopped
int flag = -1;
if (mRecordingFlagMap.get(record) == null || mRecordingFlagMap.get(record) != AudioRecord.RECORDSTATE_STOPPED) {
flag = AudioRecord.RECORDSTATE_STOPPED;
mRecordingFlagMap.put(record, flag);
}
// 打斷微信惧盹,完成發(fā)送指定的語(yǔ)音文件
Object o = new Object();
param.setResult(o);
}
} catch (Exception e) {
Log.i(TAG, "AudioRecord # stop 里的 beforeHookedMethod出錯(cuò) : " + e.getMessage());
}
}
// 錄入自己的語(yǔ)音文件
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
try {
getCurrentModeAndPath();
if (mMode == 1) {
AudioRecord record = (AudioRecord) param.thisObject;
if (mFosMap.get(record) != null) {
// 關(guān)閉文件輸出流乳幸,清理map
FileOutputStream fos = mFosMap.get(record);
fos.close();
mFosMap.remove(record);
// 修改指定輸出文件的父目錄權(quán)限。
File file = new File(mRecordPcmFileName);
String parentPath = file.getParent();
execCommand("chmod 777 "+parentPath,true);
// 覆蓋拷貝到指定的文件目錄
String pcmFileName = mPcmFileMap.get(record);
execCommand("chmod 777 /data/local/tmp/" + pcmFileName + ".pcm", true);
execCommand("\\cp /data/local/tmp/" + pcmFileName + ".pcm " + mRecordPcmFileName, true);
Log.i(TAG, "命令 : \\cp /data/local/tmp/" + pcmFileName + ".pcm " + mRecordPcmFileName);
execCommand("chmod 777 " + mRecordPcmFileName, true);
// 刪除臨時(shí)文件
execCommand("rm /data/local/tmp/" + pcmFileName + ".pcm", true);
// 清理map
mPcmFileMap.remove(record);
}
}
} catch (Exception e) {
Log.i(TAG, "AudioRecord # stop 里的 afterHookedMethod出錯(cuò) : " + e.getMessage());
}
}
}
// StartRecordingMethodHook類钧椰,當(dāng)發(fā)送指定語(yǔ)音時(shí)需要hook這個(gè)函數(shù)
private class StartRecordingMethodHook extends XC_MethodHook{
// 將recordingState置為RECORDSTATE_RECORDING粹断,打斷微信的發(fā)送過(guò)程,為了發(fā)送自己指定文件嫡霞。
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
try {
getCurrentModeAndPath();
if (mMode == 0) {
// 修改要發(fā)送的語(yǔ)音文件的權(quán)限
File file = new File(mSendPcmFileName);
String parentName = file.getParent();
Log.i(TAG, "parentName: " + parentName);
// 創(chuàng)建并修改要發(fā)送的語(yǔ)音文件的父目錄
File fileParent = file.getParentFile();
if (!fileParent.exists()) {
fileParent.mkdirs();
}
execCommand("chmod 777 " + parentName, true);
file.createNewFile();
execCommand("chmod 777 " + mSendPcmFileName, true);
AudioRecord record = (AudioRecord) param.thisObject;
int flag = -1;
// 將錄音狀態(tài)置為RECORDSTATE_RECORDING狀態(tài)
if (mRecordingFlagMap.get(record) == null || mRecordingFlagMap.get(record) != AudioRecord.RECORDSTATE_RECORDING) {
flag = AudioRecord.RECORDSTATE_RECORDING;
mRecordingFlagMap.put(record, flag);
}
// 打斷微信的錄音過(guò)程
Object o = new Object();
param.setResult(o);
}
} catch (Exception e) {
Log.i(TAG, "AudioRecord # startRecording beforeHookedMethod 出錯(cuò)");
Log.i(TAG, "出錯(cuò)原因 —— " + e.getMessage());
}
}
}
// getRecordingState瓶埋,獲取我們?cè)趕tartRecording和Stop中維護(hù)的state的值
private class GetRecordingStateMethodHook extends XC_MethodHook{
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
try {
getCurrentModeAndPath();
if (mMode == 0) {
// 獲取我們?cè)趕tartRecording和Stop中維護(hù)的state的值
AudioRecord record = (AudioRecord) param.thisObject;
int res = mRecordingFlagMap.get(record) == null ? AudioRecord.RECORDSTATE_STOPPED : mRecordingFlagMap.get(record);
// 將返回值給微信
param.setResult(res);
// 清理mRecordFlagMap
mRecordingFlagMap.remove(record);
}
} catch (Exception e) {
Log.i(TAG, "AudioRecord # getRecordingState beforeHookedMethod 出錯(cuò): " + e.getMessage());
}
}
}
// release方法,打斷微信的
private class ReleaseMethodHook extends XC_MethodHook{
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
try {
getCurrentModeAndPath();
if (mMode == 0) {
Log.i(TAG, "AudioRecord # release beforeHookedMethod ");
Object o = new Object();
param.setResult(o);
}
}catch (Exception e){
Log.i(TAG, "AudioRecord # release beforeHookedMethod 出錯(cuò): " + e.getMessage());
}
}
}
這里就是自己維護(hù)整個(gè)AudioRecord的操作流程诊沪,主要的操作就是hook了read方法里面的before回調(diào)养筒,利用FileInputStream將數(shù)據(jù)寫入到byte[ ]中,然后將這個(gè)byte數(shù)組讀到微信中端姚,接著通過(guò)setResult就是修改了微信的語(yǔ)音數(shù)據(jù)
// 創(chuàng)建byte[]數(shù)據(jù)晕粪,用來(lái)替換微信的buffer
int min = Math.min(buffer.length - off, size);
byte[] bytes = new byte[min];
// 將指定的語(yǔ)音文件讀取到微信的語(yǔ)音文件,實(shí)現(xiàn)替換發(fā)送指定語(yǔ)音
int res = fis.read(bytes);
if (res == -1) {
param.setResult(0);
} else {
for (int i = 0; i < bytes.length; i++) {
buffer[off + i] = bytes[i];
}
param.setResult(res);
}
最后在其他的方法的before回調(diào)中維護(hù)好這個(gè)過(guò)程即可渐裸,到這里我們就解決了第二個(gè)問(wèn)題了巫湘。
當(dāng)然,因?yàn)殇浿聘l(fā)送是兩個(gè)不同的操作昏鹃,所以這里通過(guò)設(shè)置一個(gè)mode來(lái)維護(hù)切換操作尚氛,再書寫接入文檔,說(shuō)明這個(gè)模塊的使用方法盆顾。
這個(gè)項(xiàng)目默認(rèn)可以錄制跟替換5條語(yǔ)音數(shù)據(jù)怠褐,你也可以修改實(shí)現(xiàn)你想要的數(shù)目等畏梆。
最后您宪,附上github地址
https://github.com/carrys17/HookWxYYDemo
嚴(yán)重說(shuō)明:本文的目的只有一個(gè)就是學(xué)習(xí)逆向分析技巧,如果有人利用本文技術(shù)進(jìn)行非法操作帶來(lái)的后果都是操作者自己承擔(dān)奠涌,和本文以及本文作者沒(méi)有任何關(guān)系
2018/6/25補(bǔ)充
今天發(fā)現(xiàn)在模擬器上發(fā)送指定語(yǔ)音失敗宪巨,經(jīng)過(guò)測(cè)試后發(fā)現(xiàn)需要hook AudioRecord的構(gòu)造函數(shù),該構(gòu)造函數(shù)有5個(gè)參數(shù)溜畅,其第五個(gè)參數(shù)bufferSizeInBytes的值在模擬器和真機(jī)上有區(qū)別捏卓,所以將模擬器的值hook后修改為真機(jī)上的值即可。代碼的修改即在Module里面增加對(duì)構(gòu)造函數(shù)的hook
// --- 構(gòu)造方法int audioSource, int sampleRateInHz, int channelConfig, int audioFormat,
// int bufferSizeInBytes
XposedHelpers.findAndHookConstructor("android.media.AudioRecord", loadPackageParam.classLoader,
int.class, int.class, int.class, int.class, int.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
Log.i(TAG, "AudioRecord # 構(gòu)造方法beforeHookedMethod: ");
// 修改
param.args[0] = 1;
param.args[1] = 16000;
param.args[2] = 2;
param.args[3] = 2;
param.args[4] = 12800;
}
});