这是我的第一篇博客,问题可能会有很多,希望大家多多指教
先给大家看效果图,这个程序,用作测试没问题,大家可以试一下
运行主界面,点击菜单
菜单界面,点击开始抓包,然后切换出去,用浏览器上网,再切换回来
上图是抓到的包,显示在界面上
主要思路
1 使用库libpcap,写个简单的c程序main.c,实现抓包,在控制台输出所抓取包的内容,可以先在linux用gcc编译,以root权限运行一次,确保程序正确
2 用ndk编译main.c,得到可执行文件pcap(名字自己取的),放到/system/bin下,修改权限,用手机连接电脑,用adb shell以root权限 试一下,确保程序正确
3 在Android程序中以root权限执行pcap,并获得输出的数据
4 将输出的数据解析成Packet(jpcap中的一个类)类的对象
5 然后在java中,你就可以做你想做的了
具体实现
以前看了一些关于libpcap移植到Android上的文章,自己动手做了一两次,由于权限问题,一直获取不到网卡设备,本人对于Android权限不太懂,没有继续尝试了
最近才将自己的手机root,发现手机里面的/system/xbin/里有tcpdump,就用tcpdum 试了下抓包,以下是抓取包的数据截图
从上图,我们可以看到,这些抓取出来的数据,不知道他的格式是什么(去网上搜了比较久,还是不知道),我决定去看了一下tcpdump的输出部分的源码,自己c语言太差了,看不下去,所以这个数据格式,就暂时没法知道了。后面想到既然tcpdump可以抓到包,为什么自己不能写一个简单一点的抓包程序,输出的数据格式,自己可以灵活处理。
然后在网上搜了一些关于Android ndk编译C语言为可执行二进制的文件教程,最后写了一个使用了libpcap简单的c语言抓包程序文件名为main.c,如果你了解一点libpcap这个就很简单了,
#include <stdio.h>
#include <pcap.h>
#include <stdlib.h>
#include <string.h>
void myCallback(u_char * uchar, const struct pcap_pkthdr * packet,const u_char * data)
{
bpf_u_int32 length= packet->caplen;
bpf_u_int32 plen= packet->len;
struct timeval time= packet->ts;
printf("the cap len: %d,the pcaket len:%d\n ",length,plen);
printf("the timeinfo:tv_sec:%ld, tv_usec:%ld\n",time.tv_sec,time.tv_usec);
int i;
for(i=0;i<packet->len;i++)
printf("%02x",data[i]);
printf("\n");
}
int main()
{
char error_buffer[100];
char *result= pcap_lookupdev(error_buffer);
char error_buffer2[100];
pcap_t* pPcap_t= pcap_open_live(result,BUFSIZ,0,-1,error_buffer2);
unsigned char error_buffer1[100];
pcap_loop(pPcap_t,-1,myCallback,error_buffer1);
return 0;
}
将这个文件和Android系统源代码中的libpcap的源代码文件放在一起,将其中的Android.mk文件修改一下
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_SRC_FILES:=\
bpf_dump.c\
bpf/net/bpf_filter.c\
bpf_image.c\
etherent.c\
fad-gifc.c\
gencode.c\
grammar.c\
inet.c\
nametoaddr.c\
optimize.c\
pcap.c\
pcap-linux.c\
savefile.c\
scanner.c\
version.c<span style="color:#FF6666;">\
main.c</span>
LOCAL_CFLAGS:=-O2 -g
LOCAL_CFLAGS+=-DHAVE_CONFIG_H -D_U_="__attribute__((unused))" -Dlinux -D__GLIBC__ -D_GNU_SOURCE
LOCAL_MODULE:= pcap
<span style="color:#FF0000;">
include $(BUILD_EXECUTABLE)</span>
红色部分是修改的地方,作用是将自己的main.c文件加进去,将编译出来的文件为可执行。如果编译没有报错的话,最后编译得到了一个pcap可执行文件,将这个文件放到Android system/bin的目录下,修改权限,
我的做法是,将这个文件先放到手机的sdcard的根目录,然后用一款叫做Root Explorer的文件管理器,将这个文件复制到/system/bin目录下,
修改它的权限
将手机连上电脑,然后可以用adb shell试下我们写的程序是否可以抓包
这是程序运行的结果,对于以上的输出结果,就是下面这段代码输出来的,数据分为3行,每一行的数据我们都知道它属于数据包的那一部分,
void myCallback(u_char * uchar, const struct pcap_pkthdr * packet,const u_char * data)
{
bpf_u_int32 length= packet->caplen;
bpf_u_int32 plen= packet->len;
struct timeval time= packet->ts;
<span style="color:#FF0000;"> printf("the cap len: %d,the pcaket len:%d\n ",length,plen);
printf("the timeinfo:tv_sec:%ld, tv_usec:%ld\n",time.tv_sec,time.tv_usec);
int i;
for(i=0;i<packet->len;i++)
printf("%02x",data[i]);
printf("\n");</span>
}
现在我们已经知道了数据的输出的格式了,这些数据输出在控制台,我们接下来要解决如何在Android程序中以root权限执行这个pcap并且获得其输出的数据
我的解决思路是,采用了一个叫做RootTools的Android工具库,用它来执行pcap,并且获得其输出在控制台的数据。下图是Android中的log输出
上图的输出,跟在电脑上用adb shell 执行pcap在控制台的输出数据是一致的,我们已经在Android应用中获得了pcap在控制台的输出数据,
接下来我们就是要将这些数据解析成Java对象了。jpcap,jnetpcap中都应该含有解析byte数据,转为Java对象的代码,阅读了jpcap,jnetpcap部分源代码,最终采用jpcap中的部分源代码(这部分纯java)来实现byte数据的解析,(其中有过自己去写解析的函数的想法,感觉不可能写出来,只有继续去看源代码),数据解析这一关就攻克了。下面是Android程序中的主要代码
package com.example.jpcapforandroid;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import net.sourceforge.jpcap.net.LinkLayer;
import net.sourceforge.jpcap.net.Packet;
import net.sourceforge.jpcap.net.PacketFactory;
import com.stericson.RootTools.RootTools;
import com.stericson.RootTools.execution.Command;
import com.stericson.RootTools.execution.Shell;
import com.tqd.utils.IPSeeker;
import android.app.Activity;
import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.Typeface;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.UserHandle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends Activity {
public static final String DIR="ipdatabase";
public static final String FILE_NAME="qqwry.dat";
private static final String TAG = "MainActivity";
UserHandle userHandle=android.os.Process.myUserHandle();
ListView mListViewPacket;
PacketListAdapter pla;
public static int tip=0;
public IPSeeker mIPSeeker;
public static long mPacketCount=0;
public TextView mTVPacketcount;
/**
* 将ip数据库文件数据流写入到sdcard指定文件
* @param is
*/
public boolean writeToSDCard(InputStream is)
{
String root= Environment.getExternalStorageDirectory().getAbsolutePath();
File dir=new File(root+File.separator+DIR);
Log.i(TAG, "dir path: "+dir.getAbsolutePath());
//不存在目录,则创建目录
if(!dir.exists())
dir.mkdir();
Log.i(TAG, "file path: "+dir.getAbsolutePath()+File.separator+FILE_NAME);
File file=new File(dir.getAbsolutePath()+File.separator+FILE_NAME);
//不存在文件,则创建文件
if(!file.exists())
try {
file.createNewFile();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
FileOutputStream fos = null;
try {
fos = new FileOutputStream(file);
} catch (FileNotFoundException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
int temp=-1;
byte[] buffer=new byte[1024];
try {
while((temp=is.read(buffer))!=-1)
{
fos.write(buffer, 0, temp);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
try {
fos.flush();
fos.close();
is.close();
return true;
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return false;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
loadIpDatabase();
getWindow().requestFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
mTVPacketcount=(TextView) findViewById(R.id.packet_count);
//加载,设置字体
Typeface tf=Typeface.createFromAsset(getAssets(), "hkww.ttf");
mTVPacketcount.setTypeface(tf);
mTVPacketcount.setText("已抓取"+mPacketCount+"个packet");
mListViewPacket=(ListView) this.findViewById(R.id.packet_list);
pla=new PacketListAdapter(this);
mListViewPacket.setAdapter(pla);
//Log.i("userHandle", userHandle.toString());
RootTools rt=new RootTools();
RootTools.default_Command_Timeout=1000*1000; //设置执行命令超时的值,pcap命令是个死循环,这个设置大一点好些,
RootTools.debugMode=true;
mListViewPacket.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
// TODO Auto-generated method stub
Packet packet=(Packet) pla.getItem(position);
Toast.makeText(MainActivity.this, packet.toColoredString(false), Toast.LENGTH_LONG).show();
}
});
}
Handler mHandler=new Handler()
{
public void handleMessage(android.os.Message msg) {
if(msg.what==0 && msg.obj instanceof Packet)
{
Packet packet=(Packet) msg.obj;
pla.addPacket(packet);
mPacketCount++;
mTVPacketcount.setText("已抓取"+mPacketCount+"个packet");
}
if(msg.what==1 && msg.obj instanceof String)
{
Toast.makeText(MainActivity.this, msg.obj.toString(), Toast.LENGTH_LONG).show();
mIPSeeker=new IPSeeker(FILE_NAME, Environment.getExternalStorageDirectory()+File.separator+DIR);
pla.setIPSeeker(mIPSeeker);
}
if(msg.what==2 && msg.obj instanceof String)
{
Toast.makeText(MainActivity.this, msg.obj.toString(), Toast.LENGTH_LONG).show();
}
};
};
/**
* 加载本地ip数据库
*/
private void loadIpDatabase()
{
new Thread()
{
@Override
public void run() {
boolean isSuccess=true;
// TODO Auto-generated method stub
Log.i(TAG,"ip数据库默认路径"+Environment.getExternalStorageDirectory()+DIR+File.separator+FILE_NAME);
//查看sdcard指定目录中的本地ip数据库是否存在
File file=new File(Environment.getExternalStorageDirectory()+DIR+File.separator+FILE_NAME);
Log.i(TAG, Environment.getExternalStorageDirectory()+DIR+File.separator+FILE_NAME+file.getAbsolutePath());
//不存在就从asset中复制到sdcard指定目录中
if(!file.exists())
{
Log.i(TAG, "不存在ip数据库,从asset复制到sdcard中");
AssetManager am=MainActivity.this.getAssets();
InputStream is = null;
try {
is= am.open("qqwry.dat");
} catch (IOException e) {
// TODO Auto-generated catch block
Log.i(TAG, "从asset读取ip数据库失败");
isSuccess=false;
e.printStackTrace();
}
if(is!=null)
isSuccess= writeToSDCard(is);
else
Log.i(TAG, "从asset复制ip数据库到sdcard中失败");
}
//将事件发送到ui线程
if(isSuccess)
mHandler.obtainMessage(1, "本地ip数据库加载完毕").sendToTarget();
else
mHandler.obtainMessage(2, "本地ip数据库加载失败").sendToTarget();
}
}.start();
}
/**
* 启动抓包
*/
public void start()
{
//用RootTools帮我们查询到pcap的路径,其实我们可以自己指定为/system/bin/pcap的,
RootTools.findBinary("pcap");
//MyRunner是我自己写的一个继承于Runner的类,用来执行命令的一个线程类
//本来使用这个的RootTools.runBinary(context, binaryName, parameter);但是不知道命令执行的输出数据,如何获取,
MyRunner mr=new MyRunner(this, RootTools.lastFoundBinaryPaths.get(0)+"pcap", "");
mr.start();
}
/**
* @author Administrator
* 这个类包含了命令执行时,数据输出的回调函数 commandOutput 我们主要在这个函数里做我们想要的事
*
*/
class MyCommandCapture extends Command
{
private StringBuilder sb = new StringBuilder();
public MyCommandCapture(int id, String... command) {
super(id, command);
}
public MyCommandCapture(int id, boolean handlerEnabled, String... command) {
super(id, handlerEnabled, command);
}
public MyCommandCapture(int id, int timeout, String... command) {
super(id, timeout, command);
}
/* (non-Javadoc)
* @see com.stericson.RootTools.execution.Command#commandOutput(int, java.lang.String)
*/
@Override
public void commandOutput(int id, String line) {
sb.append(line).append('\n');
tip++;
/*
* 在main.c中,我们每抓到一个包,数据输出三行,
*
* printf("the cap len: %d,the pcaket len:%d\n ",length,plen);
printf("the timeinfo:tv_sec:%ld, tv_usec:%ld\n",time.tv_sec,time.tv_usec);
int i;
for(i=0;i<packet->len;i++)
printf("%02x",data[i]);
printf("\n");
*
* 第一,二行是 const struct pcap_pkthdr
* 第三行 是包的数据 byte[]的String形式
*/
switch(tip)
{
case 1:
RootTools.log("Command", "ID: " + 1 + ", " + line);
break;
case 2:
RootTools.log("Command", "ID: " + 2 + ", " + line);
break;
case 3:
RootTools.log("Command", "ID: " + 3 + ", " + line);
Log.i("data length", ""+line.length());
byte []data=hexStringToBytes(line);
//将byte数组,解析成具体的包,第一个参数是数据链路层类型,我的手机是:wifi下是LinkLayer.EN10MB 移动的2G网是LinkLayer.LINUX_SLL
//我们可以在main.c里面获得这个具体的值,int pcap_datalink(pcap_t *p) 返回,例如DLT_EN10MB
//这里写死了,
//数据包的解析用的是jpcap的库,我这里只是用了一部分java代码,没有涉及native层的,纯java,
Packet packet= PacketFactory.dataToPacket(LinkLayer.EN10MB, data) ;
// ByteBuffer bb=ByteBuffer.wrap(data);
mHandler.obtainMessage(0, packet).sendToTarget();
tip=0;
break;
default: break;
}
}
public String bytesToHexString(byte[] src){
StringBuilder stringBuilder = new StringBuilder("");
if (src == null || src.length <= 0) {
return null;
}
for (int i = 0; i < src.length; i++) {
int v = src[i] & 0xFF;
String hv = Integer.toHexString(v);
if (hv.length() < 2) {
stringBuilder.append(0);
}
stringBuilder.append(hv);
}
return stringBuilder.toString();
}
/**
* 十六进制的字符串转化为byte[]
* @param hexString
* @return
*/
public byte[] hexStringToBytes(String hexString) {
if (hexString == null || hexString.equals("")) {
return null;
}
hexString = hexString.toUpperCase();
int length = hexString.length() / 2;
char[] hexChars = hexString.toCharArray();
byte[] d = new byte[length];
for (int i = 0; i < length; i++) {
int pos = i * 2;
d[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1]));
}
return d;
}
/**
* Convert char to byte
* @param c char
* @return byte
*/
private byte charToByte(char c) {
return (byte) "0123456789ABCDEF".indexOf(c);
}
@Override
public void commandTerminated(int id, String reason) {
//pass
}
@Override
public void commandCompleted(int id, int exitcode) {
//pass
}
@Override
public String toString() {
return sb.toString();
}
}
class MyRunner extends Thread
{
private static final String LOG_TAG = "RootTools::Runner";
Context context;
String binaryName;
String parameter;
/**
* @param context 这个参数,
* @param binaryName 可执行二进制文件的路径
* @param parameter 命令的参数 例如 ls -l 这个parameter就是指后面的-l
*/
public MyRunner(Context context, String binaryName, String parameter) {
this.context = context;
this.binaryName = binaryName;
this.parameter = parameter;
}
public void run() {
try {
//这个类里面包含了执行命令输出的数据的回调函数,
MyCommandCapture command = new MyCommandCapture(0, false, binaryName + " " + parameter);
//命令执行,参数是个超时的值,不太懂,设置的尽量大,不小了,找不到几个包,就结束了
Shell.startRootShell(10000*10000).add(command);
// Shell.startRootShell().add(command);
commandWait(command);
} catch (Exception e) {}
}
private void commandWait(Command cmd) {
synchronized (cmd) {
try {
if (!cmd.isFinished()) {
cmd.wait(2000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// TODO Auto-generated method stub
menu.add("开始抓包");
menu.add("停止抓包");
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onMenuItemSelected(int featureId, MenuItem item) {
// TODO Auto-generated method stub
String title=(String) item.getTitle();
Log.i(TAG, "the select menuitem is "+title);
if("开始抓包".endsWith(title))
{
start();
}
if("停止抓包".endsWith(title))
{
stop();
}
return super.onMenuItemSelected(featureId, item);
}
/**
* 终止抓包
*/
private void stop() {
// TODO Auto-generated method stub
try {
RootTools.closeAllShells();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
最后,将ip地址转化为地理地址,这个部分的代码,都是从http://blog.csdn.net/swazn_yj/article/details/1611020 拷贝过来的
感谢swazn_yj的源代码
我看过的教程,感谢这些教程的作者
不使用Cygwin,在eclipse中快速开发JNI,一键生成C头文件.h,以及一键使用NDK交叉编译
纯真ip数据库的解析读取: http://blog.csdn.net/swazn_yj/article/details/1611020
libpcap介绍 http://www.cppblog.com/flyonok/articles/49143.html
libpcap,jnetpcap 移植到Android编译 http://aswang.iteye.com/blog/1038284
jpcap项目地址:https://github.com/jpcap/jpcap
RootTools项目地址在:https://github.com/Stericson/RootTools
libpcap 大家都有吧,
本程序所用到的代码,都会上传
用的编译环境为Android sdk 4.4 ,android-ndk的版本是r9的,测试用的手机的平台为Android 4.2.2,Eclipse+ADT的Android开发环境, libpcap的源码是从Android 4.0的源码中提取出来的
源代码地址:http://download.csdn.net/detail/tanqidong1992/7944833