利用FRIDA攻击Android应用程序(三)

简介:

利用FRIDA攻击Android应用程序(三)

 

 

 

前言

在我的有关frida的第二篇博客发布不久之后,@muellerberndt决定发布另一个OWASP Android crackme,我很想知道是否可以再次用frida解决。如果你想跟着我做一遍,你需要下面的工具。

OWASP Uncrackable Level2 APK

Android SDK和模拟器(我使用的是Android 7.1 x64镜像)

frida(和frida-server)

bytecodeviewer

radare2(或您选择的其他一些反汇编工具)

apktool


如果您需要知道如何安装Frida,请查看Frida文档。对于Frida的使用,您还可以检查本教程的第I部分。我假设你在继续之前拥有上面的工具,并且基本熟悉Frida。另外,确保Frida可以连接到您的设备/模拟器(例如使用frida-ps -U)。我将向您展示各种方法来克服具体的问题,如果您只是寻找一个快速的解决方案,请在本教程末尾查看Frida脚本。


注意:如果使用frida遇到了:Error: access violation accessing 0xebad8082或者类似的错误,从模拟器中擦除用户数据,重新启动并重新安装该apk可能有助于解决问题。做好可能需要多次尝试的准备。该应用程序可能会崩溃,模拟器可能会重新启动,一切可能会搞砸,但是最终我们会成功的。


第一次尝试

和UnCrackable1 一样,当在仿真器中运行它时,它会检测到它是在root设备上运行的。

我们可以尝试像前面一样hook OnClickListener。但首先我们来看看我们是否可以连接frida开始tampering。

michael@sixtyseven:~/Development$ frida -U sg.vantagepoint.uncrackable2
____
/ _ | Frida 9.1.27 - A world-class dynamic instrumentation framework
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at http://www.frida.re/docs/home/
Failed to attach: ambiguous name; it matches: sg.vantagepoint.uncrackable2 (pid: 5184), sg.vantagepoint.uncrackable2 (pid: 5201)


有两个名称相同的进程,我们可以通过frida-ps -U验证一下。

5184 sg.vantagepoint.uncrackable2
5201 sg.vantagepoint.uncrackable2


我们来试试将frida注入父进程。

michael@sixtyseven:~/Development$ frida -U 5184
____
/ _ | Frida 9.1.27 - A world-class dynamic instrumentation framework
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at http://www.frida.re/docs/home/
Failed to attach: unable to access process with pid 5184 due to system restrictions; try `sudo sysctl kernel.yama.ptrace_scope=0`, or run Frida as root


这里发生了什么?我们来看看应用程序吧。解压缩apk并反编译classes.dex。

package sg.vantagepoint.uncrackable2;

import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v7.app.c;
import android.text.Editable;
import android.view.View;
import android.widget.EditText;
import sg.vantagepoint.a.a;
import sg.vantagepoint.a.b;
import sg.vantagepoint.uncrackable2.CodeCheck;
import sg.vantagepoint.uncrackable2.MainActivity;

public class MainActivity
extends c {
private CodeCheck m;

static {
System.loadLibrary("foo"); //[1]
}

private void a(String string) {
AlertDialog alertDialog = new AlertDialog.Builder((Context)this).create();
alertDialog.setTitle((CharSequence)string);
alertDialog.setMessage((CharSequence)"This in unacceptable. The app is now going to exit.");
alertDialog.setButton(-3, (CharSequence)"OK", (DialogInterface.OnClickListener)new /* Unavailable Anonymous Inner Class!! */);
alertDialog.setCancelable(false);
alertDialog.show();
}

static /* synthetic */ void a(MainActivity mainActivity, String string) {
mainActivity.a(string);
}

private native void init(); //[2]

protected void onCreate(Bundle bundle) {
this.init(); //[3]
if (b.a() || b.b() || b.c()) {
this.a("Root detected!");
}
if (a.a((Context)this.getApplicationContext())) {
this.a("App is debuggable!");
}
new /* Unavailable Anonymous Inner Class!! */.execute((Object[])new Void[]{null, null, null});
this.m = new CodeCheck();
super.onCreate(bundle);
this.setContentView(2130968603);
}

public void verify(View view) {
String string = ((EditText)this.findViewById(2131427422)).getText().toString();
AlertDialog alertDialog = new AlertDialog.Builder((Context)this).create();
if (this.m.a(string)) {
alertDialog.setTitle((CharSequence)"Success!");
alertDialog.setMessage((CharSequence)"This is the correct secret.");
} else {
alertDialog.setTitle((CharSequence)"Nope...");
alertDialog.setMessage((CharSequence)"That's not it. Try again.");
}
alertDialog.setButton(-3, (CharSequence)"OK", (DialogInterface.OnClickListener)new /* Unavailable Anonymous Inner Class!! */);
alertDialog.show();
}
}


