read reflection data from json
This commit is contained in:
parent
c11101b46a
commit
0b92b87bbe
6 changed files with 124 additions and 143 deletions
|
|
@ -2,6 +2,7 @@ import argparse
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import frida # type: ignore
|
import frida # type: ignore
|
||||||
|
|
@ -12,16 +13,16 @@ STACK_CONSUMER_B64 = Path(__file__).parent / "StackConsumer.dex.b64"
|
||||||
|
|
||||||
|
|
||||||
# Define handler to event generated by the scripts
|
# Define handler to event generated by the scripts
|
||||||
def on_message(message, data):
|
def on_message(message, data, data_storage: dict):
|
||||||
if message["type"] == "error":
|
if message["type"] == "error":
|
||||||
print(f"[error] {message['description']}")
|
print(f"[error] {message['description']}")
|
||||||
print(message["stack"])
|
print(message["stack"])
|
||||||
elif message["type"] == "send" and message["payload"]["type"] == "invoke":
|
elif message["type"] == "send" and message["payload"]["type"] == "invoke":
|
||||||
handle_invoke_data(message["payload"]["data"])
|
handle_invoke_data(message["payload"]["data"], data_storage)
|
||||||
elif message["type"] == "send" and message["payload"]["type"] == "class-new-inst":
|
elif message["type"] == "send" and message["payload"]["type"] == "class-new-inst":
|
||||||
handle_class_new_inst_data(message["payload"]["data"])
|
handle_class_new_inst_data(message["payload"]["data"], data_storage)
|
||||||
elif message["type"] == "send" and message["payload"]["type"] == "cnstr-new-isnt":
|
elif message["type"] == "send" and message["payload"]["type"] == "cnstr-new-isnt":
|
||||||
handle_cnstr_new_inst_data(message["payload"]["data"])
|
handle_cnstr_new_inst_data(message["payload"]["data"], data_storage)
|
||||||
else:
|
else:
|
||||||
print("[on_message] message:", message)
|
print("[on_message] message:", message)
|
||||||
|
|
||||||
|
|
@ -52,42 +53,84 @@ def print_stack(stack, prefix: str):
|
||||||
# return f"{cls}->{name}({args}){ret}"
|
# return f"{cls}->{name}({args}){ret}"
|
||||||
|
|
||||||
|
|
||||||
def handle_invoke_data(data):
|
def handle_invoke_data(data, data_storage: dict):
|
||||||
method = data["method"]
|
method = data["method"]
|
||||||
# caller_method = "?" # get_method_id(data["caller_method"])
|
if len(data["stack"]) == 0:
|
||||||
# addr = data["addr"]
|
return
|
||||||
|
caller_method = data["stack"][0]["method"]
|
||||||
|
addr = data["stack"][0]["bytecode_index"]
|
||||||
print("Method.Invoke:")
|
print("Method.Invoke:")
|
||||||
print(f" called: {method}")
|
print(f" called: {method}")
|
||||||
print(f" stack:")
|
print(f" by: {caller_method}")
|
||||||
print_stack(data["stack"], " ")
|
print(f" at: 0x{addr:08x}")
|
||||||
# print(f" by: {caller_method}")
|
# print(f" stack:")
|
||||||
# print(f" at: 0x{addr:08x}")
|
# print_stack(data["stack"], " ")
|
||||||
|
if addr < 0:
|
||||||
|
return
|
||||||
|
data_storage["invoke_data"].append(
|
||||||
|
{
|
||||||
|
"method": method,
|
||||||
|
"caller_method": caller_method,
|
||||||
|
"addr": addr,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def handle_class_new_inst_data(data):
|
def handle_class_new_inst_data(data, data_storage: dict):
|
||||||
constructor = data["constructor"]
|
constructor = data["constructor"]
|
||||||
# caller_method = "?" # get_method_id(data["caller_method"])
|
if len(data["stack"]) == 0:
|
||||||
# addr = data["addr"]
|
return
|
||||||
|
if (
|
||||||
|
data["stack"][0]["method"]
|
||||||
|
!= "Ljava/lang/Class;->newInstance()Ljava/lang/Object;"
|
||||||
|
):
|
||||||
|
frame = data["stack"][0]
|
||||||
|
elif len(data["stack"]) > 1:
|
||||||
|
frame = data["stack"][1]
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
caller_method = frame["method"]
|
||||||
|
addr = frame["bytecode_index"]
|
||||||
print("Class.NewInstance:")
|
print("Class.NewInstance:")
|
||||||
print(f" called: {constructor}")
|
print(f" called: {constructor}")
|
||||||
print(f" stack:")
|
print(f" by: {caller_method}")
|
||||||
print_stack(data["stack"], " ")
|
print(f" at: 0x{addr:08x}")
|
||||||
# print(f" by: {caller_method}")
|
# print(f" stack:")
|
||||||
# print(f" at: 0x{addr:08x}")
|
# print_stack(data["stack"], " ")
|
||||||
|
if addr < 0:
|
||||||
|
return
|
||||||
|
data_storage["class_new_inst_data"].append(
|
||||||
|
{
|
||||||
|
"constructor": constructor,
|
||||||
|
"caller_method": caller_method,
|
||||||
|
"addr": addr,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def handle_cnstr_new_inst_data(data):
|
def handle_cnstr_new_inst_data(data, data_storage: dict):
|
||||||
constructor = data["constructor"]
|
constructor = data["constructor"]
|
||||||
if not constructor.startswith("Lcom/example/theseus"):
|
if not constructor.startswith("Lcom/example/theseus"):
|
||||||
return
|
return
|
||||||
# caller_method = "?" # get_method_id(data["caller_method"])
|
if len(data["stack"]) == 0:
|
||||||
# addr = data["addr"]
|
return
|
||||||
|
caller_method = data["stack"][0]["method"]
|
||||||
|
addr = data["stack"][0]["bytecode_index"]
|
||||||
print("Constructor.newInstance:")
|
print("Constructor.newInstance:")
|
||||||
print(f" called: {constructor}")
|
print(f" called: {constructor}")
|
||||||
print(f" stack:")
|
print(f" by: {caller_method}")
|
||||||
print_stack(data["stack"], " ")
|
print(f" at: 0x{addr:08x}")
|
||||||
# print(f" by: {caller_method}")
|
# print(f" stack:")
|
||||||
# print(f" at: 0x{addr:08x}")
|
# print_stack(data["stack"], " ")
|
||||||
|
if addr < 0:
|
||||||
|
return
|
||||||
|
data_storage["cnstr_new_inst_data"].append(
|
||||||
|
{
|
||||||
|
"constructor": constructor,
|
||||||
|
"caller_method": caller_method,
|
||||||
|
"addr": addr,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
@ -104,6 +147,13 @@ def main():
|
||||||
help="The android device to connect to, eg: 'emulator-5554'",
|
help="The android device to connect to, eg: 'emulator-5554'",
|
||||||
type=str,
|
type=str,
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-o",
|
||||||
|
"--output",
|
||||||
|
default=None,
|
||||||
|
help="where to dump the collected data, default is stdout",
|
||||||
|
type=Path,
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
env = dict(os.environ)
|
env = dict(os.environ)
|
||||||
|
|
||||||
|
|
@ -132,9 +182,15 @@ def main():
|
||||||
session = device.attach(pid)
|
session = device.attach(pid)
|
||||||
script = session.create_script(script)
|
script = session.create_script(script)
|
||||||
|
|
||||||
|
data_storage = {
|
||||||
|
"invoke_data": [],
|
||||||
|
"class_new_inst_data": [],
|
||||||
|
"cnstr_new_inst_data": [],
|
||||||
|
}
|
||||||
|
|
||||||
script.on(
|
script.on(
|
||||||
"message",
|
"message",
|
||||||
on_message,
|
lambda msg, data: on_message(msg, data, data_storage),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Load script
|
# Load script
|
||||||
|
|
@ -144,3 +200,8 @@ def main():
|
||||||
|
|
||||||
print("Press ENTER to finish the analysis")
|
print("Press ENTER to finish the analysis")
|
||||||
input()
|
input()
|
||||||
|
if args.output is None:
|
||||||
|
print(json.dumps(data_storage, indent=" "))
|
||||||
|
else:
|
||||||
|
with args.output.open("w") as fp:
|
||||||
|
json.dump(data_storage, fp)
|
||||||
|
|
|
||||||
2
patcher/Cargo.lock
generated
2
patcher/Cargo.lock
generated
|
|
@ -815,6 +815,8 @@ dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -12,3 +12,5 @@ anyhow = "1.0.95"
|
||||||
clap = { version = "4.5.27", features = ["derive"] }
|
clap = { version = "4.5.27", features = ["derive"] }
|
||||||
env_logger = "0.11.6"
|
env_logger = "0.11.6"
|
||||||
reqwest = { version = "0.12.12", default-features = false, features = ["blocking", "rustls-tls"] }
|
reqwest = { version = "0.12.12", default-features = false, features = ["blocking", "rustls-tls"] }
|
||||||
|
serde = "1.0.217"
|
||||||
|
serde_json = "1.0.138"
|
||||||
|
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
use androscalpel::Apk;
|
|
||||||
use clap::Args;
|
|
||||||
use std::fs::{read_to_string, File};
|
|
||||||
use std::io::Cursor;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use crate::labeling;
|
|
||||||
|
|
||||||
#[derive(Clone, Args, Debug)]
|
|
||||||
pub struct ApkLocation {
|
|
||||||
#[arg(short, long, conflicts_with = "sha256")]
|
|
||||||
pub path: Option<PathBuf>,
|
|
||||||
#[arg(long, conflicts_with = "path", requires = "androzoo_key")]
|
|
||||||
pub sha256: Option<String>,
|
|
||||||
#[command(flatten)]
|
|
||||||
pub androzoo_key: Option<AndrozooKey>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ApkLocation {
|
|
||||||
pub fn get_id(&self) -> String {
|
|
||||||
match self {
|
|
||||||
ApkLocation {
|
|
||||||
path: Some(path), ..
|
|
||||||
} => path.as_path().file_name().unwrap().to_str().unwrap().into(),
|
|
||||||
ApkLocation {
|
|
||||||
sha256: Some(sha256),
|
|
||||||
..
|
|
||||||
} => sha256.clone(),
|
|
||||||
_ => panic!("Invalid ApkLocation"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Args, Debug)]
|
|
||||||
pub struct AndrozooKey {
|
|
||||||
#[arg(long, group = "androzoo_key", conflicts_with = "api_key")]
|
|
||||||
api_key_path: Option<PathBuf>,
|
|
||||||
#[arg(long, group = "androzoo_key", conflicts_with = "api_key_path")]
|
|
||||||
api_key: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AndrozooKey {
|
|
||||||
fn get_key(&self) -> String {
|
|
||||||
match self {
|
|
||||||
AndrozooKey {
|
|
||||||
api_key_path: Some(path),
|
|
||||||
..
|
|
||||||
} => read_to_string(path)
|
|
||||||
.expect("Failed to read key from file")
|
|
||||||
.trim()
|
|
||||||
.to_string(),
|
|
||||||
AndrozooKey {
|
|
||||||
api_key: Some(key), ..
|
|
||||||
} => key.trim().to_string(),
|
|
||||||
_ => panic!("No key here"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_apk(location: &ApkLocation) -> Apk {
|
|
||||||
match location {
|
|
||||||
ApkLocation {
|
|
||||||
androzoo_key: Some(key),
|
|
||||||
sha256: Some(sha256),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
let key = key.get_key();
|
|
||||||
let url = reqwest::Url::parse_with_params(
|
|
||||||
"https://androzoo.uni.lu/api/download",
|
|
||||||
&[("apikey", key), ("sha256", sha256.clone())],
|
|
||||||
)
|
|
||||||
.expect("Failed to parse url");
|
|
||||||
let res = reqwest::blocking::Client::builder()
|
|
||||||
.timeout(Duration::from_secs(300))
|
|
||||||
.build()
|
|
||||||
.expect("Failed to build client")
|
|
||||||
.get(url)
|
|
||||||
.send()
|
|
||||||
.expect("Failed to download apk");
|
|
||||||
match res.status() {
|
|
||||||
reqwest::StatusCode::OK => (),
|
|
||||||
s => panic!("Failed to download apk: {:?}", s),
|
|
||||||
}
|
|
||||||
Apk::load_apk(
|
|
||||||
&mut Cursor::new(res.bytes().expect("Failed to get APK bytes")),
|
|
||||||
labeling,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
ApkLocation {
|
|
||||||
path: Some(path), ..
|
|
||||||
} => Apk::load_apk(File::open(path).unwrap(), labeling, false).unwrap(),
|
|
||||||
_ => panic!("Don't know what to do with:\n{:#?}", location),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -4,14 +4,14 @@ use anyhow::{bail, Context, Result};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
pub mod get_apk;
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
// Check what
|
// Check what
|
||||||
// https://cs.android.com/android/platform/superproject/main/+/main:art/runtime/reflection.cc;drc=83db0626fad8c6e0508754fffcbbd58e539d14a5;l=698
|
// https://cs.android.com/android/platform/superproject/main/+/main:art/runtime/reflection.cc;drc=83db0626fad8c6e0508754fffcbbd58e539d14a5;l=698
|
||||||
// does.
|
// does.
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
|
||||||
pub struct ReflectionData {
|
pub struct ReflectionData {
|
||||||
pub invoke_data: Vec<ReflectionInvokeData>,
|
pub invoke_data: Vec<ReflectionInvokeData>,
|
||||||
pub class_new_inst_data: Vec<ReflectionClassNewInstData>,
|
pub class_new_inst_data: Vec<ReflectionClassNewInstData>,
|
||||||
|
|
@ -95,7 +95,7 @@ impl ReflectionData {
|
||||||
|
|
||||||
/// Structure storing the runtime information of a reflection call using
|
/// Structure storing the runtime information of a reflection call using
|
||||||
/// `java.lang.reflect.Method.invoke()`.
|
/// `java.lang.reflect.Method.invoke()`.
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
|
||||||
pub struct ReflectionInvokeData {
|
pub struct ReflectionInvokeData {
|
||||||
/// The method called by `java.lang.reflect.Method.invoke()`
|
/// The method called by `java.lang.reflect.Method.invoke()`
|
||||||
pub method: IdMethod,
|
pub method: IdMethod,
|
||||||
|
|
@ -109,7 +109,7 @@ pub struct ReflectionInvokeData {
|
||||||
|
|
||||||
/// Structure storing the runtime information of a reflection instanciation using
|
/// Structure storing the runtime information of a reflection instanciation using
|
||||||
/// `java.lang.Class.newInstance()`.
|
/// `java.lang.Class.newInstance()`.
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
|
||||||
pub struct ReflectionClassNewInstData {
|
pub struct ReflectionClassNewInstData {
|
||||||
/// The constructor called by `java.lang.Class.newInstance()`
|
/// The constructor called by `java.lang.Class.newInstance()`
|
||||||
pub constructor: IdMethod,
|
pub constructor: IdMethod,
|
||||||
|
|
@ -121,7 +121,7 @@ pub struct ReflectionClassNewInstData {
|
||||||
|
|
||||||
/// Structure storing the runtime information of a reflection instanciation using
|
/// Structure storing the runtime information of a reflection instanciation using
|
||||||
/// `java.lang.reflect.Constructor.newInstance()`.
|
/// `java.lang.reflect.Constructor.newInstance()`.
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
|
||||||
pub struct ReflectionCnstrNewInstData {
|
pub struct ReflectionCnstrNewInstData {
|
||||||
/// The constructor calleb by `java.lang.reflect.Constructor.newInstance()`
|
/// The constructor calleb by `java.lang.reflect.Constructor.newInstance()`
|
||||||
pub constructor: IdMethod,
|
pub constructor: IdMethod,
|
||||||
|
|
@ -191,7 +191,7 @@ static CNSTR_GET_DEC_CLS: LazyLock<IdMethod> = LazyLock::new(|| {
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Function passed to [`androscalpel::Apk::load_apk`] to label the instructions of interest.
|
/// Function passed to [`androscalpel::Apk::load_apk`] to label the instructions of interest.
|
||||||
fn labeling(_mth: &IdMethod, ins: &Instruction, addr: usize) -> Option<String> {
|
pub fn labeling(_mth: &IdMethod, ins: &Instruction, addr: usize) -> Option<String> {
|
||||||
match ins {
|
match ins {
|
||||||
Instruction::InvokeVirtual { method, .. }
|
Instruction::InvokeVirtual { method, .. }
|
||||||
if method == &*MTH_INVOKE
|
if method == &*MTH_INVOKE
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io::Cursor;
|
use std::fs::File;
|
||||||
|
use std::io::{Cursor, Read};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use androscalpel::IdMethod;
|
use androscalpel::Apk;
|
||||||
|
|
||||||
use patcher::get_apk::{get_apk, ApkLocation};
|
|
||||||
use patcher::{
|
use patcher::{
|
||||||
transform_method, ReflectionClassNewInstData, ReflectionCnstrNewInstData, ReflectionData,
|
labeling,
|
||||||
ReflectionInvokeData,
|
transform_method,
|
||||||
|
ReflectionData, // ReflectionInvokeData, ReflectionClassNewInstData, ReflectionCnstrNewInstData,
|
||||||
};
|
};
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
@ -15,8 +16,6 @@ use clap::Parser;
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(version, about, long_about = None, arg_required_else_help = true)]
|
#[command(version, about, long_about = None, arg_required_else_help = true)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
#[clap(flatten)]
|
|
||||||
apk: ApkLocation,
|
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
out: PathBuf,
|
out: PathBuf,
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
|
|
@ -25,13 +24,24 @@ struct Cli {
|
||||||
zipalign: Option<PathBuf>,
|
zipalign: Option<PathBuf>,
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
apksigner: Option<PathBuf>,
|
apksigner: Option<PathBuf>,
|
||||||
|
#[arg(short, long)]
|
||||||
|
path: PathBuf,
|
||||||
|
#[arg(short, long)]
|
||||||
|
reflection_data: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
let mut apk = get_apk(&cli.apk);
|
let mut apk = Apk::load_apk(File::open(&cli.path).unwrap(), labeling, false).unwrap();
|
||||||
//println!("{:#?}", apk.list_classes());
|
//println!("{:#?}", apk.list_classes());
|
||||||
|
let mut json = String::new();
|
||||||
|
File::open(&cli.reflection_data)
|
||||||
|
.unwrap()
|
||||||
|
.read_to_string(&mut json)
|
||||||
|
.unwrap();
|
||||||
|
let reflection_data: ReflectionData = serde_json::from_str(&json).unwrap();
|
||||||
|
/*
|
||||||
let reflection_data = ReflectionData {
|
let reflection_data = ReflectionData {
|
||||||
invoke_data: vec![
|
invoke_data: vec![
|
||||||
ReflectionInvokeData {
|
ReflectionInvokeData {
|
||||||
|
|
@ -102,12 +112,15 @@ fn main() {
|
||||||
addr: 0x22,
|
addr: 0x22,
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
println!("{}", serde_json::to_string(&reflection_data).unwrap());
|
||||||
|
*/
|
||||||
for method in reflection_data.get_method_referenced().iter() {
|
for method in reflection_data.get_method_referenced().iter() {
|
||||||
let class = apk.get_class_mut(&method.class_).unwrap();
|
if let Some(class) = apk.get_class_mut(&method.class_) {
|
||||||
//println!("{:#?}", class.direct_methods.keys());
|
//println!("{:#?}", class.direct_methods.keys());
|
||||||
//println!("{:#?}", class.virtual_methods.keys());
|
//println!("{:#?}", class.virtual_methods.keys());
|
||||||
let method = class.virtual_methods.get_mut(method).unwrap();
|
let method = class.virtual_methods.get_mut(method).unwrap();
|
||||||
transform_method(method, &reflection_data).unwrap();
|
transform_method(method, &reflection_data).unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let mut dex_files = vec![];
|
let mut dex_files = vec![];
|
||||||
let mut files = apk.gen_raw_dex().unwrap();
|
let mut files = apk.gen_raw_dex().unwrap();
|
||||||
|
|
@ -127,7 +140,7 @@ fn main() {
|
||||||
}
|
}
|
||||||
// TODO: aapt would be a lot more stable
|
// TODO: aapt would be a lot more stable
|
||||||
apk_frauder::replace_dex(
|
apk_frauder::replace_dex(
|
||||||
cli.apk.path.unwrap(),
|
cli.path,
|
||||||
cli.out,
|
cli.out,
|
||||||
&mut dex_files,
|
&mut dex_files,
|
||||||
cli.keystore,
|
cli.keystore,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue