最終目標:
1、保存發(fā)送的微信語音
2、發(fā)送指定的微信語音
思路:
因為要hook的是微信語音功能,所以首先要知道微信語音功能的流程。
查閱資料以及利用Xposed的hook功能,最終知道微信的語音功能是通過AudioRecord來實現的,一般的流程是這樣的:
// 1、初始化audioRecord 對象
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, frequency, channelConfiguration, EncodingBitRate, recBufSize);
// 2、調用audioRecord的start方法
audioRecord.startRecording();
// 3、讀取audioRecord的錄音數據,這些操作跟文件的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文件轉為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();
}
這就是一個AudioRecord的工作流程,這里由于我們可以想要保存文件,所以第四步的壓縮為amr文件可以省略,直接操作pcm文件即可。
-
現在就有兩個問題了:
- 1、如何保存錄制的語音?
- 2、如何替換發(fā)送指定的語音?
首先,第二個問題是建立在第一個問題所保存的語音的基礎上的。
現在來看第一個問題,因為是要保存微信所發(fā)送的語音,所以我們利用Xposed來hook在微信調用AudioRecord的后,在其after回調里面進行相應的保存操作即可。
這里先hook了AudioRecord的read方法。
// hook read 方法,當發(fā)送指定語音時需要hook這個函數需要在before操作,當錄入自己的語音文件時需要在after操作
XposedHelpers.findAndHookMethod("android.media.AudioRecord",
loadPackageParam.classLoader, "read", byte[].class, int.class,
int.class, new ReadMethodHook());
然后在它的afterHookedMethod中保存pcm文件到我們指定的目錄下。因為這個方法是在一個while循環(huán)中調用的,所以我們通過一個map來維護每個AudioRecord的FileOutputStream數據流。
// 錄入自己的語音文件時
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
try {
....
FileOutputStream fileOutputStream ;
// 拿到當前的AudioRecord對象
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)建輸出的臨時文件流
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);
}
// 獲取當前AudioRecord的read方法的返回值
int read = (int) param.getResult();
// read方法還在不斷的執(zhí)行中
if (AudioRecord.ERROR_INVALID_OPERATION != read) {
// 新建一個byte[] ,用于拿到微信的buffer數據
byte[] bytes = new byte[read];
// 將微信的buffer數據賦予到自己的byte[]中
for (int i = 0; i < bytes.length; i++) {
bytes[i] = buffer[i + offset];
}
// 將byte[] 寫到臨時的語音文件中,這樣既可拿到當前語音輸入的內容
fileOutputStream.write(bytes);
}
}
} catch (Exception e) {
Log.i(TAG, "afterHookedMethod Exception: " + e.getMessage());
e.printStackTrace();
}
}
接著就是在Stop方法中做文件的關閉以及map資源釋放等處理了。
// hook stop 方法,當發(fā)送指定語音時需要hook這個函數需要在before操作,當錄入自己的語音文件時需要在after操作
XposedHelpers.findAndHookMethod("android.media.AudioRecord",
loadPackageParam.classLoader, "stop",new StopMethodHook() );
private class StopMethodHook extends XC_MethodHook{
// 錄入自己的語音文件
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
try {
getCurrentModeAndPath();
if (mMode == 1) {
AudioRecord record = (AudioRecord) param.thisObject;
if (mFosMap.get(record) != null) {
// 關閉文件輸出流,清理map
FileOutputStream fos = mFosMap.get(record);
fos.close();
mFosMap.remove(record);
// 修改指定輸出文件的父目錄權限。
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);
// 刪除臨時文件
execCommand("rm /data/local/tmp/" + pcmFileName + ".pcm", true);
// 清理map
mPcmFileMap.remove(record);
}
}
} catch (Exception e) {
Log.i(TAG, "AudioRecord # stop 里的 afterHookedMethod出錯 : " + e.getMessage());
}
}
}
到此我們就是實現了將發(fā)送的微信語音保存到本地的功能了,第一個問題也就解決了。
接著我們看第二個問題,要想替換微信所發(fā)送的語音,就是讓微信的語音發(fā)不出去,而發(fā)出去的是我們自己的語音,所以此時要在hook方法中的before回調里面執(zhí)行操作了。
我們需要利用Xposed來hook AudioRecord的startRecording,read,getRecordingState,stop和release方法。
// hook startRecording 方法,當發(fā)送指定語音時需要hook這個函數,將微信的流程打斷,自己維護整個發(fā)送過程
XposedHelpers.findAndHookMethod("android.media.AudioRecord",
loadPackageParam.classLoader, "startRecording",new StartRecordingMethodHook());
// hook getRecordingState 方法,當發(fā)送指定語音時需要hook這個函數
XposedHelpers.findAndHookMethod("android.media.AudioRecord",
loadPackageParam.classLoader, "getRecordingState",new GetRecordingStateMethodHook());
// hook read 方法,當發(fā)送指定語音時需要hook這個函數需要在before操作,當錄入自己的語音文件時需要在after操作
XposedHelpers.findAndHookMethod("android.media.AudioRecord",
loadPackageParam.classLoader, "read", byte[].class, int.class,
int.class, new ReadMethodHook());
// hook stop 方法,當發(fā)送指定語音時需要hook這個函數需要在before操作,當錄入自己的語音文件時需要在after操作
XposedHelpers.findAndHookMethod("android.media.AudioRecord",
loadPackageParam.classLoader, "stop",new StopMethodHook() );
// hook release 方法,當發(fā)送指定語音時需要hook這個函數,打斷微信
XposedHelpers.findAndHookMethod("android.media.AudioRecord", loadPackageParam.classLoader,
"release", new ReleaseMethodHook());
注意看before回調的操作。
// 當發(fā)送指定語音時需要hook這個函數需要在before操作,當錄入自己的語音文件時需要在after操作
private class ReadMethodHook extends XC_MethodHook{
// 發(fā)送指定語音
@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ā)送的語音文件
if (mFisMap.get(record)==null) {
fis = new FileInputStream(mSendPcmFileName);
mFisMap.put(record,fis);
}else {
fis = mFisMap.get(record);
}
// 創(chuàng)建byte[]數據,用來替換微信的buffer
int min = Math.min(buffer.length - off, size);
byte[] bytes = new byte[min];
// 將指定的語音文件讀取到微信的語音文件,實現替換發(fā)送指定語音
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();
}
}
// 錄入自己的語音文件時
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
try {
getCurrentModeAndPath();
if (mMode == 1) {
FileOutputStream fileOutputStream ;
// 拿到當前的AudioRecord對象
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)建輸出的臨時文件流
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);
}
// 獲取當前AudioRecord的read方法的返回值
int read = (int) param.getResult();
// read方法還在不斷的執(zhí)行中
if (AudioRecord.ERROR_INVALID_OPERATION != read) {
// 新建一個byte[] ,用于拿到微信的buffer數據
byte[] bytes = new byte[read];
// 將微信的buffer數據賦予到自己的byte[]中
for (int i = 0; i < bytes.length; i++) {
bytes[i] = buffer[i + offset];
}
// 將byte[] 寫到臨時的語音文件中,這樣既可拿到當前語音輸入的內容
fileOutputStream.write(bytes);
}
}
} catch (Exception e) {
Log.i(TAG, "afterHookedMethod Exception: " + e.getMessage());
e.printStackTrace();
}
}
}
private class StopMethodHook extends XC_MethodHook{
// 發(fā)送指定語音
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
try {
getCurrentModeAndPath();
if (mMode == 0) {
AudioRecord record = (AudioRecord) param.thisObject;
// 關閉自己的文件輸入,清理map
if (mFisMap.get(record) != null) {
FileInputStream fis = mFisMap.get(record);
fis.close();
mFisMap.remove(record);
}
// 將錄音狀態(tài)設置為stopped
int flag = -1;
if (mRecordingFlagMap.get(record) == null || mRecordingFlagMap.get(record) != AudioRecord.RECORDSTATE_STOPPED) {
flag = AudioRecord.RECORDSTATE_STOPPED;
mRecordingFlagMap.put(record, flag);
}
// 打斷微信,完成發(fā)送指定的語音文件
Object o = new Object();
param.setResult(o);
}
} catch (Exception e) {
Log.i(TAG, "AudioRecord # stop 里的 beforeHookedMethod出錯 : " + e.getMessage());
}
}
// 錄入自己的語音文件
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
try {
getCurrentModeAndPath();
if (mMode == 1) {
AudioRecord record = (AudioRecord) param.thisObject;
if (mFosMap.get(record) != null) {
// 關閉文件輸出流,清理map
FileOutputStream fos = mFosMap.get(record);
fos.close();
mFosMap.remove(record);
// 修改指定輸出文件的父目錄權限。
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);
// 刪除臨時文件
execCommand("rm /data/local/tmp/" + pcmFileName + ".pcm", true);
// 清理map
mPcmFileMap.remove(record);
}
}
} catch (Exception e) {
Log.i(TAG, "AudioRecord # stop 里的 afterHookedMethod出錯 : " + e.getMessage());
}
}
}
// StartRecordingMethodHook類,當發(fā)送指定語音時需要hook這個函數
private class StartRecordingMethodHook extends XC_MethodHook{
// 將recordingState置為RECORDSTATE_RECORDING,打斷微信的發(fā)送過程,為了發(fā)送自己指定文件。
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
try {
getCurrentModeAndPath();
if (mMode == 0) {
// 修改要發(fā)送的語音文件的權限
File file = new File(mSendPcmFileName);
String parentName = file.getParent();
Log.i(TAG, "parentName: " + parentName);
// 創(chuàng)建并修改要發(fā)送的語音文件的父目錄
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);
}
// 打斷微信的錄音過程
Object o = new Object();
param.setResult(o);
}
} catch (Exception e) {
Log.i(TAG, "AudioRecord # startRecording beforeHookedMethod 出錯");
Log.i(TAG, "出錯原因 —— " + e.getMessage());
}
}
}
// getRecordingState,獲取我們在startRecording和Stop中維護的state的值
private class GetRecordingStateMethodHook extends XC_MethodHook{
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
try {
getCurrentModeAndPath();
if (mMode == 0) {
// 獲取我們在startRecording和Stop中維護的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 出錯: " + 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 出錯: " + e.getMessage());
}
}
}
這里就是自己維護整個AudioRecord的操作流程,主要的操作就是hook了read方法里面的before回調,利用FileInputStream將數據寫入到byte[ ]中,然后將這個byte數組讀到微信中,接著通過setResult就是修改了微信的語音數據
// 創(chuàng)建byte[]數據,用來替換微信的buffer
int min = Math.min(buffer.length - off, size);
byte[] bytes = new byte[min];
// 將指定的語音文件讀取到微信的語音文件,實現替換發(fā)送指定語音
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回調中維護好這個過程即可,到這里我們就解決了第二個問題了。
當然,因為錄制跟發(fā)送是兩個不同的操作,所以這里通過設置一個mode來維護切換操作,再書寫接入文檔,說明這個模塊的使用方法。
這個項目默認可以錄制跟替換5條語音數據,你也可以修改實現你想要的數目等。
最后,附上github地址
https://github.com/carrys17/HookWxYYDemo
嚴重說明:本文的目的只有一個就是學習逆向分析技巧,如果有人利用本文技術進行非法操作帶來的后果都是操作者自己承擔,和本文以及本文作者沒有任何關系
2018/6/25補充
今天發(fā)現在模擬器上發(fā)送指定語音失敗,經過測試后發(fā)現需要hook AudioRecord的構造函數,該構造函數有5個參數,其第五個參數bufferSizeInBytes的值在模擬器和真機上有區(qū)別,所以將模擬器的值hook后修改為真機上的值即可。代碼的修改即在Module里面增加對構造函數的hook
// --- 構造方法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 # 構造方法beforeHookedMethod: ");
// 修改
param.args[0] = 1;
param.args[1] = 16000;
param.args[2] = 2;
param.args[3] = 2;
param.args[4] = 12800;
}
});