我们注意到程序加载了foo库([1])。在onCreate方法的第一行程序调用了this.init()([3]),它被声明成一个native方法([2]),所以它可能是foo的一部分。现在我们来看看foo库。使用radare2打开它并分析,列出它的导出函数。

michael@sixtyseven:~/Development/UnCrackable2/lib/x86_64$ r2 libfoo.so
-- Don't look at the code. Don't look.
[0x000007a0]> aaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze len bytes of instructions for references (aar)
[x] Analyze function calls (aac)
[ ] [*] Use -AA or aaaa to perform additional experimental analysis.
[x] Constructing a function name for fcn.* and sym.func.* functions (aan))

[0x000007a0]> iE
[Exports]
vaddr=0x00001060 paddr=0x00001060 ord=004 fwd=NONE sz=183 bind=GLOBAL type=FUNC name=Java_sg_vantagepoint_uncrackable2_CodeCheck_bar
vaddr=0x00001050 paddr=0x00001050 ord=006 fwd=NONE sz=15 bind=GLOBAL type=FUNC name=Java_sg_vantagepoint_uncrackable2_MainActivity_init
vaddr=0x00004008 paddr=0x00003008 ord=014 fwd=NONE sz=0 bind=GLOBAL type=NOTYPE name=__bss_start
vaddr=0x00004008 paddr=0x00003008 ord=015 fwd=NONE sz=0 bind=GLOBAL type=NOTYPE name=__bss_start
vaddr=0x0000400d paddr=0x0000400d ord=016 fwd=NONE sz=0 bind=GLOBAL type=NOTYPE name=_end

5 exports

[0x000007a0]>


我们注意到程序加载了foo库([1])。在onCreate方法的第一行程序调用了this.init()([3]),它被声明成一个native方法([2]),所以它可能是foo的一部分。现在我们来看看foo库。使用radare2打开它并分析,列出它的导出函数。


该库导出两个有趣的功能:Java_sg_vantagepoint_uncrackable2_MainActivity_init和Java_sg_vantagepoint_uncrackable2_CodeCheck_bar。我们来看看Java_sg_vantagepoint_uncrackable2_MainActivity_init。

[0x000007a0]> s 0x00001050
[0x00001050]> V


这是一个很短的函数。

[0x00001050 29% 848 libfoo.so]> pd $r @ sym.Java_sg_vantagepoint_uncrackable2_MainActivity_init
/ (fcn) sym.Java_sg_vantagepoint_uncrackable2_MainActivity_init 15
| sym.Java_sg_vantagepoint_uncrackable2_MainActivity_init ();
| 0x00001050 50 push rax
| 0x00001051 e8caf7ffff call sub.fork_820 ;[1]
| 0x00001056 c605af2f0000. mov byte [0x0000400c], 1 ; [0x400c:1]=58 ; ": (GNU) 4.9.x 20150123 (prerelease)"
| 0x0000105d 58 pop rax
\ 0x0000105e c3 ret
0x0000105f 90 nop


