【Android安全-小白如何通过大模型跑通unidbg调用sgmain生成某ckey参数】此文章归类为:Android安全。
1 2 3 | 本篇文章仅适用于学习交流使用,如有不当,请联系删除。对于逆向来说,非常的没有档次,算是提供给小白一种学习进步的方法。 |
本人在安卓逆向方面属于小白,是跟着正己大佬《安卓逆向这档事》简单学习了一下基础知识,水平算是能够跟着大佬的攻略一步一步走下去的水平,因此本文可能存在大量解释有误的地方,欢迎大家指正。
最近恰逢春节在家无聊,同时刚好gemini 3.0 pro上线,想着通过大模型的能力,看是否能协助我完成“逆向”(本人的能力属实算不上)上的突破。之前学习过漁滒encryptR_client的生成教程,里面仅完成了encryptR_client“补环境”过程,最终没有完成ckey的生成。当时我就搭好了架子,但是同样一直存在一些卡点,最终一直没有完成ckey部分的生成。随着最近大模型能力的提升,我通过gemini(其他大模型大家可以自测)先后完成了某讯ckey、chacha20算法还原,以及本文要说的ckey的unidbg生成。(不得不感叹当前大模型能力的强大,能帮助我这样一个完全看不懂ida伪代码的人,完成算法还原)
回到正题,开工。
一些基础知识,本文会一笔带过,大家可以通过其他文档学习,一定讲得比我好。另外遇到没讲清楚的地方,可以咨询大模型,本文涉及的所有代码,几乎全部由大模型完成。

