最终目标:
1、保存发送的微信语音
2、发送指定的微信语音
思路:
因为要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、如何替换发送指定的语音?
首先,第二个问题是建立在第一个问题所保存的语音的基础上的。
现在来看第一个问题,因为是要保存微信所发送的语音,所以我们利用Xposed来hook在微信调用AudioRecord的后,在其after回调里面进行相应的保存操作即可。
这里先hook了AudioRecord的read方法。
// hook read 方法,当发送指定语音时需要hook这个函数需要在before操作,当录入自己的语音文件时需要在after操作
XposedHelpers.findAndHookMethod("android.media.AudioRecord",
loadPackageParam.classLoader, "read", byte[].class, int.class,
int.class, new ReadMethodHook());
然后在它的afterHookedMethod中保存pcm文件到我们指定的目录下。因为这个方法是在一个while循环中调用的,所以我们通过一个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();
// 创建输出的临时文件流
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方法还在不断的执行中
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 方法,当发送指定语音时需要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());
}
}
}
到此我们就是实现了将发送的微信语音保存到本地的功能了,第一个问题也就解决了。
接着我们看第二个问题,要想替换微信所发送的语音,就是让微信的语音发不出去,而发出去的是我们自己的语音,所以此时要在hook方法中的before回调里面执行操作了。
我们需要利用Xposed来hook AudioRecord的startRecording,read,getRecordingState,stop和release方法。
// hook startRecording 方法,当发送指定语音时需要hook这个函数,将微信的流程打断,自己维护整个发送过程
XposedHelpers.findAndHookMethod("android.media.AudioRecord",
loadPackageParam.classLoader, "startRecording",new StartRecordingMethodHook());
// hook getRecordingState 方法,当发送指定语音时需要hook这个函数
XposedHelpers.findAndHookMethod("android.media.AudioRecord",
loadPackageParam.classLoader, "getRecordingState",new GetRecordingStateMethodHook());
// hook read 方法,当发送指定语音时需要hook这个函数需要在before操作,当录入自己的语音文件时需要在after操作
XposedHelpers.findAndHookMethod("android.media.AudioRecord",
loadPackageParam.classLoader, "read", byte[].class, int.class,
int.class, new ReadMethodHook());
// hook stop 方法,当发送指定语音时需要hook这个函数需要在before操作,当录入自己的语音文件时需要在after操作
XposedHelpers.findAndHookMethod("android.media.AudioRecord",
loadPackageParam.classLoader, "stop",new StopMethodHook() );
// hook release 方法,当发送指定语音时需要hook这个函数,打断微信
XposedHelpers.findAndHookMethod("android.media.AudioRecord", loadPackageParam.classLoader,
"release", new ReleaseMethodHook());
注意看before回调的操作。
// 当发送指定语音时需要hook这个函数需要在before操作,当录入自己的语音文件时需要在after操作
private class ReadMethodHook extends XC_MethodHook{
// 发送指定语音
@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;
// 指定发送的语音文件
if (mFisMap.get(record)==null) {
fis = new FileInputStream(mSendPcmFileName);
mFisMap.put(record,fis);
}else {
fis = mFisMap.get(record);
}
// 创建byte[]数据,用来替换微信的buffer
int min = Math.min(buffer.length - off, size);
byte[] bytes = new byte[min];
// 将指定的语音文件读取到微信的语音文件,实现替换发送指定语音
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();
// 创建输出的临时文件流
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方法还在不断的执行中
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{
// 发送指定语音
@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);
}
// 将录音状态设置为stopped
int flag = -1;
if (mRecordingFlagMap.get(record) == null || mRecordingFlagMap.get(record) != AudioRecord.RECORDSTATE_STOPPED) {
flag = AudioRecord.RECORDSTATE_STOPPED;
mRecordingFlagMap.put(record, flag);
}
// 打断微信,完成发送指定的语音文件
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类,当发送指定语音时需要hook这个函数
private class StartRecordingMethodHook extends XC_MethodHook{
// 将recordingState置为RECORDSTATE_RECORDING,打断微信的发送过程,为了发送自己指定文件。
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
try {
getCurrentModeAndPath();
if (mMode == 0) {
// 修改要发送的语音文件的权限
File file = new File(mSendPcmFileName);
String parentName = file.getParent();
Log.i(TAG, "parentName: " + parentName);
// 创建并修改要发送的语音文件的父目录
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;
// 将录音状态置为RECORDSTATE_RECORDING状态
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就是修改了微信的语音数据
// 创建byte[]数据,用来替换微信的buffer
int min = Math.min(buffer.length - off, size);
byte[] bytes = new byte[min];
// 将指定的语音文件读取到微信的语音文件,实现替换发送指定语音
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回调中维护好这个过程即可,到这里我们就解决了第二个问题了。
当然,因为录制跟发送是两个不同的操作,所以这里通过设置一个mode来维护切换操作,再书写接入文档,说明这个模块的使用方法。
这个项目默认可以录制跟替换5条语音数据,你也可以修改实现你想要的数目等。
最后,附上github地址
https://github.com/carrys17/HookWxYYDemo
严重说明:本文的目的只有一个就是学习逆向分析技巧,如果有人利用本文技术进行非法操作带来的后果都是操作者自己承担,和本文以及本文作者没有任何关系
2018/6/25补充
今天发现在模拟器上发送指定语音失败,经过测试后发现需要hook AudioRecord的构造函数,该构造函数有5个参数,其第五个参数bufferSizeInBytes的值在模拟器和真机上有区别,所以将模拟器的值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;
}
});