它调用了sub.fork_820,这里面有更多的内容。

[0x00000820 14% 265 libfoo.so]> pd $r @ sub.fork_820
/ (fcn) sub.fork_820 242
| sub.fork_820 ();
| ; var int local_8h @ rsp+0x8
| ; var int local_10h @ rsp+0x10
| ; CALL XREF from 0x00001051 (sym.Java_sg_vantagepoint_uncrackable2_MainActivity_init)
| 0x00000820 4156 push r14
| 0x00000822 53 push rbx
| 0x00000823 4883ec18 sub rsp, 0x18
| 0x00000827 64488b042528. mov rax, qword fs:[0x28] ; [0x28:8]=0x3180 ; '('
| 0x00000830 4889442410 mov qword [local_10h], rax
| 0x00000835 e806ffffff call sym.imp.fork ;[1]
| 0x0000083a 8905c8370000 mov dword loc.__bss_start, eax ; [0x4008:4]=0x43434700 ; loc.__bss_start
| 0x00000840 85c0 test eax, eax
| ,=< 0x00000842 741a je 0x85e ;[2]
| | 0x00000844 488d15a5ffff. lea rdx, 0x000007f0 ; 0x7f0
| | 0x0000084b 488d7c2408 lea rdi, [local_8h] ; 0x8
| | 0x00000850 31f6 xor esi, esi
| | 0x00000852 31c9 xor ecx, ecx
| | 0x00000854 e8f7feffff call sym.imp.pthread_create ;[3]; ssize_t read(int fildes, void *buf, size_t nbyte)
| ,==< 0x00000859 e990000000 jmp 0x8ee ;[4]
| || ; JMP XREF from 0x00000842 (sub.fork_820)
| |`-> 0x0000085e e8fdfeffff call sym.imp.getppid ;[5]
| | 0x00000863 89c3 mov ebx, eax
| | 0x00000865 bf10000000 mov edi, 0x10
| | 0x0000086a 31d2 xor edx, edx
| | 0x0000086c 31c9 xor ecx, ecx
| | 0x0000086e 31c0 xor eax, eax
| | 0x00000870 89de mov esi, ebx
| | 0x00000872 e8f9feffff call sym.imp.ptrace ;[6]
| | 0x00000877 4885c0 test rax, rax
| |,=< 0x0000087a 7572 jne 0x8ee ;[4]
| || 0x0000087c 4c8d742408 lea r14, [local_8h] ; 0x8
| || 0x00000881 31d2 xor edx, edx
| || 0x00000883 89df mov edi, ebx
| || 0x00000885 4c89f6 mov rsi, r14
| || 0x00000888 e883feffff call sym.imp.waitpid ;[7]


这个函数中调用了fork、pthread_create、getppid、ptrace和waitpid等函数。这是一个基本的反调试技术,附加调试进程被阻止,因为已经有其他进程作为调试器连接。


对抗反调试方案一:frida

我们可以让frida为我们生成一个进程而不是将它注入到运行中的进程中。

frida -U -f sg.vantagepoint.uncrackable2


michael@sixtyseven:~/Development/UnCrackable2/lib/x86_64$ frida -U -f sg.vantagepoint.uncrackable2 --no-pause
____
/ _ | Frida 9.1.27 - A world-class dynamic instrumentation framework
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at http://www.frida.re/docs/home/
Spawned `sg.vantagepoint.uncrackable2`. Resuming main thread!
[USB::Android Emulator 5554::['sg.vantagepoint.uncrackable2']]->


frida注入到Zygote中,生成我们的进程并且等待输入,这个过程可能比较漫长。


对抗反调试方案二:patch

我们可以通过apktool实现patch。

michael@sixtyseven:~/Disassembly$ /opt/apktool/apktool.sh -r d UnCrackable-Level2.apk
I: Using Apktool 2.2.0 on UnCrackable-Level2.apk
I: Copying raw resources...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...


(我通过-r选项跳过了资源提取,因为在回编译apk的时候它可能会导致问题,反正我们这里不需要资源文件。) 看一下smali/sg/vantagepoint/uncrackable2/MainActivity.smali中的smali代码。你可以在第82行找到init的调用并注释掉它。

# virtual methods
.method protected onCreate(Landroid/os/Bundle;)V
.locals 4

const/4 v3, 0x0

# invoke-direct {p0}, Lsg/vantagepoint/uncrackable2/MainActivity;->init()V

invoke-static {}, Lsg/vantagepoint/a/b;->a()Z


回编译apk(忽略错误)。

michael@sixtyseven:~/Disassembly/UnCrackable-Level2$ /opt/apktool/apktool.sh b
I: Using Apktool 2.2.0
I: Checking whether sources has changed...
I: Smaling smali folder into classes.dex...
[Fatal Error] AndroidManifest.xml:1:1: Content ist nicht zul?ssig in Prolog.
I: Checking whether resources has changed...
I: Copying raw resources...
I: Copying libs... (/lib)
I: Building apk file...
I: Copying unknown files/dir...


对齐。

michael@sixtyseven:~/Disassembly/UnCrackable-Level2$ zipalign -v 4 dist/UnCrackable-Level2.apk UnCrackable2.recompiled.aligned.apk
Verifying alignment of UnCrackable2.recompiled.aligned.apk (4)...
49 AndroidManifest.xml (OK - compressed)
914 classes.dex (OK - compressed)
269899 lib/arm64-v8a/libfoo.so (OK - compressed)
273297 lib/armeabi-v7a/libfoo.so (OK - compressed)
279346 lib/armeabi/libfoo.so (OK - compressed)


签名(注意:您需要有一个密钥和密钥库)。

michael@sixtyseven:~/Disassembly/UnCrackable-Level2$ jarsigner -verbose -keystore ~/.android/debug.keystore UnCrackable2.recompiled.aligned.apk signkey
Enter Passphrase for keystore:
adding: META-INF/MANIFEST.MF
adding: META-INF/SIGNKEY.SF
adding: META-INF/SIGNKEY.RSA
signing: AndroidManifest.xml
signing: classes.dex
signing: lib/arm64-v8a/libfoo.so
signing: lib/armeabi-v7a/libfoo.so
signing: lib/armeabi/libfoo.so
signing: lib/mips/libfoo.so
[...]


你可以在OWASP Mobile Security Testing Guide中找到更详细的描述。卸载原来的apk并安装我们patch过的apk。

adb uninstall sg.vantagepoint.uncrackable2
adb install UnCrackable2.recompiled.aligned.apk


重新启动应用程序。运行frida-ps,现在只有一个进程了。

29996 sg.vantagepoint.uncrackable2


frida进行连接也没什么问题。

michael@sixtyseven:~/Disassembly/UnCrackable-Level2$ frida -U sg.vantagepoint.uncrackable2
____
/ _ | Frida 9.1.27 - A world-class dynamic instrumentation framework
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at http://www.frida.re/docs/home/

[USB::Android Emulator 5554::sg.vantagepoint.uncrackable2]->


这比在frida中增加-r选项更为繁琐,但也更普遍。如前所述,当我们使用patch过的版本(我会告诉你如何解决这个问题,所以不要把它删了)不能轻易地提取需要的字符串。现在我们继续使用原来的apk。确保安装的是原始的apk。


继续尝试

在我们摆脱反调试之后来看看如何继续进行下去。一旦按了OK按钮,应用程序就会在模拟器中运行时进行root检测并退出。我们可以patch掉这个行为,也可以用frida来解决这个问题。由于OnClickListener实现调用,我们可以hook System.exit函数使其不产生作用。

setImmediate(function() {
console.log("[*] Starting script");
Java.perform(function() {
exitClass = Java.use("java.lang.System");
exitClass.exit.implementation = function() {
console.log("[*] System.exit called");
}
console.log("[*] Hooking calls to System.exit");
});
});


再次关闭任何正在运行的UnCrackable2实例,并再次在frida的帮助下启动它。

frida -U -f sg.vantagepoint.uncrackable2 -l uncrackable2.js --no-pause


再次关闭任何正在运行的UnCrackable2实例,并再次在frida的帮助下启动它。

等到app启动,frida在控制台中显示Hooking calls…然后按OK。你应该得到这样的信息。

michael@sixtyseven:~/Development/frida$ frida -U -f sg.vantagepoint.uncrackable2 --no-pause -l uncrackable2.js
____
/ _ | Frida 9.1.27 - A world-class dynamic instrumentation framework
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at http://www.frida.re/docs/home/
Spawned `sg.vantagepoint.uncrackable2`. Resuming main thread!
[USB::Android Emulator 5554::['sg.vantagepoint.uncrackable2']]-> [*] Hooking calls to System.exit
[*] System.exit called


该应用程序不再退出,我们可以输入一个字符串。



但是我们应该在这里输入什么呢?看看MainActivity。

this.m = new CodeCheck();

[...]

//in method: public void verify
if (this.m.a(string)) {
Dialog.setTitle((CharSequence)"Success!");
Dialog.setMessage((CharSequence)"This is the correct secret.");
}


这是CodeCheck类。

package sg.vantagepoint.uncrackable2;

public class CodeCheck {
private native boolean bar(byte[] var1);

public boolean a(String string) {
return this.bar(string.getBytes()); //Call to a native function
}
}


我们注意到输入的字符串被传递给了一个native方法bar。同样,我们在libfoo.so中找到了这个函数。使用radare2寻找这个函数的地址并反汇编它。



我们注意到输入的字符串被传递给了一个native方法bar。同样,我们在libfoo.so中找到了这个函数。使用radare2寻找这个函数的地址并反汇编它。

反汇编代码中有一些字符串比较操作,有一个有趣的明文字符串Thanks for all t。输入这个字符串,但是它不起作用。看看地址0x000010d8处的反汇编代码。

0x000010d8 83f817 cmp eax, 0x17
0x000010db 7519 jne 0x10f6 ;[1]


这里有一个eax和0x17的比较,如果不相同的话strncmp函数不会被调用。我们同时注意到0x17是strncmp的一个参数。

0x000010e1 ba17000000 mov edx, 0x17


464位的linux中函数的前6个参数通过寄存器传递,前3个寄存器分别是RDI、 RSI和RDX。所以strncmp函数将比较0x17=23个字符。可以推断,输入的字符串的长度应该是23。让我们尝试hook strncmp,并打印出它的参数。如果你这样做,你会发现strncmp被调用了很多次,我们需要进一步限制输出。

var strncmp = undefined;
imports = Module.enumerateImportsSync("libfoo.so");

for(i = 0; i < imports.length; i++) {
if(imports[i].name == "strncmp") {
strncmp = imports[i].address;
break;
}

}

Interceptor.attach(strncmp, {
onEnter: function (args) {
if(args[2].toInt32() == 23 && Memory.readUtf8String(args[0],23) == "01234567890123456789012") {
console.log("[*] Secret string at " + args[1] + ": " + Memory.readUtf8String(args[1],23));
}
}
});


1.该脚本调用Module.enumerateImportsSync以从libfoo.so中获取有关导入信息的对象数组。我们遍历这个数组,直到找到strncmp并检索其地址。然后我们将interceptor附加到它。


2.Java中的字符串不会以null结束。当strncmp使用frida的Memory.readUtf8String方法访问字符串指针的内存位置并且不提供长度时,frida会期待一个\0结束符,或者输出一些垃圾内存。它不知道字符串在哪里结束。如果我们指定要读取的字符数量作为第二个参数就解决了这个问题。


3.如果我们没有限制strncmp参数的条件将得到很多输出。限制条件为第三个参数size_t为23。


我怎么如何知道args[0]是我们的输入,args[1]是我们寻找的字符串呢?我不知道,我只是测试,将大量的输出dump到屏幕以找到我的输入。如果你不想跳过这部分,可以删除上面脚本中的if语句,并使用frida的hexdump输出。


以下是完整版的脚本,可以更好地输出参数。

setImmediate(function() {
Java.perform(function() {
console.log("[*] Hooking calls to System.exit");
exitClass = Java.use("java.lang.System");
exitClass.exit.implementation = function() {
console.log("[*] System.exit called");
}

var strncmp = undefined;
imports = Module.enumerateImportsSync("libfoo.so");

for(i = 0; i < imports.length; i++) {
if(imports[i].name == "strncmp") {
strncmp = imports[i].address;
break;
}

}

Interceptor.attach(strncmp, {
onEnter: function (args) {
if(args[2].toInt32() == 23 && Memory.readUtf8String(args[0],23) == "01234567890123456789012") {
console.log("[*] Secret string at " + args[1] + ": " + Memory.readUtf8String(args[1],23));
}
},
});
console.log("[*] Intercepting strncmp");
});
});



现在启动frida加载这个脚本。

frida -U -f sg.vantagepoint.uncrackable2 --no-pause -l uncrackable2.js


输入字符串并且按下VERIFY。



在控制台会看到下面的结果。

michael@sixtyseven:~/Development/frida$ frida -U -f sg.vantagepoint.uncrackable2 --no-pause -l uncrackable2.js
____
/ _ | Frida 9.1.27 - A world-class dynamic instrumentation framework
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at http://www.frida.re/docs/home/
Spawned `sg.vantagepoint.uncrackable2`. Resuming main thread!
[USB::Android Emulator 5554::['sg.vantagepoint.uncrackable2']]-> [*] Hooking calls to System.exit
[*] Intercepting strncmp
[*] System.exit called
[*] Secret string at 0x7fffa628f010: Thanks for all the fish


我们找到了正确的字符串Thanks for all the fish。



使用patch过的apk

当我们使用patch过的apk时可能不会得到需要的字符串。libfoo库中的init函数包含一些初始化逻辑,阻止应用程序根据我们的输入检查或解码字符串。如果我们再看看init函数的反汇编代码会看到有趣的一行。

0x00001056 c605af2f0000. mov byte [0x0000400c], 1


相同的变量会在libfoo库的bar函数中检查,如果没有设置,那么代码会跳过strncmp。

0x0000107d 803d882f0000. cmp byte [0x0000400c], 1 ; [0x1:1]=69
0x00001084 7570 jne 0x10f6 ;[1]


它可能是一个boolean类型的变量,当init函数运行时被设置。如果我们想要让patch过的apk调用strncmp函数就需要设置这个变量或者至少阻止它跳过 strncmp的调用。我们可以再patch一次,但是既然这是frida教程,我们可以使用它动态改变内存。下面是可供patch过的apk使用的完整的脚本。

setImmediate(function() {
Java.perform(function() {
console.log("[*] Hooking calls to System.exit");
exitClass = Java.use("java.lang.System");
exitClass.exit.implementation = function() {
console.log("[*] System.exit called");
}

var strncmp = undefined;
imports = Module.enumerateImportsSync("libfoo.so");

for(i = 0; i < imports.length; i++) {
if(imports[i].name == "strncmp") {
strncmp = imports[i].address;
break;
}

}

//Get base address of library
var libfoo = Module.findBaseAddress("libfoo.so");

//Calculate address of variable
var initialized = libfoo.add(ptr("0x400C"));

//Write 1 to the variable
Memory.writeInt(initialized,1);

Interceptor.attach(strncmp, {
onEnter: function (args) {
if(args[2].toInt32() == 23 && Memory.readUtf8String(args[0],23) == "01234567890123456789012") {
console.log("[*] Secret string at " + args[1] + ": " + Memory.readUtf8String(args[1],23));
}
},
});
console.log("[*] Intercepting strncmp");
});
});

 

 

 

 

本文来自合作伙伴“阿里聚安全”,发表于2017年05月02日 16:34.

 

相关文章
|
4月前
|
开发框架 前端开发 Android开发
Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势
本文深入探讨了 Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势。这对于实现高效的跨平台移动应用开发具有重要指导意义。
461 4
|
1月前
|
前端开发 Java Shell
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
179 20
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
|
1月前
|
Dart 前端开发 Android开发
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
55 4
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
|
3月前
|
JSON Java API
探索安卓开发:打造你的首个天气应用
在这篇技术指南中,我们将一起潜入安卓开发的海洋,学习如何从零开始构建一个简单的天气应用。通过这个实践项目,你将掌握安卓开发的核心概念、界面设计、网络编程以及数据解析等技能。无论你是初学者还是有一定基础的开发者,这篇文章都将为你提供一个清晰的路线图和实用的代码示例,帮助你在安卓开发的道路上迈出坚实的一步。让我们一起开始这段旅程,打造属于你自己的第一个安卓应用吧!
110 14
|
3月前
|
Java Linux 数据库
探索安卓开发:打造你的第一款应用
在数字时代的浪潮中,每个人都有机会成为创意的实现者。本文将带你走进安卓开发的奇妙世界,通过浅显易懂的语言和实际代码示例,引导你从零开始构建自己的第一款安卓应用。无论你是编程新手还是希望拓展技术的开发者,这篇文章都将为你打开一扇门,让你的创意和技术一起飞扬。
|
3月前
|
搜索推荐 前端开发 测试技术
打造个性化安卓应用:从设计到开发的全面指南
在这个数字时代,拥有一个定制的移动应用不仅是一种趋势,更是个人或企业品牌的重要延伸。本文将引导你通过一系列简单易懂的步骤,从构思你的应用理念开始,直至实现一个功能齐全的安卓应用。无论你是编程新手还是希望拓展技能的开发者,这篇文章都将为你提供必要的工具和知识,帮助你将创意转化为现实。
|
4月前
|
JSON Java Android开发
探索安卓开发之旅:打造你的第一个天气应用
【10月更文挑战第30天】在这个数字时代,掌握移动应用开发技能无疑是进入IT行业的敲门砖。本文将引导你开启安卓开发的奇妙之旅,通过构建一个简易的天气应用来实践你的编程技能。无论你是初学者还是有一定经验的开发者,这篇文章都将成为你宝贵的学习资源。我们将一步步地深入到安卓开发的世界中,从搭建开发环境到实现核心功能,每个环节都充满了发现和创造的乐趣。让我们开始吧,一起在代码的海洋中航行!
|
3月前
|
Java Android开发 开发者
探索安卓开发:构建你的第一个“Hello World”应用
在安卓开发的浩瀚海洋中,每个新手都渴望扬帆起航。本文将作为你的指南针,引领你通过创建一个简单的“Hello World”应用,迈出安卓开发的第一步。我们将一起搭建开发环境、了解基本概念,并编写第一行代码。就像印度圣雄甘地所说:“你必须成为你希望在世界上看到的改变。”让我们一起开始这段旅程,成为我们想要见到的开发者吧!
102 0
|
4月前
|
存储 搜索推荐 Java
打造个性化安卓应用:从设计到实现
【10月更文挑战第30天】在数字化时代,拥有一个个性化的安卓应用不仅能够提升用户体验,还能加强品牌识别度。本文将引导您了解如何从零开始设计和实现一个安卓应用,涵盖用户界面设计、功能开发和性能优化等关键环节。我们将以一个简单的记事本应用为例,展示如何通过Android Studio工具和Java语言实现基本功能,同时确保应用流畅运行。无论您是初学者还是希望提升现有技能的开发者,这篇文章都将为您提供宝贵的见解和实用的技巧。
|
4月前
|
搜索推荐 开发工具 Android开发
打造个性化Android应用:从设计到实现的旅程
【10月更文挑战第26天】在这个数字时代,拥有一个能够脱颖而出的移动应用是成功的关键。本文将引导您了解如何从概念化阶段出发,通过设计、开发直至发布,一步步构建一个既美观又实用的Android应用。我们将探讨用户体验(UX)设计的重要性,介绍Android开发的核心组件,并通过实际案例展示如何克服开发中的挑战。无论您是初学者还是有经验的开发者,这篇文章都将为您提供宝贵的见解和实用的技巧,帮助您在竞争激烈的应用市场中脱颖而出。

热门文章

最新文章

  • 1
    Android历史版本与APK文件结构
  • 2
    【01】噩梦终结flutter配安卓android鸿蒙harmonyOS 以及next调试环境配鸿蒙和ios真机调试环境-flutter项目安卓环境配置-gradle-agp-ndkVersion模拟器运行真机测试环境-本地环境搭建-如何快速搭建android本地运行环境-优雅草卓伊凡-很多人在这步就被难倒了
  • 3
    【03】仿站技术之python技术,看完学会再也不用去购买收费工具了-修改整体页面做好安卓下载发给客户-并且开始提交网站公安备案-作为APP下载落地页文娱产品一定要备案-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
  • 4
    【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
  • 5
    【03】微信支付商户申请下户到配置完整流程-微信开放平台创建APP应用-填写上传基础资料-生成安卓证书-获取Apk签名-申请+配置完整流程-优雅草卓伊凡
  • 6
    【02】仿站技术之python技术,看完学会再也不用去购买收费工具了-本次找了小影-感觉页面很好看-本次是爬取vue需要用到Puppeteer库用node.js扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
  • 7
    Cellebrite UFED 4PC 7.71 (Windows) - Android 和 iOS 移动设备取证软件
  • 8
    escrcpy:【技术党必看】Android开发,Escrcpy 让你无线投屏新体验!图形界面掌控 Android,30-120fps 超流畅!🔥
  • 9
    Android实战经验之Kotlin中快速实现MVI架构
  • 10
    即时通讯安全篇(一):正确地理解和使用Android端加密算法
  • 1
    【03】微信支付商户申请下户到配置完整流程-微信开放平台创建APP应用-填写上传基础资料-生成安卓证书-获取Apk签名-申请+配置完整流程-优雅草卓伊凡
    64
  • 2
    android FragmentManager 删除所有Fragment 重建
    25
  • 3
    Android实战经验之Kotlin中快速实现MVI架构
    41
  • 4
    即时通讯安全篇(一):正确地理解和使用Android端加密算法
    41
  • 5
    escrcpy:【技术党必看】Android开发,Escrcpy 让你无线投屏新体验!图形界面掌控 Android,30-120fps 超流畅!🔥
    46
  • 6
    【01】噩梦终结flutter配安卓android鸿蒙harmonyOS 以及next调试环境配鸿蒙和ios真机调试环境-flutter项目安卓环境配置-gradle-agp-ndkVersion模拟器运行真机测试环境-本地环境搭建-如何快速搭建android本地运行环境-优雅草卓伊凡-很多人在这步就被难倒了
    156
  • 7
    Cellebrite UFED 4PC 7.71 (Windows) - Android 和 iOS 移动设备取证软件
    53
  • 8
    【03】仿站技术之python技术,看完学会再也不用去购买收费工具了-修改整体页面做好安卓下载发给客户-并且开始提交网站公安备案-作为APP下载落地页文娱产品一定要备案-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
    73
  • 9
    Android历史版本与APK文件结构
    179
  • 10
    【02】仿站技术之python技术,看完学会再也不用去购买收费工具了-本次找了小影-感觉页面很好看-本次是爬取vue需要用到Puppeteer库用node.js扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
    54