diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..bc0e016 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +EZHZ \ No newline at end of file diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml index 904be2a..b967a5a 100644 --- a/.idea/deploymentTargetDropDown.xml +++ b/.idea/deploymentTargetDropDown.xml @@ -12,6 +12,6 @@ - + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..fd64720 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 88ee048..80cbba5 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 76ed4b2..2a367c8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -35,6 +35,7 @@ android { } buildFeatures { viewBinding = true + dataBinding = true } } @@ -50,6 +51,13 @@ dependencies { implementation("androidx.navigation:navigation-ui-ktx:2.6.0") implementation("androidx.preference:preference:1.2.0") implementation("androidx.preference:preference-ktx:1.2.0") + implementation("com.github.topjohnwu.libsu:core:5.2.0") + implementation("com.github.topjohnwu.libsu:service:5.2.0") + implementation("com.github.topjohnwu.libsu:nio:5.2.0") + implementation("com.github.ixuea:android-downloader:3.0.1") + implementation("com.squareup.okhttp3:okhttp:4.11.0") + implementation("com.google.code.gson:gson:2.10.1") + implementation("org.tukaani:xz:1.9") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 65b1c31..8f2b95f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,8 +1,13 @@ - + + + - + android:name=".SplashActivity" + android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar" + android:exported="true"> - + + + + \ No newline at end of file diff --git a/app/src/main/assets/armeabi-v7a/gost b/app/src/main/assets/armeabi-v7a/gost new file mode 100755 index 0000000..ce61745 Binary files /dev/null and b/app/src/main/assets/armeabi-v7a/gost differ diff --git a/app/src/main/assets/x86/gost b/app/src/main/assets/x86/gost new file mode 100755 index 0000000..cec0624 Binary files /dev/null and b/app/src/main/assets/x86/gost differ diff --git a/app/src/main/java/cc/ggez/ezhz/MainActivity.kt b/app/src/main/java/cc/ggez/ezhz/MainActivity.kt index 98dc93f..2241624 100644 --- a/app/src/main/java/cc/ggez/ezhz/MainActivity.kt +++ b/app/src/main/java/cc/ggez/ezhz/MainActivity.kt @@ -1,18 +1,21 @@ package cc.ggez.ezhz +import android.content.Intent import android.os.Bundle -import com.google.android.material.bottomnavigation.BottomNavigationView +import android.util.Log import androidx.appcompat.app.AppCompatActivity -import androidx.navigation.findNavController +import androidx.navigation.NavController import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupWithNavController import cc.ggez.ezhz.databinding.ActivityMainBinding +import com.google.android.material.bottomnavigation.BottomNavigationView class MainActivity : AppCompatActivity() { - + val TAG = "MainActivity" private lateinit var binding: ActivityMainBinding + private lateinit var navController: NavController override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -20,9 +23,19 @@ class MainActivity : AppCompatActivity() { binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) + val menu = intent?.getStringExtra("menu") + Log.d(TAG, "onCreate: ${menu}") + if (menu.equals("proxy")) { + navController.navigate(R.id.navigation_frida) + } else if (menu.equals("shared_pref")) { + navController.navigate(R.id.navigation_shared_pref) + } else if (menu.equals("frida")) { + navController.navigate(R.id.navigation_proxy) + } + val navView: BottomNavigationView = binding.navView - val navController = binding.navHostFragmentActivityMain.getFragment().navController + navController = binding.navHostFragmentActivityMain.getFragment().navController // Passing each menu ID as a set of Ids because each // menu should be considered as top level destinations. val appBarConfiguration = AppBarConfiguration( @@ -32,5 +45,20 @@ class MainActivity : AppCompatActivity() { ) setupActionBarWithNavController(navController, appBarConfiguration) navView.setupWithNavController(navController) + + + } + + override fun onNewIntent(intent: Intent?) { + val menu = intent?.getStringExtra("menu") + Log.d(TAG, "onActivityReenter: ${intent.toString()}") + if (menu.equals("proxy")) { + navController.navigate(R.id.navigation_frida) + } else if (menu.equals("shared_pref")) { + navController.navigate(R.id.navigation_shared_pref) + } else if (menu.equals("frida")) { + navController.navigate(R.id.navigation_proxy) + } + super.onNewIntent(intent) } } \ No newline at end of file diff --git a/app/src/main/java/cc/ggez/ezhz/SplashActivity.kt b/app/src/main/java/cc/ggez/ezhz/SplashActivity.kt new file mode 100644 index 0000000..3b5bddf --- /dev/null +++ b/app/src/main/java/cc/ggez/ezhz/SplashActivity.kt @@ -0,0 +1,116 @@ +package cc.ggez.ezhz + +import android.Manifest +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.util.Log +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.RequiresApi +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import com.topjohnwu.superuser.Shell +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream + + +class SplashActivity : AppCompatActivity() { + companion object { + init { + Shell.enableVerboseLogging = true + } + } + + private lateinit var requestPermissionLauncher: ActivityResultLauncher + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { + if (it) { + startMain() + } else { + val builder: AlertDialog.Builder = AlertDialog.Builder(this) + builder.setTitle("Notification Permission Required") + builder.setMessage("For foreground service, notification permission is required.") + builder.setPositiveButton("OK") { _, _ -> + startMain() + } + + val dialog: AlertDialog = builder.create() + dialog.show() + } + } + if (ContextCompat.checkSelfPermission( + this, + Manifest.permission.POST_NOTIFICATIONS, + ) != PackageManager.PERMISSION_GRANTED && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + ) { + requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } else { + startMain() + } + + } + + private fun startMain() { + Shell.getShell { shell -> + if (!shell.isRoot) { + val builder: AlertDialog.Builder = AlertDialog.Builder(this) + builder.setTitle("Root Required") + builder.setMessage("Please root your device and grant su permission first.") + builder.setPositiveButton("Exit") { _, _ -> + finish() + } + + val dialog: AlertDialog = builder.create() + dialog.show() + return@getShell + } + + copyAssets() + + val intent = Intent(this, MainActivity::class.java) + startActivity(intent) + finish() + } + } + + @Throws(IOException::class) + private fun copyFile(`in`: InputStream, out: OutputStream) { + val buffer = ByteArray(1024) + var read: Int + while (`in`.read(buffer).also { read = it } != -1) { + out.write(buffer, 0, read) + } + } + + private fun copyAssets() { + val assetManager = assets + var files: Array? = null + val abi = Build.SUPPORTED_ABIS[0] + val arch = if (abi.matches("armeabi-v7a|arm64-v8a".toRegex())) "armeabi-v7a" else "x86" + + files = assetManager.list(arch) + + if (files != null) { + for (file in files) { + try { + val inFile = assetManager.open("${arch}/$file") + val outFile = FileOutputStream("${filesDir.absolutePath}/${file}") + copyFile(inFile, outFile) + inFile.close() + outFile.flush() + outFile.close() + } catch (e: IOException) { + Log.e("tag", "Failed to copy asset file: $file", e) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/cc/ggez/ezhz/core/appselect/AppAdapter.kt b/app/src/main/java/cc/ggez/ezhz/core/appselect/AppAdapter.kt new file mode 100644 index 0000000..7688908 --- /dev/null +++ b/app/src/main/java/cc/ggez/ezhz/core/appselect/AppAdapter.kt @@ -0,0 +1,107 @@ +package cc.ggez.ezhz.core.appselect + +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.Filter +import androidx.recyclerview.widget.RecyclerView +import cc.ggez.ezhz.databinding.ItemAppBinding + +class AppAdapter : RecyclerView.Adapter() { + + private var isSystemAppShown = false + val rawAppList = ArrayList() + var appList = ArrayList() + + + // create an inner class with name ViewHolder + // It takes a view argument, in which pass the generated class of single_item.xml + // ie SingleItemBinding and in the RecyclerView.ViewHolder(binding.root) pass it like this + inner class ViewHolder(val binding: ItemAppBinding) : RecyclerView.ViewHolder(binding.root) + + // inside the onCreateViewHolder inflate the view of SingleItemBinding + // and return new ViewHolder object containing this layout + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = ItemAppBinding.inflate(LayoutInflater.from(parent.context), parent, false) + + return ViewHolder(binding) + } + + // bind the items with each item + // of the list languageList + // which than will be + // shown in recycler view + // to keep it simple we are + // not setting any image data to view + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + with(holder){ + with(appList[position]){ + binding.tvAppName.text = this.name + binding.tvPackageName.text = this.packageName + binding.ivAppIcon.setImageDrawable(this.icon) + binding.cardApp.isChecked = this.proxyed + binding.cardApp.setOnClickListener { + binding.cardApp.isChecked = !binding.cardApp.isChecked + this.proxyed = binding.cardApp.isChecked + } + } + } + } + + // return the size of languageList + override fun getItemCount(): Int { + return appList.size + } + + fun addAll(appList: ArrayList) { + rawAppList.clear() + rawAppList.addAll(appList) + this.appList.clear() + this.appList.addAll(getAppListFromRaw()) + } + + fun getFilter(): Filter { + return cityFilter + } + + fun getSelectedApp(): ArrayList { + return ArrayList(appList.filter { it.proxyed }) + } + + fun setIsSystemAppShown(isSystemAppShown: Boolean) { + this.isSystemAppShown = isSystemAppShown + appList.clear() + appList.addAll(getAppListFromRaw()) + notifyDataSetChanged() + } + + fun getAppListFromRaw(): ArrayList { + return ArrayList(rawAppList.filter { !it.system || isSystemAppShown || it.proxyed }) + } + + private val cityFilter = object : Filter() { + override fun performFiltering(constraint: CharSequence?): FilterResults { + val filteredAppList: ArrayList = ArrayList() + if (constraint.isNullOrEmpty()) { + getAppListFromRaw().let { filteredAppList.addAll(it) } + } else { + val query = constraint.toString().trim().lowercase() + getAppListFromRaw().forEach { + if (it.name.lowercase().contains(query) || it.packageName.lowercase().contains(query) || it.proxyed) { + filteredAppList.add(it) + } + } + } + val results = FilterResults() + results.values = filteredAppList + return results + } + + override fun publishResults(constraint: CharSequence?, results: FilterResults?) { + if (results?.values is ArrayList<*>) { + appList.clear() + appList.addAll(results.values as ArrayList) + notifyDataSetChanged() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/cc/ggez/ezhz/core/appselect/AppObject.kt b/app/src/main/java/cc/ggez/ezhz/core/appselect/AppObject.kt new file mode 100644 index 0000000..bf5f2a2 --- /dev/null +++ b/app/src/main/java/cc/ggez/ezhz/core/appselect/AppObject.kt @@ -0,0 +1,15 @@ +package cc.ggez.ezhz.core.appselect + +import android.graphics.drawable.Drawable + +data class AppObject( + val name: String, + val packageName: String, + val icon: Drawable, + val enabled: Boolean, + val uid: Int, + val username: String?, + val procname: String, + var proxyed: Boolean, + val system: Boolean +) diff --git a/app/src/main/java/cc/ggez/ezhz/core/appselect/AppSelectActivity.kt b/app/src/main/java/cc/ggez/ezhz/core/appselect/AppSelectActivity.kt new file mode 100644 index 0000000..f77e6f8 --- /dev/null +++ b/app/src/main/java/cc/ggez/ezhz/core/appselect/AppSelectActivity.kt @@ -0,0 +1,106 @@ +package cc.ggez.ezhz.core.appselect + +import android.content.Intent +import android.opengl.Visibility +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.SearchView +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import cc.ggez.ezhz.R +import cc.ggez.ezhz.databinding.AppSelectActivityBinding +import cc.ggez.ezhz.core.appselect.AppSelectUtil.Companion.getApps +import kotlin.concurrent.thread + + +class AppSelectActivity : AppCompatActivity() { + private lateinit var binding: AppSelectActivityBinding + private lateinit var appAdapter: AppAdapter + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val selectedApps = intent.getStringArrayListExtra("selectedApps") ?: ArrayList() + selectedApps.sort() + + binding = AppSelectActivityBinding.inflate(layoutInflater) + setContentView(binding.root) + + setLoading(true) + + val layoutManager: RecyclerView.LayoutManager = LinearLayoutManager(this) + + binding.rvAppList.layoutManager = layoutManager + + appAdapter = AppAdapter() + binding.rvAppList.adapter = appAdapter + + binding.sbAppSearch.setOnQueryTextListener(object: SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + appAdapter.getFilter().filter(query) + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + appAdapter.getFilter().filter(newText) + return false + } + }) + + binding.tbAppType.setOnCheckedChangeListener { _, isChecked -> + appAdapter.setIsSystemAppShown(isChecked) + } + + thread { + val appList = getApps(baseContext) + + appList.forEach { + if (selectedApps.binarySearch("${it.packageName}:${it.uid}") >= 0) { + it.proxyed = true + } + } + appAdapter.addAll(appList.sortedBy { it.proxyed }.reversed() as ArrayList) + runOnUiThread { + appAdapter.notifyDataSetChanged() + setLoading(false) + } + } + } + + private fun setLoading(loading: Boolean) { + if (loading) { + binding.pbLoading.visibility = View.VISIBLE + binding.rvAppList.visibility = View.GONE + binding.sbAppSearch.visibility = View.GONE + binding.tbAppType.visibility = View.GONE + } else { + binding.pbLoading.visibility = View.GONE + binding.rvAppList.visibility = View.VISIBLE + binding.sbAppSearch.visibility = View.VISIBLE + binding.tbAppType.visibility = View.VISIBLE + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + val inflater: MenuInflater = menuInflater + inflater.inflate(R.menu.manu_app_select, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + // Handle item selection + return when (item.itemId) { + R.id.action_save -> { + setResult(RESULT_OK, Intent().apply { + putStringArrayListExtra("selectedApps", appAdapter.getSelectedApp().map { "${it.packageName}:${it.uid}" } as ArrayList) + }) + finish() + true + } + else -> super.onOptionsItemSelected(item) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/cc/ggez/ezhz/core/appselect/AppSelectUtil.kt b/app/src/main/java/cc/ggez/ezhz/core/appselect/AppSelectUtil.kt new file mode 100644 index 0000000..da6b999 --- /dev/null +++ b/app/src/main/java/cc/ggez/ezhz/core/appselect/AppSelectUtil.kt @@ -0,0 +1,34 @@ +package cc.ggez.ezhz.core.appselect + +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.os.Build + + +class AppSelectUtil { + companion object { + fun getApps(context: Context): ArrayList { + val pm = context.packageManager + val appInfos = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + pm.getInstalledApplications(PackageManager.ApplicationInfoFlags.of(0L)) + } else { + pm.getInstalledApplications(0) + } + + return ArrayList(appInfos.map { + AppObject( + name = pm.getApplicationLabel(it).toString(), + packageName = it.packageName, + icon = pm.getApplicationIcon(it), + enabled = it.enabled, + system = (it.flags and ApplicationInfo.FLAG_SYSTEM) != 0, + uid = it.uid, + username = pm.getNameForUid(it.uid), + procname = it.processName, + proxyed = false + ) + }) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/cc/ggez/ezhz/module/frida/FridaAdapter.kt b/app/src/main/java/cc/ggez/ezhz/module/frida/FridaAdapter.kt new file mode 100644 index 0000000..16bc636 --- /dev/null +++ b/app/src/main/java/cc/ggez/ezhz/module/frida/FridaAdapter.kt @@ -0,0 +1,70 @@ +package cc.ggez.ezhz.module.frida + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import androidx.annotation.NonNull +import androidx.recyclerview.widget.RecyclerView +import android.view.ViewGroup +import cc.ggez.ezhz.databinding.ItemFridaBinding +import cc.ggez.ezhz.module.frida.model.FridaItem +import cc.ggez.ezhz.module.frida.model.GithubTag + +class FridaAdapter: RecyclerView.Adapter() { + private var fridaItems = mutableListOf() + private lateinit var onExecuteCallback: ((FridaItem, Int) -> Unit) + private lateinit var onInstallCallback: ((FridaItem, Int) -> Unit) + private var globalDisabled = false + @SuppressLint("NotifyDataSetChanged") + fun setFridaList(fridaItems: List) { + this.fridaItems = fridaItems.toMutableList() + notifyDataSetChanged() + } + + fun setExecuteListener(callback: (FridaItem, Int) -> Unit) { + onExecuteCallback = callback + } + + fun setInstallListener(callback: (FridaItem, Int) -> Unit) { + onInstallCallback = callback + } + + @SuppressLint("NotifyDataSetChanged") + fun disableAll() { + if (globalDisabled) return + globalDisabled = true + notifyDataSetChanged() + } + + @SuppressLint("NotifyDataSetChanged") + fun enableAll() { + if(!globalDisabled) return + globalDisabled = false + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FridaViewHolder = + FridaViewHolder(ItemFridaBinding.inflate(LayoutInflater.from(parent.context), parent, false)) + + override fun onBindViewHolder(holder: FridaViewHolder, position: Int) { + holder.apply { + bind(fridaItems[position], position, onExecuteCallback, onInstallCallback, globalDisabled) + } + } + + override fun getItemCount(): Int { + return fridaItems.size + } +} +class FridaViewHolder(private val binding: ItemFridaBinding) : RecyclerView.ViewHolder(binding.root) { + + fun bind(item: FridaItem, position: Int, onExecuteCallback: ((FridaItem, Int) -> Unit), onInstallCallback: ((FridaItem, Int) -> Unit), globalDisabled: Boolean) { + binding.frida = item + binding.btnExecute.setOnClickListener { + onExecuteCallback(item, position) + } + binding.btnInstall.setOnClickListener { + onInstallCallback(item, position) + } + binding.globalDisabled = globalDisabled + } +} \ No newline at end of file diff --git a/app/src/main/java/cc/ggez/ezhz/module/frida/FridaFragment.kt b/app/src/main/java/cc/ggez/ezhz/module/frida/FridaFragment.kt new file mode 100644 index 0000000..fd83e6c --- /dev/null +++ b/app/src/main/java/cc/ggez/ezhz/module/frida/FridaFragment.kt @@ -0,0 +1,104 @@ +package cc.ggez.ezhz.module.frida + +import android.app.NotificationManager +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import cc.ggez.ezhz.R +import cc.ggez.ezhz.databinding.DialogDownloadBinding +import cc.ggez.ezhz.databinding.FragmentFridaBinding +import cc.ggez.ezhz.module.frida.helper.FridaHelper.Companion.checkFridaServerProcessTag + + +class FridaFragment : Fragment() { + + private var _binding: FragmentFridaBinding? = null + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + private val fridaAdapter = FridaAdapter() + private lateinit var dialogDownloadBinding: DialogDownloadBinding + private lateinit var dialog: AlertDialog + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val fridaViewModel = + ViewModelProvider(this)[FridaViewModel::class.java] + + _binding = FragmentFridaBinding.inflate(inflater, container, false) + val root: View = binding.root + + dialogDownloadBinding = DialogDownloadBinding.inflate(LayoutInflater.from(context)) + val builder: AlertDialog.Builder = AlertDialog.Builder(requireContext()) + builder.setCancelable(false) + builder.setView(dialogDownloadBinding.root) + dialog = builder.create() + + + binding.rvFrida.adapter = fridaAdapter + + fridaAdapter.setInstallListener { fridaItem, position -> + if (!fridaItem.isInstallable) return@setInstallListener + + if (fridaItem.isInstalled) { + fridaViewModel.uninstallFridaServer(fridaItem) + } else { + fridaViewModel.installFridaServer(fridaItem) + } + } + + fridaAdapter.setExecuteListener { fridaItem, position -> + if (!fridaItem.isExecutable) return@setExecuteListener + + if (fridaItem.isExecuted) { + fridaViewModel.killFridaServer(fridaItem) + } else { + val tag = checkFridaServerProcessTag() + if (tag.isNotEmpty()) { + Toast.makeText(requireContext(), "Frida Server v${tag} has already been executed.", Toast.LENGTH_LONG).show() + return@setExecuteListener + } + + fridaViewModel.executeFridaServer(fridaItem) + } + } + + fridaViewModel.fridaItemList.observe(viewLifecycleOwner) { fridaItems -> + fridaAdapter.setFridaList(fridaItems) + } + + fridaViewModel.errorMessage.observe(viewLifecycleOwner, { + }) + + fridaViewModel.installProgress.observe(viewLifecycleOwner) { + if (it == -1) { + fridaAdapter.enableAll() + dialog.dismiss() + dialogDownloadBinding.progress = 0 + } else { + fridaAdapter.disableAll() + dialog.show() + dialogDownloadBinding.progress = it + } + } + + fridaViewModel.getAllFridaItems(true) + + return root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/cc/ggez/ezhz/module/frida/FridaService.kt b/app/src/main/java/cc/ggez/ezhz/module/frida/FridaService.kt new file mode 100644 index 0000000..bfe7d22 --- /dev/null +++ b/app/src/main/java/cc/ggez/ezhz/module/frida/FridaService.kt @@ -0,0 +1,127 @@ +package cc.ggez.ezhz.module.frida + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Intent +import android.media.AudioManager +import android.os.Build +import android.os.IBinder +import android.telephony.mbms.StreamingService +import android.util.Log +import androidx.core.app.NotificationCompat +import cc.ggez.ezhz.MainActivity +import cc.ggez.ezhz.R +import cc.ggez.ezhz.module.frida.helper.FridaHelper +import java.io.File + + +class FridaService : Service() { + val TAG = "FridaService" + lateinit var fridaServerDir: String + private val notificationManager: NotificationManager by lazy { + getSystemService(NOTIFICATION_SERVICE) as NotificationManager + } + private lateinit var pendIntent: PendingIntent + private lateinit var pStopSelf: PendingIntent + override fun onBind(intent: Intent?): IBinder? { + return null + } + + override fun onCreate() { + Log.d(TAG, "Service Frida Create"); + super.onCreate() + fridaServerDir = "${filesDir.absolutePath}/server" + createNotificationChannel() + val intent = Intent(this, MainActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP + intent.putExtra("menu", "frida") + pendIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) + } else { + PendingIntent.getActivity( + this, + 0, + intent, + PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE + ) + } + val stopIntent = Intent(this, FridaService::class.java) + stopIntent.action = "STOP" + pStopSelf = PendingIntent.getForegroundService(this, 0, stopIntent, + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE) + notifyAlert( + getString(R.string.app_name) + " | Frida Module", + getString(R.string.service_running) + ) + } + + private fun handlerCommand(tag: String): Boolean { + if (File("$fridaServerDir/frida-server-$tag").exists()) { + FridaHelper.startFridaServer(fridaServerDir, tag) + return true + } + return false + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if(intent?.action.equals("STOP")) { + stopSelf() + return super.onStartCommand(intent, flags, startId) + } + val tag = intent?.getStringExtra("tag") + Thread { + if (tag == null || !handlerCommand(tag)) { + stopSelf() + } + }.start() + return super.onStartCommand(intent, flags, startId) + } + + override fun onDestroy() { + FridaHelper.stopFridaServer() + notificationManager.cancelAll(); + stopForeground(STOP_FOREGROUND_DETACH); + super.onDestroy() + } + + private fun createNotificationChannel() { + val name: CharSequence = "EZHZ Frida Service" + val description = "EZHZ Frida Background Service" + val importance = NotificationManager.IMPORTANCE_DEFAULT + val channel = NotificationChannel("EZHZ Frida Service", name, importance) + channel.description = description + notificationManager.createNotificationChannel(channel) + } + + private fun initSoundVibrateLights(builder: NotificationCompat.Builder) { + val audioManager = this.getSystemService(AUDIO_SERVICE) as AudioManager + if (audioManager.getStreamVolume(AudioManager.STREAM_RING) == 0) { + builder.setSound(null) + } + + builder.setVibrate(longArrayOf(0, 1000, 500, 1000, 500, 1000)) + } + + private fun notifyAlert(title: String, info: String) { + val notiTitle = "${getString(R.string.app_name)} | Frida Module" + val builder = NotificationCompat.Builder(this, "Service") + initSoundVibrateLights(builder) + builder.setAutoCancel(false) + builder.setTicker(notiTitle) + builder.setContentTitle(title) + builder.setContentText(info) + builder.setSmallIcon(R.drawable.ic_debugging) + builder.setContentIntent(pendIntent) + builder.addAction( + android.R.drawable.ic_lock_power_off, + getString(R.string.service_stop), + pStopSelf) + builder.priority = NotificationCompat.PRIORITY_DEFAULT + builder.setOngoing(true) + builder.setChannelId("EZHZ Frida Service") + + startForeground(2, builder.build()) + } +} \ No newline at end of file diff --git a/app/src/main/java/cc/ggez/ezhz/module/frida/FridaViewModel.kt b/app/src/main/java/cc/ggez/ezhz/module/frida/FridaViewModel.kt new file mode 100644 index 0000000..8390bb5 --- /dev/null +++ b/app/src/main/java/cc/ggez/ezhz/module/frida/FridaViewModel.kt @@ -0,0 +1,256 @@ +package cc.ggez.ezhz.module.frida + +import android.app.Application +import android.content.Intent +import android.os.Bundle +import android.util.Log +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData +import cc.ggez.ezhz.module.frida.helper.FridaHelper.Companion.checkFridaServerProcessTag +import cc.ggez.ezhz.module.frida.helper.FridaHelper.Companion.getDownloadedFridaTags +import cc.ggez.ezhz.module.frida.helper.FridaHelper.Companion.removeFridaServer +import cc.ggez.ezhz.module.frida.helper.FridaHelper.Companion.startFridaServer +import cc.ggez.ezhz.module.frida.helper.FridaHelper.Companion.stopFridaServer +import cc.ggez.ezhz.module.frida.helper.GithubHelper +import cc.ggez.ezhz.module.frida.model.FridaItem +import cc.ggez.ezhz.module.frida.model.FridaItemState +import cc.ggez.ezhz.module.frida.model.GithubRelease +import cc.ggez.ezhz.module.frida.model.GithubTag +import cc.ggez.ezhz.module.proxy.ProxyService +import com.ixuea.android.downloader.DownloadService +import com.ixuea.android.downloader.callback.DownloadListener +import com.ixuea.android.downloader.domain.DownloadInfo +import com.ixuea.android.downloader.exception.DownloadException +import org.tukaani.xz.XZInputStream +import java.io.BufferedInputStream +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException + + +class FridaViewModel(application: Application) : AndroidViewModel(application) { + + val TAG = "FridaViewModel" + + val fridaItemList = MutableLiveData>() + val installProgress = MutableLiveData(-1) + val errorMessage = MutableLiveData() + val cacheDir: File = getApplication().cacheDir + var fridaServerDir = "${getApplication().filesDir.absolutePath}/server" + val downloadManager = DownloadService.getDownloadManager(getApplication().applicationContext); + + private fun checkCacheTags(): Boolean { + val file = File(cacheDir, "frida_tags.json") + return file.exists() + } + + private fun getCacheTags(): String { + val file = File(cacheDir, "frida_tags.json") + return file.readText() + } + + private fun cacheTags(tags: String) { + val file = File(cacheDir, "frida_tags.json") + file.writeText(tags) + } + + private fun fridaItemsFromTags(tags: List): List { + val installedTags = getDownloadedFridaTags(fridaServerDir) + val executedTag = checkFridaServerProcessTag() + + Log.d("installedTags", installedTags.toString()) + + val fridaItems = tags.map { + val state = if (executedTag == it.name) { + FridaItemState.EXECUTING + } + else if (installedTags.contains(it.name)) { + FridaItemState.INSTALLED + } else { + FridaItemState.NOT_INSTALL + } + + FridaItem(it, state) + } + + return fridaItems.sortedWith(Comparator{ a, b -> + if (a.state < b.state) { + return@Comparator 1 + } + else if (a.state > b.state) { + return@Comparator -1 + } + + return@Comparator b.tag.name.compareTo(a.tag.name) + }) + } + + fun getAllFridaItems(forceReload: Boolean = false) { + if (forceReload && checkCacheTags()) { + val tags = GithubHelper.translateTags(getCacheTags()) + fridaItemList.postValue(fridaItemsFromTags(tags)) + } else { + GithubHelper.fetchFridaTags("frida", "frida", 0, object: GithubHelper.GithubTagsCallback { + override fun onSuccess(resJson: String) { + cacheTags(resJson) + val tags = GithubHelper.translateTags(getCacheTags()) + fridaItemList.postValue(fridaItemsFromTags(tags)) + } + + override fun onFailure(e: IOException) { + errorMessage.postValue(e.message) + } + }) + } + } + + fun executeFridaServer(fridaItem: FridaItem) { + try { + val it = Intent(getApplication(), FridaService::class.java) + val bundle = Bundle() + bundle.putString("tag", fridaItem.tag.name) + it.putExtras(bundle) + getApplication().startForegroundService(it) + } catch (e: Exception) { + Log.e(TAG, "serviceStart: ${e.message}") + } + + val fridaItems = fridaItemList.value!! + fridaItems.map { item -> + if (item.tag.name == fridaItem.tag.name) { + item.state = FridaItemState.EXECUTING + } + } + fridaItemList.postValue(fridaItems.toMutableList()) + } + + fun killFridaServer(fridaItem: FridaItem) { + try { + getApplication().stopService(Intent(getApplication(), FridaService::class.java)) + stopFridaServer() + } catch (e: Exception) { + Log.e(TAG, "serviceStop: ${e.message}") + } + val fridaItems = fridaItemList.value!! + fridaItems.map { item -> + if (item.tag.name == fridaItem.tag.name) { + item.state = FridaItemState.INSTALLED + } + } + fridaItemList.postValue(fridaItems.toMutableList()) + } + + fun installFridaServer(fridaItem: FridaItem) { + installProgress.postValue(0) + Log.d(TAG, "[+] Install Start ${fridaItem.tag.name}") + GithubHelper.fetchFridaRelease("frida", "frida", fridaItem.tag.name, object: GithubHelper.GithubReleaseCallback { + override fun onSuccess(resJson: String) { + val release = GithubHelper.translateRelease(resJson) + val downloadUrl = GithubHelper.downloadUrlFromRelease(release) + Log.d(TAG, "[+] Download Start ${downloadUrl}") + val downloadInfo = DownloadInfo.Builder().setUrl(downloadUrl) + .setPath("${cacheDir.absolutePath}/frida-server-${fridaItem.tag.name}.xz") + .build() + + downloadInfo.downloadListener = object : DownloadListener { + override fun onStart() { + installProgress.postValue(0) + } + + override fun onWaited() { + } + + override fun onPaused() { + } + + override fun onDownloading(progress: Long, size: Long) { + installProgress.postValue((progress * 100 / size).toInt()) + } + + override fun onRemoved() { + installProgress.postValue(-1) + } + + override fun onDownloadSuccess() { + installProgress.postValue(-1) + Log.d(TAG, "[+] Download Finish") + val fridaServerDir = File(fridaServerDir) + if (!fridaServerDir.exists()) { + fridaServerDir.mkdir() + } + + Log.d(TAG, "[+] Uncompressing XZ") + try { + val fin = + FileInputStream( + File( + cacheDir, + "frida-server-${fridaItem.tag.name}.xz" + ) + ) + val inXZ = BufferedInputStream(fin) + val outXZ = + FileOutputStream( + File( + fridaServerDir, + "frida-server-${fridaItem.tag.name}" + ) + ) + + val xzIn = XZInputStream(inXZ) + val buffer = ByteArray(8192) + + var n = 0 + while (-1 != xzIn.read(buffer).also { n = it }) { + outXZ.write(buffer, 0, n) + } + xzIn.close() + fin.close() + outXZ.close() + val fridaItems = fridaItemList.value!! + fridaItems.map { item -> + if (item.tag.name == fridaItem.tag.name) { + item.state = FridaItemState.INSTALLED + } + } + fridaItemList.postValue(fridaItems.toMutableList()) + } catch (e: IOException) { + Log.e(TAG, "Uncompress XZ Error: ${e.message}") + } + + val cacheXZ = File(cacheDir, "frida-server-${fridaItem.tag.name}.xz") + if (cacheXZ.exists()) { + cacheXZ.delete() + } + } + + override fun onDownloadFailed(e: DownloadException?) { + installProgress.postValue(-1) + errorMessage.postValue("Frida server download failed: ${e?.message}") + } + + } + + downloadManager.download(downloadInfo) + } + + override fun onFailure(e: IOException) { + Log.d(TAG, e.message.toString()) + errorMessage.postValue(e.message) + } + }) + } + + fun uninstallFridaServer(fridaItem: FridaItem) { + removeFridaServer(fridaServerDir, fridaItem.tag.name) + + val fridaItems = fridaItemList.value!! + fridaItems.map { item -> + if (item.tag.name == fridaItem.tag.name) { + item.state = FridaItemState.NOT_INSTALL + } + } + fridaItemList.postValue(fridaItems.toMutableList()) + } +} \ No newline at end of file diff --git a/app/src/main/java/cc/ggez/ezhz/module/frida/helper/CommonHelper.kt b/app/src/main/java/cc/ggez/ezhz/module/frida/helper/CommonHelper.kt new file mode 100644 index 0000000..18f7f36 --- /dev/null +++ b/app/src/main/java/cc/ggez/ezhz/module/frida/helper/CommonHelper.kt @@ -0,0 +1,21 @@ +package cc.ggez.ezhz.module.frida.helper + +import com.topjohnwu.superuser.Shell +import java.io.File + +class CommonHelper { + + companion object { + fun getArchType(): String { + val archType = Shell.cmd("getprop ro.product.cpu.abi").exec().out[0] + return when (archType) { + "x86" -> "x86" + "arm64-v8a" -> "arm64" + "x86_64" -> "x86_64" + "armeabi-v7a" -> "arm" + "armeabi" -> "arm" + else -> "arm64" + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/cc/ggez/ezhz/module/frida/helper/FridaHelper.kt b/app/src/main/java/cc/ggez/ezhz/module/frida/helper/FridaHelper.kt new file mode 100644 index 0000000..8ee2a3c --- /dev/null +++ b/app/src/main/java/cc/ggez/ezhz/module/frida/helper/FridaHelper.kt @@ -0,0 +1,69 @@ +package cc.ggez.ezhz.module.frida.helper + +import android.util.Log +import cc.ggez.ezhz.module.frida.model.GithubRelease +import cc.ggez.ezhz.module.frida.model.GithubTag +import com.google.gson.Gson +import com.topjohnwu.superuser.Shell +import okhttp3.Call +import okhttp3.Callback +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import java.io.File +import java.io.IOException +import java.util.concurrent.TimeUnit + +class FridaHelper { + companion object { + val archType: String = CommonHelper.getArchType() + + fun checkFridaServerProcessTag(): String { + val stdout: List = ArrayList() + Shell.cmd("ps -A | grep -v grep | grep ggez-server | awk '{print \$9}'").to(stdout).exec() + if (stdout.isNotEmpty()) { + return stdout[0].replace( "ggez-server-", "") + } + return "" + } + + fun checkFridaServerProcessId(): String { + val stdout: List = ArrayList() + Shell.cmd("ps -A | grep -v grep | grep ggez-server | awk '{print \$2}'").to(stdout).exec() + if (stdout.isNotEmpty()) { + return stdout[0].replace( "ggez-server-", "") + } + return "" + } + + fun startFridaServer(serverPath: String, tag: String) { + Shell.cmd("cp ${serverPath}/frida-server-${tag} /data/local/tmp/ggez-server-${tag}", + "chmod +x /data/local/tmp/ggez-server-${tag}", + "/data/local/tmp/ggez-server-${tag} &" + ).submit() + } + + fun stopFridaServer() { + val pid = checkFridaServerProcessId() + if (pid.isNotEmpty()) Shell.cmd("kill -9 ${pid}").exec() + } + + fun getDownloadedFridaTags(path: String): List { + val serverDir = File(path) + val files = serverDir.listFiles() + return files?.map { it.name.replace("frida-server-", "") } ?: emptyList() + } + + fun removeFridaServer(path: String, tag: String) { + val serverDir = File(path) + val files = serverDir.listFiles() + files?.forEach { + if (it.name.contains(tag)) { + it.delete() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/cc/ggez/ezhz/module/frida/helper/GithubHelper.kt b/app/src/main/java/cc/ggez/ezhz/module/frida/helper/GithubHelper.kt new file mode 100644 index 0000000..e99f564 --- /dev/null +++ b/app/src/main/java/cc/ggez/ezhz/module/frida/helper/GithubHelper.kt @@ -0,0 +1,113 @@ +package cc.ggez.ezhz.module.frida.helper + +import android.util.Log +import cc.ggez.ezhz.module.frida.model.GithubRelease +import cc.ggez.ezhz.module.frida.model.GithubTag +import com.google.gson.Gson +import okhttp3.Call +import okhttp3.Callback +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import java.io.File +import java.io.IOException +import java.util.concurrent.TimeUnit + +class GithubHelper { + interface GithubTagsCallback { + fun onSuccess(resJson: String) + fun onFailure(e: IOException) + } + + interface GithubReleaseCallback { + fun onSuccess(resJson: String) + fun onFailure(e: IOException) + } + companion object { + + const val TAG = "FridaGithubHelper" + const val fridaGithubEndpoint = "https://api.github.com/repos" + val archType = CommonHelper.getArchType() + + private val client = OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .build() + + fun fetchFridaTags(owner: String, repo: String, page: Int, callback: GithubTagsCallback) { + val urlBuilder: HttpUrl.Builder = + ("$fridaGithubEndpoint/$owner/$repo/tags").toHttpUrl().newBuilder() + + urlBuilder.addQueryParameter("per_page", "100") + urlBuilder.addQueryParameter("page", page.toString()) + val url = urlBuilder.build().toString() + + val request = Request.Builder() + .url(url) + .build() + + val call = client.newCall(request) + call.enqueue(object : Callback { + + override fun onFailure(call: Call, e: IOException) { + Log.e(TAG, "${e.message}") + callback.onFailure(e) + } + + override fun onResponse(call: Call, response: Response) { + val resJson = response.body!!.string() + callback.onSuccess(resJson) + } + }) + } + + fun fetchFridaRelease(owner: String, repo: String, tag: String, callback: GithubReleaseCallback) { + val urlBuilder: HttpUrl.Builder = + ("$fridaGithubEndpoint/$owner/$repo/releases/tags/$tag").toHttpUrl().newBuilder() + + val url = urlBuilder.build().toString() + Log.d(TAG, url) + + val request: Request = Request.Builder() + .url(url) + .build() + + val call = client.newCall(request) + call.enqueue(object : Callback { + + override fun onFailure(call: Call, e: IOException) { + Log.e(TAG, "${e.message}") + callback.onFailure(e) + } + + override fun onResponse(call: Call, response: Response) { + val resJson = response.body!!.string() + callback.onSuccess(resJson) + } + }) + } + + fun translateTags(resJson: String): List { + val gson = Gson() + val tags = gson.fromJson(resJson, Array::class.java) + return ArrayList(tags.toList()) + } + + fun translateRelease(resJson: String): GithubRelease { + val gson = Gson() + return gson.fromJson(resJson, GithubRelease::class.java) + } + + fun downloadUrlFromRelease(release: GithubRelease): String { + val assets = release.assets + for (asset in assets) { + if (asset.name.contains("-$archType") && asset.name.contains("frida-server")) { + return asset.browser_download_url + } + } + return "" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/cc/ggez/ezhz/module/frida/model/FridaItem.kt b/app/src/main/java/cc/ggez/ezhz/module/frida/model/FridaItem.kt new file mode 100644 index 0000000..2ee5e4b --- /dev/null +++ b/app/src/main/java/cc/ggez/ezhz/module/frida/model/FridaItem.kt @@ -0,0 +1,27 @@ +package cc.ggez.ezhz.module.frida.model + +enum class FridaItemState(s: Int) { + NOT_INSTALL(0), INSTALLED(1), EXECUTING(2) +} +data class FridaItem( + val tag: GithubTag, + var state: FridaItemState = FridaItemState.NOT_INSTALL, +) { + val isExecuted: Boolean + get(): Boolean { + return state == FridaItemState.EXECUTING + } + val isExecutable: Boolean + get(): Boolean { + return state == FridaItemState.INSTALLED || state == FridaItemState.EXECUTING + } + val isInstalled: Boolean + get(): Boolean { + return state == FridaItemState.EXECUTING || state == FridaItemState.INSTALLED + } + + val isInstallable: Boolean + get(): Boolean { + return state != FridaItemState.EXECUTING + } +} \ No newline at end of file diff --git a/app/src/main/java/cc/ggez/ezhz/module/frida/model/GithubAsset.kt b/app/src/main/java/cc/ggez/ezhz/module/frida/model/GithubAsset.kt new file mode 100644 index 0000000..ad50c2c --- /dev/null +++ b/app/src/main/java/cc/ggez/ezhz/module/frida/model/GithubAsset.kt @@ -0,0 +1,14 @@ +package cc.ggez.ezhz.module.frida.model + +data class GithubAsset( + val url: String, + val browser_download_url: String, + val id: Int, + val node_id: String, + val name: String, + val label: String, + val state: String, + val created_at: String, + val updated_at: String, + val uploader: GithubUser +) diff --git a/app/src/main/java/cc/ggez/ezhz/module/frida/model/GithubCommit.kt b/app/src/main/java/cc/ggez/ezhz/module/frida/model/GithubCommit.kt new file mode 100644 index 0000000..105a655 --- /dev/null +++ b/app/src/main/java/cc/ggez/ezhz/module/frida/model/GithubCommit.kt @@ -0,0 +1,6 @@ +package cc.ggez.ezhz.module.frida.model + +data class GithubCommit( + val sha: String, + val url: String, +) \ No newline at end of file diff --git a/app/src/main/java/cc/ggez/ezhz/module/frida/model/GithubRelease.kt b/app/src/main/java/cc/ggez/ezhz/module/frida/model/GithubRelease.kt new file mode 100644 index 0000000..6a88636 --- /dev/null +++ b/app/src/main/java/cc/ggez/ezhz/module/frida/model/GithubRelease.kt @@ -0,0 +1,23 @@ +package cc.ggez.ezhz.module.frida.model + +data class GithubRelease( + val url: String, + val html_url: String, + val assets_url: String, + val upload_url: String, + val tarball_url: String, + val zipball_url: String, + val discussion_url: String, + val id: Int, + val node_id: String, + val tag_name: String, + val target_commitish: String, + val name: String, + val body: String, + val draft: Boolean, + val prerelease: Boolean, + val created_at: String, + val published_at: String, + val author: GithubUser, + val assets: List, +) \ No newline at end of file diff --git a/app/src/main/java/cc/ggez/ezhz/module/frida/model/GithubTag.kt b/app/src/main/java/cc/ggez/ezhz/module/frida/model/GithubTag.kt new file mode 100644 index 0000000..79ec7ae --- /dev/null +++ b/app/src/main/java/cc/ggez/ezhz/module/frida/model/GithubTag.kt @@ -0,0 +1,9 @@ +package cc.ggez.ezhz.module.frida.model + +data class GithubTag( + val name: String, + val commit: GithubCommit, + val zipball_url: String, + val tarball_url: String, + val node_id: String, +) \ No newline at end of file diff --git a/app/src/main/java/cc/ggez/ezhz/module/frida/model/GithubUser.kt b/app/src/main/java/cc/ggez/ezhz/module/frida/model/GithubUser.kt new file mode 100644 index 0000000..73f017b --- /dev/null +++ b/app/src/main/java/cc/ggez/ezhz/module/frida/model/GithubUser.kt @@ -0,0 +1,22 @@ +package cc.ggez.ezhz.module.frida.model + +data class GithubUser( + val login: String, + val id: Int, + val node_id: String, + val avatar_url: String, + val gravatar_id: String, + val url: String, + val html_url: String, + val followers_url: String, + val following_url: String, + val gists_url: String, + val starred_url: String, + val subscriptions_url: String, + val organizations_url: String, + val repos_url: String, + val events_url: String, + val received_events_url: String, + val type: String, + val site_admin: Boolean, +) \ No newline at end of file diff --git a/app/src/main/java/cc/ggez/ezhz/module/proxy/ProxyFragment.kt b/app/src/main/java/cc/ggez/ezhz/module/proxy/ProxyFragment.kt new file mode 100644 index 0000000..b7ad1af --- /dev/null +++ b/app/src/main/java/cc/ggez/ezhz/module/proxy/ProxyFragment.kt @@ -0,0 +1,193 @@ +package cc.ggez.ezhz.module.proxy + +import android.app.Activity +import android.content.Intent +import android.content.SharedPreferences +import android.os.Bundle +import android.util.Log +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.preference.CheckBoxPreference +import androidx.preference.EditTextPreference +import androidx.preference.ListPreference +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.PreferenceManager +import androidx.preference.SwitchPreferenceCompat +import cc.ggez.ezhz.R +import cc.ggez.ezhz.core.appselect.AppSelectActivity + + +class ProxyFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener { + + val TAG = "ProxyFragment" + + private lateinit var isProxyRunningSwitch: SwitchPreferenceCompat + + private lateinit var isProxyAutoCheck: CheckBoxPreference + private lateinit var proxyAutoUrlText: EditTextPreference + private lateinit var proxyHostText: EditTextPreference + private lateinit var proxyPortText: EditTextPreference + private lateinit var proxyTypeList: ListPreference + private lateinit var dnsText: EditTextPreference + + private lateinit var isAuthCheck: CheckBoxPreference + private lateinit var authUsernameText: EditTextPreference + private lateinit var authPasswordText: EditTextPreference + + private lateinit var isTargetGlobalCheck: CheckBoxPreference + private lateinit var targetApps: Preference + private lateinit var isTargetBypassModeCheck: CheckBoxPreference + + val appSelectActivityLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result: ActivityResult? -> + if (result?.resultCode == Activity.RESULT_OK) { + val data = result.data + val selectedApps = data?.getStringArrayListExtra("selectedApps") + val targetAppsEdit = targetApps!!.sharedPreferences?.edit()!! + targetAppsEdit.putStringSet("target_apps", selectedApps?.toSet()) + targetAppsEdit.apply() + targetApps.summaryProvider = targetApps.summaryProvider + } + } + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.setting_proxy, rootKey) + + isProxyRunningSwitch = findPreference("proxy_running")!! + + isProxyAutoCheck = findPreference("proxy_auto")!! + proxyAutoUrlText = findPreference("proxy_pac_url")!! + proxyHostText = findPreference("proxy_host")!! + proxyPortText = findPreference("proxy_port")!! + proxyTypeList = findPreference("proxy_type")!! + dnsText = findPreference("proxy_dns")!! + + isAuthCheck = findPreference("auth_enable")!! + authUsernameText = findPreference("auth_username")!! + authPasswordText = findPreference("auth_password")!! + + isTargetGlobalCheck = findPreference("target_global")!! + targetApps = findPreference("target_apps")!! + isTargetBypassModeCheck = findPreference("target_bypass_mode")!! + + // App select + targetApps.setOnPreferenceClickListener { + val intent = Intent(context, AppSelectActivity::class.java) + val selectedApps = it.sharedPreferences!!.getStringSet("target_apps", setOf()) + intent.putExtra("selectedApps", ArrayList(selectedApps!!)) + appSelectActivityLauncher.launch(intent) + true + } + + targetApps.summaryProvider = Preference.SummaryProvider { preference -> + getTargetAppsSummary(preference) + } + + val isProxyAuto = findPreference("proxy_auto")!!.sharedPreferences!!.getBoolean("proxy_auto", false) + controlProxyAuto(isProxyAuto) + } + + override fun onResume() { + super.onResume() + preferenceScreen.sharedPreferences + ?.registerOnSharedPreferenceChangeListener(this); + } + + override fun onPause() { + super.onPause() + preferenceScreen.sharedPreferences + ?.unregisterOnSharedPreferenceChangeListener(this) + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + when (key) { + "proxy_running" -> { + val isProxyRunning = sharedPreferences!!.getBoolean("proxy_running", false) + if (isProxyRunning) { + disableAll(); + if (!ProxySingleton.isConnecting) serviceStart() + } else { + enableAll(); + if (!ProxySingleton.isConnecting) serviceStop() + } + } + "proxy_auto" -> { + val isProxyAuto = sharedPreferences!!.getBoolean("proxy_auto", false) + controlProxyAuto(isProxyAuto) + } + } + } + + private fun getTargetAppsSummary(preference: Preference): String { + val selectedApps = preference.sharedPreferences?.getStringSet("target_apps", setOf()) + val selectedSize = selectedApps?.size ?: 0 + return if (selectedSize > 0) + "You have already selected $selectedSize app" + else + "Not select any app yet" + } + + private fun controlProxyAuto(isProxyAuto: Boolean) { + proxyAutoUrlText.isEnabled = isProxyAuto + proxyHostText.isEnabled = !isProxyAuto + proxyPortText.isEnabled = !isProxyAuto + } + + private fun disableAll() { + isProxyAutoCheck.isEnabled = false + proxyAutoUrlText.isEnabled = false + proxyHostText.isEnabled = false + proxyPortText.isEnabled = false + proxyTypeList.isEnabled = false + dnsText.isEnabled = false + + isAuthCheck.isEnabled = false + authUsernameText.isEnabled = false + authPasswordText.isEnabled = false + + isTargetGlobalCheck.isEnabled = false + targetApps.isEnabled = false + isTargetBypassModeCheck.isEnabled = false + } + + private fun enableAll() { + isProxyAutoCheck.isEnabled = false + proxyAutoUrlText.isEnabled = false + proxyHostText.isEnabled = true + proxyPortText.isEnabled = true + proxyTypeList.isEnabled = true + dnsText.isEnabled = true + + isAuthCheck.isEnabled = true + authUsernameText.isEnabled = true + authPasswordText.isEnabled = true + + isTargetGlobalCheck.isEnabled = true + targetApps.isEnabled = true + isTargetBypassModeCheck.isEnabled = true + + } + + private fun serviceStart() { + if (ProxyService.isServiceStarted()) return + val settings: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity()) + val proxyProfile = ProxyProfile.fromSharedPref(settings) + try { + val it = Intent(requireActivity(), ProxyService::class.java) + it.putExtras(proxyProfile.toBundle()) + requireActivity().startForegroundService(it) + } catch (e: Exception) { + Log.e(TAG, "serviceStart: ${e.message}") + } + } + + private fun serviceStop() { + if (!ProxyService.isServiceStarted()) return + try { + requireActivity().stopService(Intent(requireActivity(), ProxyService::class.java)) + } catch (e: Exception) { + Log.e(TAG, "serviceStop: ${e.message}") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/cc/ggez/ezhz/module/proxy/ProxyProfile.kt b/app/src/main/java/cc/ggez/ezhz/module/proxy/ProxyProfile.kt new file mode 100644 index 0000000..4e00667 --- /dev/null +++ b/app/src/main/java/cc/ggez/ezhz/module/proxy/ProxyProfile.kt @@ -0,0 +1,217 @@ +package cc.ggez.ezhz.module.proxy + +import android.content.SharedPreferences +import android.os.Bundle +import org.json.JSONObject +import java.io.Serializable +import java.net.InetAddress +import java.util.Vector +import java.util.regex.Pattern + + +class ProxyProfile : Serializable { + var isProxyRunning: Boolean = false + + var isProxyAuto: Boolean = false + var pacUrl: String + var host: String + var port: Int = 0 + var proxyType: String + var dns: String + + var isAuth: Boolean = false + var username: String + var password: String + + var isTargetGlobal: Boolean = true + var targetApps: ArrayList + var isTargetBypassMode: Boolean = false + + companion object { + fun fromSharedPref(settings: SharedPreferences): ProxyProfile { + val proxyProfile = ProxyProfile() + + proxyProfile.isProxyRunning = settings.getBoolean("proxy_running", false) + + proxyProfile.isProxyAuto = settings.getBoolean("proxy_auto", false) + proxyProfile.pacUrl = settings.getString("proxy_pac_url", "").toString() + proxyProfile.host = settings.getString("proxy_host", "127.0.0.1").toString() + proxyProfile.port = settings.getString("proxy_port", "1337").toString().toInt() + proxyProfile.proxyType = settings.getString("proxy_type", "http").toString() + proxyProfile.dns = settings.getString("proxy_dns", "1.1.1.1").toString() + proxyProfile.isAuth = settings.getBoolean("auth_enable", false) + proxyProfile.username = settings.getString("auth_username", "").toString() + proxyProfile.password = settings.getString("auth_password", "").toString() + + proxyProfile.isTargetGlobal = settings.getBoolean("target_global", false) + proxyProfile.targetApps = ArrayList(settings.getStringSet("target_apps", setOf())?: setOf()) + proxyProfile.isTargetBypassMode = settings.getBoolean("target_bypass_mode", false) + + return proxyProfile + } + + fun fromBundle(bundle: Bundle): ProxyProfile { + val proxyProfile = ProxyProfile() + + proxyProfile.isProxyRunning = bundle.getBoolean("proxy_running", false) + + proxyProfile.isProxyAuto = bundle.getBoolean("proxy_auto", false) + proxyProfile.pacUrl = bundle.getString("proxy_pac_url", "").toString() + proxyProfile.host = bundle.getString("proxy_host", "127.0.0.1").toString() + proxyProfile.port = bundle.getInt("proxy_port", 1337) + proxyProfile.proxyType = bundle.getString("proxy_type", "http").toString() + proxyProfile.dns = bundle.getString("proxy_dns", "1.1.1.1").toString() + + proxyProfile.isAuth = bundle.getBoolean("auth_enable", false) + proxyProfile.username = bundle.getString("auth_username", "").toString() + proxyProfile.password = bundle.getString("auth_password", "").toString() + + proxyProfile.isTargetGlobal = bundle.getBoolean("target_global", false) + proxyProfile.targetApps = bundle.getStringArrayList("target_apps") ?: ArrayList() + proxyProfile.isTargetBypassMode = bundle.getBoolean("target_bypass_mode", false) + + return proxyProfile + } + + fun validateAddr(ia: String?): String? { + val valid1: Boolean = Pattern.matches( + "[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}/[0-9]{1,2}", + ia + ) + val valid2: Boolean = Pattern.matches( + "[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}", ia + ) + return if (valid1 || valid2) { + ia + } else { + var addrString: String? = null + try { + val addr = InetAddress.getByName(ia) + addrString = addr.hostAddress + } catch (ignore: Exception) { + } + if (addrString != null) { + val valid3: Boolean = Pattern.matches( + "[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}", + addrString + ) + if (!valid3) addrString = null + } + addrString + } + } + + fun decodeAddrs(addrs: String): Array { + val list = addrs.split("\\|".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + val ret = Vector() + for (addr in list) { + val ta = validateAddr(addr) + if (ta != null) ret.add(ta) + } + return ret.toTypedArray() + } + + fun encodeAddrs(addrs: Array): String { + if (addrs.size == 0) return "" + val sb = StringBuilder() + for (addr in addrs) { + val ta = validateAddr(addr) + if (ta != null) sb.append(ta).append("|") + } + return sb.substring(0, sb.length - 1) + } + } + + init { + isProxyRunning = false + + isProxyAuto = false + pacUrl = "" + host = "127.0.0.1" + port = 1337 + proxyType = "http" + dns = "1.1.1.1" + + isAuth = false + username = "" + password = "" + + isTargetGlobal = true + targetApps = ArrayList() + isTargetBypassMode = false + } + + fun toBundle(): Bundle { + val bundle = Bundle() + + bundle.putBoolean("proxy_running", isProxyRunning) + + bundle.putBoolean("proxy_auto", isProxyAuto) + bundle.putString("proxy_pac_url", pacUrl) + bundle.putString("proxy_host", host) + bundle.putInt("proxy_port", port) + bundle.putString("proxy_type", proxyType) + bundle.putString("proxy_dns", dns) + + bundle.putBoolean("auth_enable", isAuth) + bundle.putString("auth_username", username) + bundle.putString("auth_password", password) + + bundle.putBoolean("target_global", isTargetGlobal) + bundle.putStringArrayList("target_apps", targetApps) + bundle.putBoolean("target_bypass_mode", isTargetBypassMode) + + return bundle + } + + fun toSharedPref(settings: SharedPreferences) { + val ed = settings.edit() + + ed.putBoolean("proxy_running", isProxyRunning) + + ed.putBoolean("proxy_auto", isProxyAuto) + ed.putString("proxy_pac_url", pacUrl) + ed.putString("proxy_host", host) + ed.putString("proxy_port", port.toString()) + ed.putString("proxy_type", proxyType) + ed.putString("proxy_dns", dns) + + ed.putBoolean("auth_enable", isAuth) + ed.putString("auth_username", username) + ed.putString("auth_password", password) + + ed.putBoolean("target_global", isTargetGlobal) + ed.putBoolean("target_bypass_mode", isTargetBypassMode) + ed.putStringSet("target_apps", targetApps.toSet()) + ed.apply() + } + + override fun toString(): String { + return toJson().toString() + } + + fun toJson(): JSONObject { + val obj = JSONObject() + + obj.put("proxy_running", isProxyRunning) + + obj.put("proxy_auto", isProxyAuto) + obj.put("proxy_pac_url", pacUrl) + obj.put("proxy_host", host) + obj.put("proxy_port", port) + obj.put("proxy_type", proxyType) + obj.put("proxy_dns", dns) + + obj.put("auth_enable", isAuth) + obj.put("auth_username", username) + obj.put("auth_password", password) + + obj.put("target_global", isTargetGlobal) + obj.put("target_apps", targetApps) + obj.put("target_bypass_mode", isTargetBypassMode) + + return obj + } + +} \ No newline at end of file diff --git a/app/src/main/java/cc/ggez/ezhz/module/proxy/ProxyService.kt b/app/src/main/java/cc/ggez/ezhz/module/proxy/ProxyService.kt new file mode 100644 index 0000000..18a5bee --- /dev/null +++ b/app/src/main/java/cc/ggez/ezhz/module/proxy/ProxyService.kt @@ -0,0 +1,325 @@ +package cc.ggez.ezhz.module.proxy + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Intent +import android.content.SharedPreferences +import android.content.SharedPreferences.Editor +import android.media.AudioManager +import android.os.Build +import android.os.Handler +import android.os.IBinder +import android.os.Looper +import android.util.Log +import android.widget.Toast +import androidx.core.app.NotificationCompat +import androidx.preference.PreferenceManager +import cc.ggez.ezhz.MainActivity +import cc.ggez.ezhz.R +import cc.ggez.ezhz.module.frida.FridaService +import com.topjohnwu.superuser.Shell +import java.lang.ref.WeakReference + +class ProxyService : Service() { + + companion object { + private const val TAG = "ProxyService" + + private const val MSG_CONNECT_START = 0 + private const val MSG_CONNECT_FINISH = 1 + private const val MSG_CONNECT_SUCCESS = 2 + private const val MSG_CONNECT_FAIL = 3 + private const val MSG_CONNECT_PAC_ERROR = 4 + private const val MSG_CONNECT_RESOLVE_ERROR = 5 + + const val CMD_IPTABLES_RETURN = "iptables -t nat -A OUTPUT -p tcp -d 0.0.0.0 -j RETURN\n" + + const val CMD_IPTABLES_REDIRECT_ADD_HTTP = + ("iptables -t nat -A OUTPUT -p tcp --dport 80 -j REDIRECT --to 8123\n" + + "iptables -t nat -A OUTPUT -p tcp --dport 443 -j REDIRECT --to 8123\n" + + "iptables -t nat -A OUTPUT -p tcp --dport 8443 -j REDIRECT --to 8123\n" + + "iptables -t nat -A OUTPUT -p tcp --dport 5228 -j REDIRECT --to 8123\n" + + "iptables -t nat -A OUTPUT -p udp --dport 443 -j REDIRECT --to 8124\n" + + "iptables -t nat -A OUTPUT -p udp --dport 8443 -j REDIRECT --to 8124\n" + + "iptables -t nat -A OUTPUT -p udp --dport 5228 -j REDIRECT --to 8124\n") + + const val CMD_IPTABLES_DNAT_ADD_HTTP = + ("iptables -t nat -A OUTPUT -p tcp --dport 80 -j DNAT --to-destination 127.0.0.1:8123\n" + + "iptables -t nat -A OUTPUT -p tcp --dport 443 -j DNAT --to-destination 127.0.0.1:8123\n" + + "iptables -t nat -A OUTPUT -p tcp --dport 8443 -j DNAT --to-destination 127.0.0.1:8123\n" + + "iptables -t nat -A OUTPUT -p tcp --dport 5228 -j DNAT --to-destination 127.0.0.1:8123\n" + + "iptables -t nat -A OUTPUT -p udp --dport 443 -j DNAT --to-destination 127.0.0.1:8124\n" + + "iptables -t nat -A OUTPUT -p udp --dport 8443 -j DNAT --to-destination 127.0.0.1:8124\n" + + "iptables -t nat -A OUTPUT -p udp --dport 5228 -j DNAT --to-destination 127.0.0.1:8124\n") + + const val CMD_IPTABLES_REDIRECT_ADD_SOCKS = + "iptables -t nat -A OUTPUT -p tcp -j REDIRECT --to 8123\n" + + const val CMD_IPTABLES_DNAT_ADD_SOCKS = + "iptables -t nat -A OUTPUT -p tcp -j DNAT --to-destination 127.0.0.1:8123\n" + + private var sRunningInstance: WeakReference? = null + fun isServiceStarted(): Boolean { + val isServiceStarted: Boolean + if (sRunningInstance == null) { + isServiceStarted = false + } else if (sRunningInstance!!.get() == null) { + isServiceStarted = false + sRunningInstance = null + } else { + isServiceStarted = true + } + return isServiceStarted + } + } + + lateinit var basePath: String + private val notificationManager: NotificationManager by lazy { + getSystemService(NOTIFICATION_SERVICE) as NotificationManager + } + private lateinit var settings: SharedPreferences + private lateinit var pendIntent: PendingIntent + private lateinit var pStopSelf: PendingIntent + private lateinit var proxyProfile: ProxyProfile + + val handler = Handler(Looper.getMainLooper()) { msg -> + val ed = settings.edit() + when (msg.what) { + MSG_CONNECT_START -> { + ed.putBoolean("is_connecting", true) + ProxySingleton.isConnecting = true + } + + MSG_CONNECT_FINISH -> { + ed.putBoolean("is_connecting", false) + ProxySingleton.isConnecting = false + } + + MSG_CONNECT_SUCCESS -> ed.putBoolean("proxy_running", true) + MSG_CONNECT_FAIL -> ed.putBoolean("proxy_running", false) + MSG_CONNECT_PAC_ERROR -> Toast.makeText( + this@ProxyService, + R.string.msg_pac_error, + Toast.LENGTH_SHORT + ).show() + + MSG_CONNECT_RESOLVE_ERROR -> Toast.makeText( + this@ProxyService, R.string.msg_resolve_error, + Toast.LENGTH_SHORT + ).show() + } + ed.apply() + true + } + + private fun markServiceStarted() { + sRunningInstance = WeakReference(this) + } + + private fun markServiceStopped() { + sRunningInstance = null + } + + override fun onBind(intent: Intent?): IBinder? { + return null + } + + override fun onCreate() { + Log.d(TAG, "Service Proxy Create"); + super.onCreate() + basePath = filesDir.absolutePath; + settings = PreferenceManager.getDefaultSharedPreferences(this) + + createNotificationChannel() + + val intent = Intent(this, MainActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP + intent.putExtra("menu", "proxy") + pendIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_MUTABLE) + } else { + PendingIntent.getActivity( + this, + 0, + intent, + PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE + ) + } + val stopIntent = Intent(this, ProxyService::class.java) + stopIntent.action = "STOP" + pStopSelf = PendingIntent.getForegroundService(this, 0, stopIntent, + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE) + notifyAlert( + getString(R.string.app_name) + " | Proxy Module", + getString(R.string.service_running) + ) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + Log.d("xxx", intent?.action.toString()) + if(intent?.action.equals("STOP")) { + stopSelf() + return super.onStartCommand(intent, flags, startId) + } + + if (intent?.extras == null) { + return super.onStartCommand(intent, flags, startId) + } + + Log.d(TAG, "Service Start"); + + proxyProfile = ProxyProfile.fromBundle(intent.extras!!) + + Thread { + handler.sendEmptyMessage(MSG_CONNECT_START) + if (handleCommand()) { + // Connection and forward successful + handler.sendEmptyMessage(MSG_CONNECT_SUCCESS) + } else { + // Connection or forward unsuccessful + stopSelf() + handler.sendEmptyMessage(MSG_CONNECT_FAIL) + } + handler.sendEmptyMessage(MSG_CONNECT_FINISH) + }.start() + + markServiceStarted() + + return super.onStartCommand(intent, flags, startId) + } + + override fun onDestroy() { + Log.d(TAG, "Service Stop"); + ProxySingleton.isConnecting = true + notificationManager.cancelAll(); + stopForeground(STOP_FOREGROUND_DETACH); + super.onDestroy() + + onDisconnect() + + val ed: Editor = settings.edit() + ed.putBoolean("proxy_running", false) + ed.apply() + + markServiceStopped() + ProxySingleton.isConnecting = false + } + + private fun onDisconnect() { + val sb = StringBuilder() + sb.append("iptables -t nat -F OUTPUT\n") + sb.append("kill -9 `cat ${basePath}/gost_tcp.pid`\n") + sb.append("kill -9 `cat ${basePath}/gost_udp.pid`\n") + sb.append("kill -9 `cat ${basePath}/gost_dns.pid`\n") + Shell.cmd(sb.toString()).exec() + } + + private fun handleCommand(): Boolean { + Shell.cmd("chmod +x $basePath/gost").exec() + + try { + val u: String = ProxySingleton.preserve(proxyProfile.username) + val p: String = ProxySingleton.preserve(proxyProfile.password) + val srcTcp = "-L=red://127.0.0.1:8123?sniffing=true" + val srcUdp = "-L=redu://127.0.0.1:8124?ttl=30s" + val srcDns = "-L=dns://:53/${proxyProfile.dns}" + var auth = "" + if (u.isNotEmpty() && p.isNotEmpty()) { + auth = "$u:$p" + } + val dstTcp = "-F=${proxyProfile.proxyType}://$auth@${proxyProfile.host}:${proxyProfile.port}" + val dstUdp = "-F=relay://${proxyProfile.host}:${proxyProfile.port}" + + // Start gost tcp here + Shell.cmd("$basePath/gost $srcTcp $dstTcp &> $basePath/gost_tcp.log &\n echo $! > $basePath/gost_tcp.pid").exec() + Shell.cmd("$basePath/gost $srcUdp $dstUdp &> $basePath/gost_udp.log &\n echo $! > $basePath/gost_udp.pid").exec() + Shell.cmd("$basePath/gost $srcDns &> $basePath/gost_dns.log &\n echo $! > $basePath/gost_dns.pid").exec() + + val cmd = StringBuilder() + cmd.append(CMD_IPTABLES_RETURN.replace("0.0.0.0", proxyProfile.host)) + + var redirectCmd = CMD_IPTABLES_REDIRECT_ADD_HTTP + var dnatCmd = CMD_IPTABLES_DNAT_ADD_HTTP + + if (proxyProfile.proxyType.equals("socks4") || proxyProfile.proxyType.equals("socks5")) { + redirectCmd = CMD_IPTABLES_REDIRECT_ADD_SOCKS + dnatCmd = CMD_IPTABLES_DNAT_ADD_SOCKS + } + + if (proxyProfile.isTargetGlobal) { + cmd.append(if (ProxySingleton.isRedirectSupport == 1) redirectCmd else dnatCmd) + } + else if (proxyProfile.isTargetBypassMode) { + // for host specified apps + for (app in proxyProfile.targetApps) { + val appData = app.split(":") + cmd.append( + CMD_IPTABLES_RETURN.replace("-d 0.0.0.0", "").replace( + "-t nat", + "-t nat -m owner --uid-owner ${appData[1]}" + ) + ) + } + + cmd.append(if (ProxySingleton.isRedirectSupport == 1) redirectCmd else dnatCmd) + } else { + for (app in proxyProfile.targetApps) { + val appData = app.split(":") + cmd.append( + (if (ProxySingleton.isRedirectSupport == 1) redirectCmd else dnatCmd).replace( + "-t nat", + "-t nat -m owner --uid-owner ${appData[1]}" + ) + ) + } + } + + Shell.cmd(cmd.toString()).exec() + } catch (e: Exception) { + Log.e(TAG, "Error setting up port forward during connect", e) + } + + return true + } + + private fun createNotificationChannel() { + val name: CharSequence = "EZHZ Proxy Service" + val description = "EZHZ Proxy Background Service" + val importance = NotificationManager.IMPORTANCE_DEFAULT + val channel = NotificationChannel("EZHZ Proxy Service", name, importance) + channel.description = description + notificationManager.createNotificationChannel(channel) + } + + private fun initSoundVibrateLights(builder: NotificationCompat.Builder) { + val audioManager = this.getSystemService(AUDIO_SERVICE) as AudioManager + if (audioManager.getStreamVolume(AudioManager.STREAM_RING) == 0) { + builder.setSound(null) + } + + builder.setVibrate(longArrayOf(0, 1000, 500, 1000, 500, 1000)) + } + + private fun notifyAlert(title: String, info: String) { + val notiTitle = "${getString(R.string.app_name)} | Proxy Module" + val builder = NotificationCompat.Builder(this, "Service") + initSoundVibrateLights(builder) + builder.setAutoCancel(false) + builder.setTicker(notiTitle) + builder.setContentTitle(title) + builder.setContentText(info) + builder.setSmallIcon(R.drawable.ic_proxy) + builder.setContentIntent(pendIntent) + builder.addAction( + android.R.drawable.ic_lock_power_off, + getString(R.string.service_stop), + pStopSelf) + builder.priority = NotificationCompat.PRIORITY_DEFAULT + builder.setOngoing(true) + builder.setChannelId("EZHZ Proxy Service") + + startForeground(1, builder.build()) + } +} \ No newline at end of file diff --git a/app/src/main/java/cc/ggez/ezhz/module/proxy/ProxySingleton.kt b/app/src/main/java/cc/ggez/ezhz/module/proxy/ProxySingleton.kt new file mode 100644 index 0000000..ece07ab --- /dev/null +++ b/app/src/main/java/cc/ggez/ezhz/module/proxy/ProxySingleton.kt @@ -0,0 +1,39 @@ +package cc.ggez.ezhz.module.proxy + +import com.topjohnwu.superuser.Shell + + +class ProxySingleton { + companion object { + var isConnecting = false + var isRedirectSupport = -1 + get() { + if (field == -1) initHasRedirectSupported() + return field + } + + fun initHasRedirectSupported() { + val sb = StringBuilder() + val command = "iptables -t nat -A OUTPUT -p udp --dport 54 -j REDIRECT --to 8154" + val result = Shell.cmd(command).exec() + val lines = result.out + result.err + isRedirectSupport = 1 + + // flush the check command + Shell.cmd(command.replace("-A", "-D")) + + if (lines.contains("No chain/target/match")) { + isRedirectSupport = 0 + } + } + + fun preserve(s: String): String { + val sb = java.lang.StringBuilder() + for (element in s) { + if (element == '\\' || element == '$' || element == '`' || element == '"') sb.append('\\') + sb.append(element) + } + return sb.toString() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/cc/ggez/ezhz/ui/proxy/ProxyViewModel.kt b/app/src/main/java/cc/ggez/ezhz/module/proxy/ProxyViewModel.kt similarity index 89% rename from app/src/main/java/cc/ggez/ezhz/ui/proxy/ProxyViewModel.kt rename to app/src/main/java/cc/ggez/ezhz/module/proxy/ProxyViewModel.kt index 08fdd2a..2cc595e 100644 --- a/app/src/main/java/cc/ggez/ezhz/ui/proxy/ProxyViewModel.kt +++ b/app/src/main/java/cc/ggez/ezhz/module/proxy/ProxyViewModel.kt @@ -1,4 +1,4 @@ -package cc.ggez.ezhz.ui.proxy +package cc.ggez.ezhz.module.proxy import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData diff --git a/app/src/main/java/cc/ggez/ezhz/ui/sharedpref/SharedPrefFragment.kt b/app/src/main/java/cc/ggez/ezhz/module/sharedpref/SharedPrefFragment.kt similarity index 96% rename from app/src/main/java/cc/ggez/ezhz/ui/sharedpref/SharedPrefFragment.kt rename to app/src/main/java/cc/ggez/ezhz/module/sharedpref/SharedPrefFragment.kt index 4670f00..0241a1e 100644 --- a/app/src/main/java/cc/ggez/ezhz/ui/sharedpref/SharedPrefFragment.kt +++ b/app/src/main/java/cc/ggez/ezhz/module/sharedpref/SharedPrefFragment.kt @@ -1,4 +1,4 @@ -package cc.ggez.ezhz.ui.sharedpref +package cc.ggez.ezhz.module.sharedpref import android.os.Bundle import android.view.LayoutInflater diff --git a/app/src/main/java/cc/ggez/ezhz/ui/sharedpref/SharedPrefViewModel.kt b/app/src/main/java/cc/ggez/ezhz/module/sharedpref/SharedPrefViewModel.kt similarity index 74% rename from app/src/main/java/cc/ggez/ezhz/ui/sharedpref/SharedPrefViewModel.kt rename to app/src/main/java/cc/ggez/ezhz/module/sharedpref/SharedPrefViewModel.kt index de9f48e..33e74b5 100644 --- a/app/src/main/java/cc/ggez/ezhz/ui/sharedpref/SharedPrefViewModel.kt +++ b/app/src/main/java/cc/ggez/ezhz/module/sharedpref/SharedPrefViewModel.kt @@ -1,4 +1,4 @@ -package cc.ggez.ezhz.ui.sharedpref +package cc.ggez.ezhz.module.sharedpref import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -7,7 +7,7 @@ import androidx.lifecycle.ViewModel class SharedPrefViewModel : ViewModel() { private val _text = MutableLiveData().apply { - value = "This is shared pref Fragment" + value = "SharedPref Modifier Coming Soon" } val text: LiveData = _text } \ No newline at end of file diff --git a/app/src/main/java/cc/ggez/ezhz/ui/appselect/AppSelectActivity.kt b/app/src/main/java/cc/ggez/ezhz/ui/appselect/AppSelectActivity.kt deleted file mode 100644 index cb9e3fe..0000000 --- a/app/src/main/java/cc/ggez/ezhz/ui/appselect/AppSelectActivity.kt +++ /dev/null @@ -1,27 +0,0 @@ -package cc.ggez.ezhz.ui.appselect - -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity -import androidx.preference.PreferenceFragmentCompat -import cc.ggez.ezhz.R - -class AppSelectActivity : AppCompatActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.settings_activity) - if (savedInstanceState == null) { - supportFragmentManager - .beginTransaction() - .replace(R.id.settings, SettingsFragment()) - .commit() - } - supportActionBar?.setDisplayHomeAsUpEnabled(true) - } - - class SettingsFragment : PreferenceFragmentCompat() { - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - setPreferencesFromResource(R.xml.root_preferences, rootKey) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/cc/ggez/ezhz/ui/appselect/AppSelectService.kt b/app/src/main/java/cc/ggez/ezhz/ui/appselect/AppSelectService.kt deleted file mode 100644 index 18aaa36..0000000 --- a/app/src/main/java/cc/ggez/ezhz/ui/appselect/AppSelectService.kt +++ /dev/null @@ -1,51 +0,0 @@ -package cc.ggez.ezhz.ui.appselect - -import android.content.Context -import android.content.SharedPreferences -import android.content.pm.ApplicationInfo -import android.content.pm.PackageManager -import android.os.Build -import java.util.Arrays -import java.util.StringTokenizer -import java.util.Vector - - -class AppSelectService { - companion object { - fun getApps(context: Context) { - - val pm = context.packageManager - val appInfos = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - pm.getInstalledApplications(PackageManager.ApplicationInfoFlags.of(0L)) - } else { - pm.getInstalledApplications(0) - } - - val itAppInfo: Iterator = lAppInfo.iterator() - var aInfo: ApplicationInfo - while (itAppInfo.hasNext()) { - aInfo = itAppInfo.next() - - // ignore system apps - if (aInfo.uid < 10000) continue - if (aInfo.processName == null) continue - if (pMgr.getApplicationLabel(aInfo) == null || pMgr.getApplicationLabel(aInfo) - .toString() == "" - ) continue - if (pMgr.getApplicationIcon(aInfo) == null) continue - val tApp = ProxyedApp() - tApp.setEnabled(aInfo.enabled) - tApp.setUid(aInfo.uid) - tApp.setUsername(pMgr.getNameForUid(tApp.getUid())) - tApp.setProcname(aInfo.processName) - tApp.setName(pMgr.getApplicationLabel(aInfo).toString()) - - // check if this application is allowed - tApp.setProxyed(Arrays.binarySearch(tordApps, tApp.getUsername()) >= 0) - vectorApps.add(tApp) - } - apps = arrayOfNulls(vectorApps.size) - vectorApps.toArray(apps) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/cc/ggez/ezhz/ui/frida/FridaFragment.kt b/app/src/main/java/cc/ggez/ezhz/ui/frida/FridaFragment.kt deleted file mode 100644 index d8a88a8..0000000 --- a/app/src/main/java/cc/ggez/ezhz/ui/frida/FridaFragment.kt +++ /dev/null @@ -1,42 +0,0 @@ -package cc.ggez.ezhz.ui.frida - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider -import cc.ggez.ezhz.databinding.FragmentFridaBinding - -class FridaFragment : Fragment() { - - private var _binding: FragmentFridaBinding? = null - - // This property is only valid between onCreateView and - // onDestroyView. - private val binding get() = _binding!! - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - val fridaViewModel = - ViewModelProvider(this)[FridaViewModel::class.java] - - _binding = FragmentFridaBinding.inflate(inflater, container, false) - val root: View = binding.root - - val textView: TextView = binding.textDashboard - fridaViewModel.text.observe(viewLifecycleOwner) { - textView.text = it - } - return root - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } -} \ No newline at end of file diff --git a/app/src/main/java/cc/ggez/ezhz/ui/frida/FridaViewModel.kt b/app/src/main/java/cc/ggez/ezhz/ui/frida/FridaViewModel.kt deleted file mode 100644 index da761c3..0000000 --- a/app/src/main/java/cc/ggez/ezhz/ui/frida/FridaViewModel.kt +++ /dev/null @@ -1,13 +0,0 @@ -package cc.ggez.ezhz.ui.frida - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel - -class FridaViewModel : ViewModel() { - - private val _text = MutableLiveData().apply { - value = "This is frida Fragment" - } - val text: LiveData = _text -} \ No newline at end of file diff --git a/app/src/main/java/cc/ggez/ezhz/ui/proxy/ProxyFragment.kt b/app/src/main/java/cc/ggez/ezhz/ui/proxy/ProxyFragment.kt deleted file mode 100644 index 50fd2f4..0000000 --- a/app/src/main/java/cc/ggez/ezhz/ui/proxy/ProxyFragment.kt +++ /dev/null @@ -1,11 +0,0 @@ -package cc.ggez.ezhz.ui.proxy - -import android.os.Bundle -import androidx.preference.PreferenceFragmentCompat -import cc.ggez.ezhz.R - -class ProxyFragment : PreferenceFragmentCompat() { - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - setPreferencesFromResource(R.xml.setting_proxy, rootKey) - } -} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_correct.xml b/app/src/main/res/drawable/ic_correct.xml new file mode 100644 index 0000000..a2cbc93 --- /dev/null +++ b/app/src/main/res/drawable/ic_correct.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_proxy.xml b/app/src/main/res/drawable/ic_proxy.xml index d649c1c..44b90bc 100644 --- a/app/src/main/res/drawable/ic_proxy.xml +++ b/app/src/main/res/drawable/ic_proxy.xml @@ -1,60 +1,27 @@ + android:width="682dp" + android:height="682dp" + android:viewportWidth="682.67" + android:viewportHeight="682"> + android:fillColor="#FF000000" + android:pathData="m442.14,225.06h120.55c-7.84,-93.6 -73.88,-170.9 -161.71,-195.51 25.25,45.65 39.17,119.75 41.16,195.51zM442.14,225.06"/> + android:fillColor="#FF000000" + android:pathData="m404.63,225.06c-1.48,-55.3 -9.5,-106.49 -22.94,-145.75 -13.73,-40.05 -30.33,-57.98 -40.69,-57.98s-26.96,17.93 -40.69,57.98c-13.44,39.26 -21.46,90.45 -22.94,145.75zM404.63,225.06"/> + android:fillColor="#FF000000" + android:pathData="m281.02,29.55c-87.84,24.61 -153.87,101.91 -161.71,195.51h120.55c1.99,-75.76 15.91,-149.86 41.16,-195.51zM281.02,29.55"/> + android:fillColor="#FF000000" + android:pathData="m562.69,262.56h-120.55c-1.99,75.76 -15.91,149.86 -41.16,195.51 87.84,-24.61 153.87,-101.91 161.71,-195.51zM562.69,262.56"/> + android:fillColor="#FF000000" + android:pathData="m381.69,408.3c13.44,-39.26 21.46,-90.45 22.94,-145.75h-127.25c1.48,55.3 9.5,106.48 22.94,145.75 13.73,40.05 30.33,57.98 40.69,57.98s26.96,-17.93 40.69,-57.98zM381.69,408.3"/> + android:fillColor="#FF000000" + android:pathData="m399.96,580.66c-6.08,-19.06 -21.15,-34.14 -40.21,-40.21v-37.34c-6.2,0.45 -12.45,0.68 -18.75,0.68s-12.55,-0.23 -18.75,-0.68v37.34c-19.06,6.07 -34.14,21.15 -40.21,40.21h-261.04v37.5h261.04c7.95,24.98 31.39,43.13 58.96,43.13s51.01,-18.15 58.96,-43.13h261.04v-37.5zM399.96,580.66"/> - - - - - - - - - - - + android:fillColor="#FF000000" + android:pathData="m119.31,262.56c7.84,93.6 73.88,170.9 161.71,195.51 -25.25,-45.65 -39.17,-119.75 -41.16,-195.51zM119.31,262.56"/> diff --git a/app/src/main/res/layout/activity_splash.xml b/app/src/main/res/layout/activity_splash.xml new file mode 100644 index 0000000..5825a84 --- /dev/null +++ b/app/src/main/res/layout/activity_splash.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/app_select_activity.xml b/app/src/main/res/layout/app_select_activity.xml new file mode 100644 index 0000000..c2e80cf --- /dev/null +++ b/app/src/main/res/layout/app_select_activity.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_download.xml b/app/src/main/res/layout/dialog_download.xml new file mode 100644 index 0000000..19e6f43 --- /dev/null +++ b/app/src/main/res/layout/dialog_download.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_frida.xml b/app/src/main/res/layout/fragment_frida.xml index a4edeac..4312611 100644 --- a/app/src/main/res/layout/fragment_frida.xml +++ b/app/src/main/res/layout/fragment_frida.xml @@ -4,18 +4,16 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".ui.frida.FridaFragment"> + tools:context=".module.frida.FridaFragment"> - + android:layout_height="match_parent" + app:layout_constraintTop_toTopOf="parent" + tools:itemCount="5" + tools:listitem="@layout/item_frida" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + app:layout_constraintStart_toStartOf="parent"/> + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_shared_pref.xml b/app/src/main/res/layout/fragment_shared_pref.xml index 393a7a6..71fe5b9 100644 --- a/app/src/main/res/layout/fragment_shared_pref.xml +++ b/app/src/main/res/layout/fragment_shared_pref.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".ui.sharedpref.SharedPrefFragment"> + tools:context=".module.sharedpref.SharedPrefFragment"> + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_frida.xml b/app/src/main/res/layout/item_frida.xml new file mode 100644 index 0000000..acf8671 --- /dev/null +++ b/app/src/main/res/layout/item_frida.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/settings_activity.xml b/app/src/main/res/layout/settings_activity.xml deleted file mode 100644 index de6591a..0000000 --- a/app/src/main/res/layout/settings_activity.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/manu_app_select.xml b/app/src/main/res/menu/manu_app_select.xml new file mode 100644 index 0000000..34a61f6 --- /dev/null +++ b/app/src/main/res/menu/manu_app_select.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/manu_frida_select.xml b/app/src/main/res/menu/manu_frida_select.xml new file mode 100644 index 0000000..f13c551 --- /dev/null +++ b/app/src/main/res/menu/manu_frida_select.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index 7198239..c962d98 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -7,18 +7,18 @@ \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 0a25a0d..2697114 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,10 +1,11 @@ - #7FAEF7 - #427BD2 - #000732 - #d24287 - #932e5f + #FF7FAEF7 + #FF427BD2 + #FF000732 + #FFd24287 + #FF932e5f + #FF666666 #FF000000 #FFFFFFFF \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1a6282e..542a739 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,7 +3,7 @@ Proxy Frida Shared Preferences - AppSelectActivity + App Select Activity Messages @@ -19,4 +19,22 @@ Automatically download attachments for incoming emails Only download attachments when manually requested + App Name + package.name + App Icon + User + System + Save + Reload + Cannot connect to proxy by using PAC URL + Cannot resolve proxy hostname + The service is running + Execute + Kill + Install + Uninstall + Frida Server v%s + Download Frida Server + This may take a moment. + Stop Service \ No newline at end of file diff --git a/app/src/main/res/xml/setting_proxy.xml b/app/src/main/res/xml/setting_proxy.xml index a6f6e29..a7b2138 100644 --- a/app/src/main/res/xml/setting_proxy.xml +++ b/app/src/main/res/xml/setting_proxy.xml @@ -1,15 +1,13 @@ - - - + app:enabled="false" + app:title="Auto Proxy with PAC" /> @@ -70,12 +80,16 @@ app:key="auth_username" app:summary="Proxy's username" app:iconSpaceReserved="false" + app:useSimpleSummaryProvider="true" + app:dependency="auth_enable" app:title="Username" /> @@ -89,19 +103,25 @@ app:key="target_global" app:iconSpaceReserved="false" app:summary="Enable the global proxy" + app:disableDependentsState="true" + app:defaultValue="true" app:title="Global Proxy" /> + + app:dependency="target_global" + app:title="Apps Proxy"> + diff --git a/settings.gradle.kts b/settings.gradle.kts index d995ed2..99013f5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,6 +3,7 @@ pluginManagement { google() mavenCentral() gradlePluginPortal() + } } dependencyResolutionManagement { @@ -10,6 +11,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven("https://jitpack.io") } }