需要hook的com.taobao.wireless.security.adapter.JNICLibrary下的doCommandNative方法。因为是动态加载的,所以普通的hook方法不好使,需要在BaseDexClassLoader加载class的时候进行hook。
大致逻辑:hook BaseDexClassLoader,然后判断dexPath是否有sgmain,如果有,切换class loader之后,就可以hook到doCommandNative了。hook到方法之后,可以使用下面代码将入参和出参完整打印出来。这里就不贴代码了,大模型可以轻松搞定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | function printRecursive(obj, indent, prefix) { if (indent === undefined) indent = ""; if (prefix === undefined) prefix = ""; var currentIndent = indent + prefix; // Null Check if (obj === null || obj === undefined) { console.log(currentIndent + "null"); return; } // JS Types var type = typeof obj; if (type === 'string') { console.log(currentIndent + "(JS-String) " + JSON.stringify(obj)); return; } if (type === 'number') { console.log(currentIndent + "(JS-Number) " + obj); return; } if (type === 'boolean') { console.log(currentIndent + "(JS-Boolean) " + obj); return; } // JS Array (doCommandNative 的 n 参数) if (Array.isArray(obj)) { console.log(currentIndent + "(JS-Array) Length: " + obj.length + " {"); for (var i = 0; i < obj.length; i++) { printRecursive(obj[i], indent + " ", "[" + i + "] "); } console.log(indent + "}"); return; } // Java Object if (obj.getClass) { try { var clsName = obj.getClass().getName(); if (clsName === "[B") { // === Byte Array === // 直接调用上面的终极格式化函数 console.log(currentIndent + formatByteArray(obj)); } else if (clsName.startsWith("[L")) { // === Java Object Array === // 使用反射获取长度和元素 var len = ReflectArray.getLength(obj); console.log(currentIndent + "(" + clsName + ") Length: " + len + " {"); for (var i = 0; i < len; i++) { var subElem = ReflectArray.get(obj, i); printRecursive(subElem, indent + " ", "[" + i + "] "); } console.log(indent + "}"); } else { // === Ordinary Object === console.log(currentIndent + "(" + clsName + ") " + obj.toString()); } } catch (e) { console.log(currentIndent + "[Error analysing Java Object]: " + e); } return; } console.log(currentIndent + "(Unknown Type) " + obj);} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | function formatByteArray(obj) { try { var len = ReflectArray.getLength(obj); if (len === 0) return "(byte[0])"; var hexBuffer = ""; var strBuffer = ""; // 阈值:如果在 Logcat 里打印太长会截断,这里限制一下预览长度 // 如果你想看全量,可以把 limit 调大,或者去掉 var PREVIEW_LIMIT = 512; var loopLen = (len > PREVIEW_LIMIT) ? PREVIEW_LIMIT : len; for (var i = 0; i < loopLen; i++) { var b = ReflectArray.getByte(obj, i); // 1. 处理 Hex var h = (b & 0xFF).toString(16); if (h.length < 2) h = "0" + h; hexBuffer += h; // 2. 处理 String (JS 硬解码) // 判断是否为可见 ASCII 字符 (32-126) // 0x20(空格) ~ 0x7E(~) if (b >= 32 && b <= 126) { strBuffer += String.fromCharCode(b); } else { // 不可见字符用点代替,保持长度对齐,方便观察 strBuffer += "."; } } if (len > PREVIEW_LIMIT) { hexBuffer += "...(truncated)"; strBuffer += "...(truncated)"; } // 格式化输出 // 如果数据很短,单行显示;很长,分行显示 if (len < 32) { return "(byte[" + len + "]) hex: " + hexBuffer + " | str: " + strBuffer; } else { return "(byte[" + len + "])\n" + " hex: " + hexBuffer + "\n" + " str: " + strBuffer; } } catch (e) { return "format_error: " + e; }} |
unidbg补环境主要会涉及JNI、文件以及系统调用,这里我将3种不同环境放到不同文件下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | package com.taobao.wireless.security.adapter;import com.github.unidbg.AndroidEmulator;import com.github.unidbg.Module;import com.github.unidbg.arm.backend.Unicorn2Factory;import com.github.unidbg.file.linux.AndroidFileIO;import com.github.unidbg.linux.android.AndroidARMEmulator;import com.github.unidbg.linux.android.AndroidEmulatorBuilder;import com.github.unidbg.linux.android.AndroidResolver;import com.github.unidbg.linux.android.dvm.*;import com.github.unidbg.linux.android.dvm.array.ArrayObject;import com.github.unidbg.linux.android.dvm.array.ByteArray;import com.github.unidbg.linux.android.dvm.wrapper.DvmInteger;import com.github.unidbg.linux.android.dvm.wrapper.DvmLong;import com.github.unidbg.memory.Memory;import com.github.unidbg.memory.SvcMemory;import com.github.unidbg.unix.UnixSyscallHandler;import java.io.File;import org.apache.commons.logging.Log;import org.apache.commons.logging.LogFactory;public class JNICLibrary { private static final Log log = LogFactory.getLog(JNICLibrary.class); private final AndroidEmulator emulator; private final VM vm; private final Memory memory; private final Module module; private final DvmClass myjniclass; public JNICLibrary() { // syscall override AndroidEmulatorBuilder builder = new AndroidEmulatorBuilder(false) { @Override public AndroidEmulator build() { return new AndroidARMEmulator(processName, rootDir, backendFactories) { @Override protected UnixSyscallHandler<AndroidFileIO> createSyscallHandler(SvcMemory svcMemory) { return new MySyscallHandler(svcMemory); } }; } }; emulator = builder .setProcessName("com.youku.phone") .setRootDir(new File("unidbg-android/src/main/java/com/taobao/wireless/security/adapter/rootfs")) .addBackendFactory(new Unicorn2Factory(true)) .build(); emulator.getBackend().registerEmuCountHook(100000); emulator.getSyscallHandler().setVerbose(true); emulator.getSyscallHandler().setEnableThreadDispatcher(true); // 文件处理 emulator.getSyscallHandler().addIOResolver(new MyIOResolver()); memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver(23)); memory.setCallInitFunction(true); vm = emulator.createDalvikVM(); vm.setVerbose(true); // 补环境 vm.setJni(new MyJni()); DalvikModule dalvikModule = vm.loadLibrary(new File("unidbg-android/src/main/java/com/taobao/wireless/security/adapter/lib/libsgmainso-6.4.170.so"), true); module = dalvikModule.getModule(); vm.callJNI_OnLoad(emulator, module); myjniclass = vm.resolveClass("com/taobao/wireless/security/adapter/JNICLibrary"); } public static void main(String[] args) { JNICLibrary jnicLibrary = new JNICLibrary(); jnicLibrary.init(); }} |
(pkg名称,统一改成了com.youku.phone,没有使用getvideo)
1 2 3 4 5 6 7 8 9 | package com.taobao.wireless.security.adapter;import com.github.unidbg.linux.android.dvm.AbstractJni;import org.apache.commons.logging.Log;import org.apache.commons.logging.LogFactory;public class MyJni extends AbstractJni { private static final Log log = LogFactory.getLog(MyJni.class);} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | package com.taobao.wireless.security.adapter;import com.github.unidbg.Emulator;import com.github.unidbg.file.FileResult;import com.github.unidbg.file.IOResolver;import com.github.unidbg.file.linux.AndroidFileIO;import org.apache.commons.logging.Log;import org.apache.commons.logging.LogFactory;public class MyIOResolver implements IOResolver<AndroidFileIO> { private static final Log log = LogFactory.getLog(MyIOResolver.class); @Override public FileResult<AndroidFileIO> resolve(Emulator<AndroidFileIO> emulator, String pathname, int oflags) { // 打印所有文件访问请求,无论是否处理 log.info("[MyIOResolver] ========================> File open request: " + pathname); return null; }} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | package com.taobao.wireless.security.adapter;import com.github.unidbg.Emulator;import com.github.unidbg.arm.backend.Backend;import com.github.unidbg.file.linux.AndroidFileIO;import com.github.unidbg.linux.ARM32SyscallHandler;import com.github.unidbg.memory.SvcMemory;import com.github.unidbg.pointer.UnidbgPointer;import org.apache.commons.logging.Log;import org.apache.commons.logging.LogFactory;import unicorn.ArmConst;public class MySyscallHandler extends ARM32SyscallHandler { private static final Log log = LogFactory.getLog(MySyscallHandler.class); public MySyscallHandler(SvcMemory svcMemory) { super(svcMemory); setVerbose(true); // 可按需开启详细日志 } @Override public void hook(Backend backend, int intno, int swi, Object user) { Emulator<AndroidFileIO> emulator = (Emulator<AndroidFileIO>) user; UnidbgPointer pc = UnidbgPointer.register(emulator, ArmConst.UC_ARM_REG_PC); int NR = backend.reg_read(ArmConst.UC_ARM_REG_R7).intValue(); // 打印所有系统调用,无论是否处理 log.info("[MySyscallHandler] ========================>"); log.info("syscall intno=0x" + Integer.toHexString(intno) + ", swi=" + swi + ", NR=" + NR + ", pc=" + pc); super.hook(backend, intno, swi, user); }} |
初始化流程其他很多文档都讲清楚了的,大致如下:10101 => 10102(sgmain) => 10102(securitybody) => 10102(avmp)。
(init代码和后面avmp调用方法大家可以参考别的文章,这里就不贴了)
日志等级调成INFO(DEBUG太慢了),从头开始看运行流程。
创建文件夹即可,其中的内容如果没有似乎不影响。
暂时不用管,测试下来如果从真机pull下来这部分文件,后面补环境会少一些步骤,如果不补,也能够成功。
目测unidbg已经补了?可以不用管。

遇到第一个需要补的。

1 2 3 | case "com/youku/phone/App->getPackageCodePath()Ljava/lang/String;": { return new StringObject(vm, dataAppPath + "/base.apk"); // dataAppPath = "/data/app/com.youku.phone"} |
同时,将base.apk复制到对应路径。(因为一开始设置了rootDir,设置的rootDir就是"/"目录,其他文件/目录可以直接往里面复制,不用处理所有文件)
对于不清楚的文件,可以到真机中去看一眼。
这里因为用的是getvideo,所以存在一个隐形的坑,base.apk需要用getvideo解压后里面的一个Youku_xxxx.apk。后续会从该文件中读取关键安全信息。

1 2 3 | case "com/youku/phone/App->getFilesDir()Ljava/io/File;": { return ProxyDvmObject.createObject(vm, new File("/data/user/0/com.youku.phone/files"));} |
同样,相应的文件夹创建好。

1 2 3 | case "java/io/File->getAbsolutePath()Ljava/lang/String;": { return new StringObject(vm, dvmObject.getValue().toString());} |
常规需要补的环境,可以上网搜,或者直接问大模型。本文后续只介绍一些可能会踩坑的。
这里强烈推荐看看正己大佬的《安卓逆向这档事》第二十五课、Unidbg之补完环境我就睡(中)。很多可能的坑大佬已经介绍了怎么绕过去。
1 2 3 4 5 6 7 8 9 10 11 12 13 | String pkg = emulator.getProcessName();int pid = emulator.getPid();if (pathname.equals("/proc/self/status") || pathname.equals("/proc/" + pid + "/status")) { // 返回一个包含 "TracerPid: 0" 的文件内容,表示未被调试 String statusContent = "Name:\t" + pkg + "\n" + "Umask:\t0077\n" + "State:\tS (sleeping)\n" + "Tgid:\t" + pid + "\n" + "Pid:\t" + pid +"\n" + "PPid:\t1\n" + "TracerPid:\t0\n"; // 关键行 return FileResult.success(new ByteArrayFileIO(oflags, pathname, statusContent.getBytes()));} |
不懂的地方,优先google一下,看看有没有别人遇到过。github issues
1 2 3 4 5 6 7 8 | if (("/proc/" + emulator.getPid() + "/stat").equals(pathname)) { return FileResult.success(new ByteArrayFileIO(oflags, pathname, (emulator.getPid() + " (a.out) R 6723 6873 6723 34819 6873 8388608 77 0 0 0 41958 31 0 0 25 0 3 0 5882654 1409024 56 4294967295 134512640 134513720 3215579040 0 2097798 0 0 0 0 0 0 0 17 0 0 0\n").getBytes()));}if (("/proc/" + emulator.getPid() + "/wchan").equals(pathname)) { return FileResult.success(new ByteArrayFileIO(oflags, pathname, "sys_epoll".getBytes()));} |
参考安卓逆向小案例,很多本文需要补的环境,几乎都能从别人的文档中搜到。
需要注意的就是,尽量所有补的环境都加上log,方便后续debug。
1 2 3 4 5 | @Overridepublic void callStaticVoidMethod(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) { log.info("callStaticVoidMethod signature=" + signature); return ;} |
参考安卓逆向小案例。
结论:在/data/user/0/{PKG}/files下面,,有个SGMANAGER_DATA2文件,里面JSON格式保存key-value。获取数据方法是通过arg1 + "_" + arg2作为key去取
1 2 3 4 5 6 7 8 9 10 11 12 13 | case "com/taobao/wireless/security/adapter/common/SPUtility2->readFromSPUnified(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;": { String arg1 = varArg.getObjectArg(0).getValue().toString(); String arg2 = varArg.getObjectArg(1).getValue().toString(); String key = arg1 + "_" + arg2; System.out.println("KEY==> "+ key); JSONObject data = JSONObject.parseObject("{\"dynamicreid_dynamicreid\":\"xxxx\",\"dynamicrsid_dynamicrs这里还有很多,直接省略了\"}"); String result = data.getString(key); System.out.println("data ==> " + result); if (result != null) { return new StringObject(vm, result); } return null;} |
返回第二个参数。
返回1。
看起来是存到刚才的JSON里面,当你补了SG_INNER_DATA这个文件之后,这个方法就用不到了,所以不补。
返回1就行。感兴趣可以将参数都打印出来看看,应该是用来记录设备行为的。
get方法时将值保存下来,set的时候返回值。
至此,so的初始化就结束了,前面如果遇到乱七八糟的报错,很有可能是文件/目录访问没有处理好,相应的文件和目录都存在的情况下,基本没有什么大坑。
流程:通过60901初始化avmp,然后60902获取ckey
经过测试,60901获取到avmp instance之后,应该可以复用。
_str输入为:ccode=01010101&client_ip=192.168.1.1&client_ts=1770000000&utid=xxxx&vid=XMjk4ODAyMzIyOA==
上面已经讲了base.apk存在坑,如果没有踩这个坑,这里简单补几个环境应该就能完成avmp初始化了。后面很多不太常见的补环境,基本都是依靠gemini帮我补完的,大家也可以试试请教一下大模型。
问大模型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | case "[B->getClass()Ljava/lang/Class;": { // 获取调用这个方法的对象,也就是 byte 数组本身 Object value = dvmObject.getValue(); // 方法1:如果 value 本身就是 byte[],可以直接获取它的 Class 对象 if (value instanceof byte[]) { Class<?> clazz = value.getClass(); // 将 Java 的 Class 对象转换为 unidbg 的 DvmObject // 这里可以直接使用 ProxyDvmObject.createObject 来封装 byte[] byteArray = (byte[]) value; log.info("=== [B->getClass() 调用 ==="); log.info("Byte数组长度: " + byteArray.length); log.info("Byte数组内容(hex): " + bytesToHex(byteArray)); log.info("Byte数组内容(ASCII): " + new String(byteArray).replaceAll("[^\\x20-\\x7E]", ".")); log.info("========================="); return ProxyDvmObject.createObject(vm, clazz); } // 方法2:或者直接返回一个代表 byte[] 类型的 Class 对象 // 这种方式更直接,不依赖于实际对象 Class<?> byteArrayClass = (new byte[0]).getClass(); return ProxyDvmObject.createObject(vm, byteArrayClass);} |
获取设备信息,大模型给了hook代码。需要什么参数,获取什么参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | function doCom(i) { Java.perform(function() { Java.enumerateClassLoaders({ "onMatch": function(loader) { if (loader.toString().indexOf("libsgmain.so") >= 0 ) { Java.classFactory.loader = loader; // 将当前class factory中的loader指定为我们需要的 console.log("loader = ",loader.toString()); } }, "onComplete": function() { console.log("success"); } }); let DeviceInfoCapturer = Java.classFactory.use("com.taobao.wireless.security.adapter.datacollection.DeviceInfoCapturer"); var result = DeviceInfoCapturer.doCommandForString(i); console.log("result ==> " + result) }) } |
直接上图:

这个问题困扰了我很久,一直不知道怎么回事,直到最近问了gemini 3.0 pro,他给了我一个解释:
发生了什么? 1. SO 想要获取设备的 IMEI,所以它去拿 TelephonyManager.getDeviceId() 的方法 ID。 2. Unidbg 作为一个模拟器,为了方便管理,把方法的 Hash 值(0x63bb9035)作为 MethodID 返回给了 SO。 3. 关键来了:在真实的 Android 系统中,MethodID 其实是一个内存指针,指向底层的 ArtMethod 结构体。libsgmain 非常狡猾,它拿到这个 ID 后,并没有立刻去调用方法,而是把它当成内存指针,硬加上偏移量(0x63bb9035 + 5 = 0x63bb903a),试图去读取内存里面的方法的 Access Flags 或者入口点,以此来判断这个方法有没有被 Frida 或 Xposed Hook 掉! 4. 因为 0x63bb9035 只是 Unidbg 算出来的一个 Hash 数字,这块内存地址在 Unicorn 引擎里根本没有被映射(Unmapped),所以 SO 一读就直接触发了内存访问异常,导致进程崩溃。 解决方案 要跑通这个逻辑,我们需要做两步:第一步是“骗”过它的 Hook 检测,第二步是给它返回一个假的 IMEI。
这个解释是否正确,我也不清楚,各位大佬如果有懂的可以评论回复。通过gemini给的方法,我发现压根不用在对应地址写入数据,只需要把内存空间开辟出来,读取内存不报错,这里就可以跑过去了。
当然,有可能是这里处理地不够好,导致后面生成的ckey长度会比frida hook出来的短上一些,好在可以正常使用。

继续给大模型,但是这里他判断错了,他把NR=65的方法给我了。
这里显示NR=20,svc=65。按照正己大佬《安卓逆向这档事》第二十六课、Unidbg之补完环境我就睡(下)中的解释,这里应该是JNI调用(svcNumber 不等于 0x0),但是后面又没有UnsupportedOperationException。目前超出小白的能力范畴了,使用了最简单粗暴的方法,啥也不干,直接return。(留个作业后续再看)
1 2 3 4 | // MySyscallHandler中if (NR == 20 && swi == 0x41) { return;} |
之前抄另一个文档,返回了Thread.currentThread(),后来发现这俩有区别,一个是java/lang/Thread,这里是android/app/ActivityThread,正常补就行。
暂时new了个空的给过去。

至此,大家应该和我一样,拿到最终的ckey了,但是因为很多地方处理并不是很完善,只能说跑出了可用的结果。另外如果想要跑encryptR_client,直接调用应该就能出,环境全部补好了的。
念念不忘,必有回响。前前后后开始->放弃->开始->放弃....重复了n次,最终借用gemini的能力跑通了,虽然在逆向学习中,自己仍然是小白,逆向的知识似乎也没有什么提升,但是通过使用大模型等能力完成多年未完成工程,心里还是十分开心的。
最后,如果还有精力,也会分享使用gemini + unidbg + ida还原算法的方法。整体来说本文难度还是比还原算法简单很多。
完结,撒花!!!
unidbg调用sgmain的doCommandNative函数生成某酷encryptR_client参数
念念不忘,必有回响
unidbg实现xx请求参数算法
《安卓逆向这档事》系列
unidbg升级到最新版后 跑不起来libsgmain.so
分享9.23 mtop
安卓逆向小案例——某电影票务APP加密参数还原-Unidbg篇
最后贴一个提示词(轻喷)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | ## 背景你现在是精通安卓逆向的工程师,需要你帮助完成unidbg调用一个so的签名。现在我已经通过frida,hook到了签名函数ckey的输入和输出,现在我希望你帮助我使用unidbg模拟so的环境,并成功计算出ckey。其中我可以给你ida解析的so的伪代码,以及帮你使用frida hook更多参数供你分析。其他ida的脚本,我也可以尝试帮你使用。(我是小白,如果需要ida插件,需要详细说明一次)## 要求1. unidbg补环境时,对于常见环境,可以直接补充,对于不常见或者拿不准的地方,可以使用frida hook到相应参数再补充。2. 每次回复,可以简单解释你的分析,不要太多,因为我看不太懂,我不需要知道原理,我只需要协助你运行代码,帮你获取so的伪代码。## 技能1. 可以让我协助你hook函数参数,inline hook等各种frida支持的方式。2. 可以让我给你提供so的伪代码。3. 可以让我使用ida的插件协助分析(注意,我是小白,如果使用这个技能,需要你详细指导我一次使用方法)。4. 其他可能有助于你分析的方法,可以教我使用。## 目前进展### unidbg脚本所有unidbg脚本 balabala### frida hook脚本hook脚本### frida hook结果hook结果### 简单解释现有hook结果1. 10101、10102都是初始化工作;2. 60901初始化avmp,并拿到实例;3. 60902是输入参数,并获取签名ckey;(重点调用)4. 10601是获取R,可以暂时忽略。## 任务基于目前frida hook的结果,协助我完成unidbg脚本补环境的过程,其中存在大量环境监测的地方,需要你凭借经验和frida hook,拿到真机环境数据,帮助我最终拿到ckey的模拟计算。 |
更多【Android安全-小白如何通过大模型跑通unidbg调用sgmain生成某ckey参数】相关视频教程:www.yxfzedu.com