首页
社区
课程
招聘
[翻译]Windows 8 ASLR Internals(Windows 8 地址空间布局随机化揭秘)
发表于: 2016-3-5 15:51 23632

[翻译]Windows 8 ASLR Internals(Windows 8 地址空间布局随机化揭秘)

2016-3-5 15:51
23632

.
.
前言

原文链接为  http://blog.ptsecurity.com/2012/12/windows-8-aslr-internals.html
这里只是对原文进行了翻译,并且尝试与 Windows 7 / Vista 的 ASLR 作出对比,对于原文博客中较有内涵的评论与作者的回复,也进行了适当的翻译。下面是原文 + 译文:


Windows 8 ASLR Internals
ASLR stands for Address Space Layout Randomization. It is a security mechanism which involves randomization of the virtual memory
addresses of various data structures, which may be attacked. It is difficult to predict where the target structure is located in the memory, and thus an attacker has small chances to succeed.

ASLR 代表“地址空间布局随机化”。它是一个涉及各种数据结构(可能会被攻击)的虚拟内存地址随机化的安全机制。要预测目标数据结构在
内存中的定位会变得很困难,因此攻击者很少有机会成功。


ASLR implementation on Windows is closely related to the image relocation mechanism. In fact, relocation allows a PE file to be loaded
not only at the fixed preferred image base. The PE file relocation section is a key structure for the relocating process. It describes how to modify certain code and data elements of the executable to ensure its proper functioning at another image base.

Windows 上的 ASLR 实现与映像重定位机制密切相关。事实上,重定位能够允许一个 PE 文件不仅是被加载到一个固定的首选映像基址。PE 文件的重定位节是重定位过程中的一个关键结构。它描述了如何修改可执行文件的某些代码和数据元素,以确保它被加载到另一个映像基址时能够正常工作。

The key part of ASLR is a random number generator subsystem and a couple of stub functions that modify the image base of a PE file,
which is going to be loaded.

ASLR 的关键环节是一个随机数生成器子系统,以及数个修改将被加载的 PE 文件映像基址的存根函数。

Windows 8 ASRL relies on a random number generator, which is actually a Lagged Fibonacci Generator with parameters j=24 and k=55 and which is seeded at Windows startup in the winload.exe module. Winload.exe gathers entropy at boot time and has different sources:
registry keys, TPM, Time, ACPI, and a new rdrand CPU instruction. Windows kernel random number generator and its initialization are described in detail in [1].

Windows 8 的 ASRL 依赖于一个随机数生成器,这实际上是一个参数为 j=24,K=55 的“滞后斐波那契数生成器”,它由 winload.exe 模块
(Windows 启动加载器)在 Windows 启动时“种下”。Winload.exe 在启动阶段从不同的来源收集熵:注册表键,TPM,Time,ACPI,以及一个新的 CPU 指令:rdrand 。
(译注:熵的概念起源于热力学,用于度量一个封闭系统的无序性,作者引用此术语来指代无序的随机性)
有关 Windows 内核随机数生成器及其初始化请参考
[1] [Chris Valasek, Tarjei Mandt. Windows 8 Heap Internals. 2012.]


We would like to give a small note about the new rdrand CPU instruction. The Ivy Bridge architecture of Intel processors has introduced the Intel Secure Key technology for generating high-quality pseudo-random numbers. It consists of a hardware digital random number
generator (DRNG) and a new instruction rdrand, which is used to retrieve values from DRNG programmatically.

关于新的 rdrand CPU 指令,需注意一些事项。Ivy Bridge 架构(第三代 i3~i7)的 Intel 处理器引入了“Intel Secure Key”技术,用于生成高质量的伪随机数。它包括一个硬件数字(digital)随机数生成器(DRNG),以及一个新的指令 rdrand,该指令用于以编程方式从 DRNG 获取值。

As a hardware unit, DRNG is a separate module on a processor chip. It operates asynchronously with the main processor cores at the frequency of 3 GHz. DRNG uses thermal noise as an entropy source. It also has a built-in testing system performing a series of tests to ensure high quality output. If one of these tests fails, DRNG refuses to generate random numbers at all.
作为一个硬件单元,DRNG 是处理器芯片上的一个独立模块。它使用主处理器核的 3 GHz 时钟频率异步运行。DRNG 使用热噪声作为一个熵源。它还具有一个内置的测试系统,可以进行一系列的测试,以确保高质量输出。假设这些测试的其中一个失败,则 DRNG 拒绝生成一切随机数。

The RDRAND instruction is used to retrieve random numbers from DRNG. The documentation states that theoretically DRNG can return nulls instead of random number sequence due to health test failure or if a generated random number queue is empty. However,
we were unable to drain the DRNG in practice.
Intel Secure Key is a really powerful random number generator producing high quality random numbers at a very high speed. Unlike other entropy sources, it is practically impossible to guess the initial RNG state initialized with rdrand instruction.

RDRAND 指令用于从 DRNG 处获取随机数。Intel 的相关文档指出:从理论上而言,由于健康性测试失败,或者如果生成的随机数队列为空,那么 DRNG 可以返回空值,而不是随机数序列。然而在实践中,我们无法用尽它生成的所有随机数(即队列为空的情况)。
Intel Secure Key 是一个非常强大的随机数生成器,能够以非常高的速度产生高质量的随机数。与其它的熵源不同,几乎不可能猜测出通过 rdrand 指令初始化的初始随机数生成器(RNG)的状态。


The internal RNG interface function is ExGenRandom(). It also has an exported wrapper function RtlRandomEx(). Windows 8 ASLR uses this function as opposed to the previous version that relied on the rdtsc instruction. The rdtsc instruction is used for retrieving a timestamp counter on a CPU, which changes linearly so that it cannot be considered a secure random number generator.
内部的 RNG 接口函数为 ExGenRandom() 。还有一个导出的包装函数(封装了 ExGenRandom())叫做 RtlRandomEx() 。
Windows 8  的 ASLR 使用了该函数。 而在此之前的 Windows 版本中,MiInitializeRelocations() 例程会执行一个 CPU 指令 rdtsc ,它将获取的 CPU 时间戳计数器
(TSC,由于它的值仅会线性地改变,也就是可预测,因此不被视为一种安全的随机数生成器。)
(译注:在 Windows vista[NT 6.0 内核] 的 MiInitializeRelocations() 例程中,会执行 rdtsc   指令 ,它将获取的 CPU 时间戳计数器保存在内核变量 MiImageBias 中,MiSelectImageBase() 例程随机加载 DLL 时,就会将 MiImageBias 作为 RtlFindClearBits() 函数的参数,在
MiImageBitMap 位图中随机查找并设置可用的比特位来标识 DLL 在进程地址空间中的加载基址,
MiSelectImageBase() 例程随机加载可执行文件(EXE)时,会调用 ReadTimeStampCounter() 例程,后者也通过 rdtsc 指令取得 CPU 时间戳计数器,然后将该值进行一系列算术运算并保存到一个叫做 Delta 的变量中,从 PE 文件可选头的首选映像基址减去这个 Delta 值,就得出新的加载基址。
在 Windows 7 [NT 6.1 内核] 的 MiInitializeRelocations() 例程中, 也依赖于 rdtsc 指令来初始化 MiImageBias,并且它同样被 MiSelectImageBase() 例程随机化 DLL 的加载基址时使用,仅有的区别在于,MiSelectImageBase() 例程随机加载可执行文件时,计算 Delta 值的
方式不同,关于 Win7 和 Vista 的 ASLR 源码实现可以参考下面这篇文章:http://bbs.pediy.com/showthread.php?t=206911
正如原文所述,由于 rdtsc 指令生成的随机数有一定的规律,带来了安全隐患,所以在 Windows 8 中,MiInitializeRelocations() 通过新的 ExGenRandom() 例程及 rdrand 指令初始化 MiImageBias,如下代码所示:

注意,MiInitializeRelocations() 只用来随机化 DLL(动态连接库)的加载基址,由 MiInitializeRelocations() 初始化的随机种子仅被 MiSelectImageBase() 中的 DLL 重定位逻辑使用。
而对于 EXE 可执行文件,MiSelectImageBase() 会自行调用 ExGenRandom() 来随机化 EXE 的加载基址


VOID MiInitializeRelocations()
{
        MiImageBias = ExGenRandom(1) % 256;          //直接用 ExGenRandom() 返回的值与 256 求模(只用 8 位来表示随机性),构建用于 32位 DLL 的随机种子

         //与 SizeOfBitMap 成员求摸,构建用于基址在 4GB 以下 DLL 的随机种子
        MiImageBias64Low = ExGenRandom(1) % MiImageBitMap64Low.SizeOfBitMap;

         // 随机种子 MiImageBias64High  用于映像基址在 4 GB 以上的 64 位 DLL     
        MiImageBias64High = ExGenRandom(1) % MiImageBitMap64High.SizeOfBitMap;

    return;
}




The core function of the ASLR mechanism is MiSelectImageBase. It has the following pseudocode on Windows 8.
ASLR 机制的核心函数是 MiSelectImageBase(),它在 Windows 8 中的伪代码如下所示:


//这个按照 64 KB 对齐的宏,其作用是把 x 加上 10 进制的 15,然后除以 16
#define MI_64K_ALIGN(x) (x + 0x0F) >> 4
#define MmHighsetUserAddress 0x7FFFFFEFFFF

/*定义一个枚举类型 _MI_MEMORY_HIGHLOW,其中只有 3 个常量,MiSelectBitMapForImage() 会根据检测出的 PE 文件类型,返回相应的常量值。注意,_MI_MEMORY_HIGHLOW 是一个全局数据结构,因此它可以被 MiSelectBitMapForImage() 与 MiSelectImageBase() 读取。*/
typedef PIMAGE_BASE ULONG_PTR;

typedef enum _MI_MEMORY_HIGHLOW
{
    MiMemoryHigh    = 0,                 //旨在用于 32 位 PE 文件映像
    MiMemoryLow     = 1,                 //用于 64 位 PE 文件映像,加载基址在 4GB 以下        
    MiMemoryHighLow = 2                //用于 64 位 文件映像,加载基址在 4GB 以上
} MI_MEMORY_HIGHLOW, *PMI_MEMORY_HIGHLOW;

/*  MiSelectBitMapForImage() 的返回值类型就是前面自定义 _MI_MEMORY_HIGHLOW。MiSelectImageBase() 会调用 MiSelectBitMapForImage(),后者通过传入的 SEGMENT对象指针,读取由映像加载器填充的相关成员的值,这些成员标识了 PE 文件映像是 32 位还是 64 位。对于不同类型的 PE 文件映像,MiSelectBitMapForImage() 返回不同的全局枚举常量值。 */
MI_MEMORY_HIGHLOW  MiSelectBitMapForImage(PSEGMENT pSeg)
{
                if ( !(pSeg->SegmentFlags & FLAG_BINARY32) )        //如果是 64 位 PE 文件:   
                {
                                if ( !(pSeg->ImageInformation->ImageFlags & FLAG_BASE_BELOW_4GB) )           //对于支持 4 GB 以上加载基址的 64 位 PE 文件:
                                {
                                                if (pSeg->BasedAddress > 0x100000000)
                                                {
                                                                return MiMemoryHighLow;        //如果首选加载基址大于 0x100000000 ,则返回 2
                                                }
                                                else
                                                {
                                                                return MiMemoryLow;            //否则,被视为加载基址在 4 GB 以下,返回 1
                                                }
                                }
                }

                return MiMemoryHigh;                 //如果是 32 位 PE 文件,返回 0(MiMemoryHigh 常量的值)
}

/* MiSelectImageBase() 的第二个参数是一个 SEGMENT 对象指针。SEGMENT 结构与 PE 文件中的段结构无关。前者是虚拟内存管理器内部使用的一个结构。SEGMENT 结构由操作系统加载器构建和填充(其成员)。操作系统加载器负责所有与解析 PE 文件相关的工作。SEGMENT 结构是由加载器填充的许多控制结构的其中一个。可以在 WinDbg 中输入“dt nt!_SEGMENT”命令来查看该结构的所有成员。此结构并未文档化,但是我们可以观察结构的字段名称,它给出了一些有意义的提示信息。此结构包含了一些与 PE 文件相关的信息,但仅止于此而已。因此内核中并没有与 PE 文件头中每个字段一对一映射的数据结构。简而言之,不同的内部内核结构用于解析并存储 PE 文件头中的不同字段和结构,以便加载器在构建进程地址空间时能够充分利用这些信息。
MiSelectImageBase() 的第一个参数 a1 ,它是通过 rcx 寄存器传递进来的,这个指针指向的某种形式的内核结构的类型不确定(void* )。所以我们无法将此结构与一个可用的微软公共调试器符号对应起来。另一种可能性是,由于源码经某种渠道从微软泄露的,可能被员工进行了一些模糊或者混淆处理,以保护核心技术。在 MiSelectImageBase() 的内部逻辑中,可以看到多次通过类似 a1->SelectedBase 等方式来访问这个不透明结构的成员。  */

PIMAGE_BASE  MiSelectImageBase(void* a1<rcx>, PSEGMENT pSeg)
{
                MI_MEMORY_HIGHLOW ImageBitmapType;                        //保存由例程 MiSelectBitMapForImage() 返回的值
                ULONG ImageBias;                               
                RTL_BITMAP *pImageBitMap;
                ULONG_PTR ImageTopAddress;                   /*  对于映像基址在 4 GB 以下的 64 位或 32 位映像,为 0x78000000,对于基址在 4 GB 以上的 64 位映像,为 0x7FFFFFE0000,ImageTopAddress 的用途类似于 Windows 7 / Vista 的 ASLR 中的 dwHighVA,减去该值才能得出最终的随机化地址  */

               
                ULONG RelocationSizein64k;                       
                MI_SECTION_IMAGE_INFORMATION *pImageInformation;
                ULONG_PTR RelocDelta;                       //RelocDelta = (pSeg->BasedAddress) - (a1->SelectedBase)
                PIMAGE_BASE Result = NULL;                        //最终返回的随机化基址

                // rsi = rcx
                // rcx = rdx
                // rdi = rdx

                pImageInformation = pSeg->ImageInformation;
                ImageBitmapType = MiSelectBitMapForImage(pSeg);

                a1->off_40h = ImageBitmapType;

                /* 当 ImageBitmapType 为 MiMemoryLow 时,使用 MiImageBitMap64Low 位图,它用于记录 64 位 DLL 映像在地址空间中的随机加载情况;(映像基址在 4 GB 以下)  
*/

                if (ImageBitmapType == MiMemoryLow)
                {
                               
                                ImageBias = MiImageBias64Low;
                                pImageBitMap = MiImageBitMap64Low;
                                ImageTopAddress = 0x78000000;
                }
                else
                {
                                /* 当 ImageBitmapType 为 MiMemoryHighLow 时,使用 MiImageBitMap64High 位图,它用于记录支持 4 GB 以上用户空间的 64 位 DLL 映像在地址空间中的随机加载情况;(映像基址在 4 GB 以上)*/
                                if (ImageBitmapType == MiMemoryHighLow)
                                {
                                       
                                        ImageBias = MiImageBias64High;
                                        pImageBitMap = MiImageBitMap64High;
                                        ImageTopAddress = 0x7FFFFFE0000;
                                }
                                else
                                {
                                        /* ImageBitmapType 为 MiMemoryHigh ,或者不是上面两者时,使用 MiImageBitMap 位图,它用于记录 32 位 EXE 映像在地址空间中的随机加载情况 */
                                        ImageBias = MiImageBias;
                                        pImageBitMap = MiImageBitMap;
                                        ImageTopAddress = 0x78000000;
                                }
                }

                // pSeg->ControlArea->BitMap ^= (pSeg->ControlArea->BitMap ^ (ImageBitmapType << 29)) & 0x60000000;
                // or bitfield form
                pSeg->ControlArea.BitMap = ImageBitmapType;

                //根据代码起始处的宏定义可知,RelocationSizein64k = ((pSeg->TotalNumberOfPtes) + 0x0F) >> 4
                RelocationSizein64k = MI_64K_ALIGN(pSeg->TotalNumberOfPtes);

                //如果映像为 DLL
                if (pSeg->ImageInformation->ImageCharacteristics & IMAGE_FILE_DLL)
                {
                                ULONG StartBit = 0;
                                ULONG GlobalRelocStartBit = 0;

                                //在位图中寻找并返回空闲的比特位
                                StartBit = RtlFindClearBits(pImageBitMap, RelocationSizein64k, ImageBias);
                                if (StartBit != 0xFFFFFFFF)
                                {
                                                //推测这里的 MiObtainRelocationBits() 在功能上等同于 Windows 7/ Vista 中的 RtlFindClearBitsAndSet()
                                                StartBit = MiObtainRelocationBits(pImageBitMap, RelocationSizein64k, StartBit, 0);
                                                if (StartBit != 0xFFFFFFFF)
                                                {
                                                                /* 对比 Windows 7/ Vista 的 DLL 加载基址计算过程  dwImageBase = (dwHighVA - ((dwStartIndex + usPageCountdiv16) * 16 * PAGE_SIZE)),可以看出与 Windows 8 的计算过程还是有一些差别的  */
                                                                Result = ImageTopAddress - (((RelocationSizein64k) + StartBit) << 0x10);

                                                                /* 这里的逻辑类似 Windows 7/ Vista 中的二次随机化,即当计算出的新地址与原始的映像基址相同时,再次随机化 */
                                                                if ( Result == ((pSeg->BasedAddress) - (a1->SelectedBase)) )
                                                                {
                                                                                GlobalRelocStartBit = MiObtainRelocationBits(pImageBitMap, RelocationSizein64k, StartBit, 1);
                                                                                StartBit = (GlobalRelocStartBit != 0xFFFFFFFF) ? GlobalRelocStartBit : StartBit;
                                                                                Result = ImageTopAddress - (RelocationSizein64k + StartBit) << 0x10;
                                                                }

                                                                a1->RelocStartBit = StartBit;
                                                                a1->RelocationSizein64k = RelocationSizein64k;
                                                                pSeg->ControlArea->ImageRelocationStartBit = StartBit;   
                                                                pSeg->ControlArea->ImageRelocationSizeIn64k = RelocationSizein64k;

                                                                return Result;
                                                }
                                }
                }
                else
                {
                                // 否则,映像为 EXE,假设已指定了加载基址,则直接返回,不作随机化处理
                                if (a1->SelectedBase != NULL)
                                {
                                                return pSeg->BasedAddress;
                                }

                                //如果 a1->SelectedBase 为 NULL,则继续执行下面检查
                                if (ImageBitmapType == MiMemoryHighLow)
                                {
                                                a1->RelocStartBit = 0xFFFFFFFF;
                                                a1->RelocationSizein64k = (WORD)RelocationSizein64k;
                                                pSeg->ControlArea->ImageRelocationStartBit = 0xFFFFFFFF;
                                                pSeg->ControlArea->ImageRelocationSizeIn64k = (WORD)RelocationSizein64k;

                                                /* 对于映像基址高于 4 GB 的 64 位可执行文件,通过下面算术运算来返回随机化的基址,基于 0x20001 取模的结果,意味着 17 位的随机性 */
                                                return  ((DWORD)(ExGenRandom(1) % (0x20001 - RelocationSizein64k)) + 0x7F60000)  << 16;
                                }
                }

                //对于映像基址低于 4 GB 的 32 位和 64 位可执行映像,仅接收了 8 位随机性(与 0xFE 取模)
                ULONG RandomVal = ExGenRandom(1);
                RandomVal = (RandomVal % 0xFE + 1) << 0x10;

                RelocDelta = (pSeg->BasedAddress) - (a1->SelectedBase);

                /* 执行一系列的错误检查,以增加 MiSelectImageBase() 本身逻辑的健壮性。注意,返回 0 表示随机化失败,将由主调函数进行处理 */
                if (RelocDelta > MmHighsetUserAddress)
                {
                                return 0;
                }

                if ( (RelocationSizein64k << 0x10) >  MmHighsetUserAddress )
                {
                                return 0;
                }

                if ( (RelocDelta + (RelocationSizein64k << 0x10)) <= RelocDelta )
                {
                                return 0;
                }

                if ( (RelocDelta + (RelocationSizein64k << 0x10)) > MmHighsetUserAddress )
                {
                                return 0;
                }

                if ( a1->SelectedBase + RandomVal == 0 )
                {
                                Result = pSeg->BasedAddress;
                }
                else
                {
                                if (RelocDelta > RandomVal)
                                {
                                        Result = RelocDelta - RandomVal;
                                }
                                else
                                {
                                                Result = RelocDelta + RandomVal;
                                                if (Result < RelocDelta)
                                                {
                                                                return 0;
                                                }

                                                if (((RelocationSizein64k << 0x10) + RelocDelta + RandomVal)  > 0x7FFFFFDFFFF)
                                                {
                                                                return 0;
                                                }

                                                if (((RelocationSizein64k << 0x10) + RelocDelta + RandomVal)  <  (RelocDelta + (RelocationSizein64k << 0x10))))
                                                {
                                                                return 0;
                                                }
                                }
                }

                //random_epilog
                a1->RelocStartBit = 0xFFFFFFFF;
                a1->RelocationSizein64k = RelocationSizein64k;
                pSeg->ControlArea->ImageRelocationStartBit = 0xFFFFFFFF;
                pSeg->ControlArea->ImageRelocationSizeIn64k = RelocationSizein64k;

                return Result;
}


As we can see, there are three different image bitmaps. The first one is for 32-bit executables, the second is for x64, and the third is for x64 with the image base above 4GB, which grants them a high-entropy virtual address.
正如我们所见, Windows 8 使用了三种不同的映像位图。第一个用于 32 位可执行文件(MiImageBitMap),第二个用于 x64
(MiImageBitMap64Low),第三个用于 4 GB 以上的 x64 映像基址(MiImageBitMap64High,它给予这些映像一个高熵的虚拟地址)。


The executables are randomized by a direct modification of the image base. As for the DLLs, ASLR is a part of relocation, and the random part of the image base selection process is ImageBias. It is a value that is initialized during the system startup.
源码中通过直接修改可执行文件的映像基址来对其进行随机化。至于 DLL,ASLR 属于其重定位的一部分,并且,映像基址选择过程的随机部分就是 ImageBias(它保存了三种内核全局变量之一:MiImageBias 或 MiImageBias64Low ,或  MiImageBias64High )。它的值在系统启动期间被初始化,请参考上面给出的 MiInitializeRelocations() 例程源码。

Image bitmaps represent the address space of the running user processes. Once an executable image is loaded, it will have the same address for all the processes that reference it. It is natural because of efficiency and memory usage optimization, since executables use
the copy-on-write mechanism.
ASLR implemented on Windows 8 can now force images, which are not ASLR aware, to be loaded at a random virtual address. The table below demonstrates the loader's behavior with different combinations of ASLR-relevant linker flags.

映像位图表示正在运行的用户进程的地址空间。一旦一个可执行映像被加载,对于所有引用它的进程都会有相同的地址。这是很自然的,因为考量到了引用效率和内存使用率优化,以及配合可执行文件使用的“写时复制”机制。
Windows 8 实现的 ASLR 能够对没有显式参与 ASLR 的映像,强制启用 ASLR,这样就能够加载到随机的虚拟地址。下表给出当使用不同的 ASLR 相关链接器标志组合时,映像加载器的 ASLR 相关行为:(绿色表示指定了该链接器标志,红色则表示没有指定)




*Cannot be built with MSVS because the /DYNAMICBASE option also implies /FIXED:NO, which generates a relocation section in an executable.
如果仅指定了 /DYNAMICBASE 链接器标志,它就隐含了 /FIXED:NO 链接器选项(这会在可执行文件中生成一个重定位节),然而,这样加载基址不会随机化,只有同时指定三个标志,或者至少前 2 个,才能随机化。

We can spot that the loader's behavior changed in Windows 8 — if a relocation section is available in the PE file, it will be loaded anyway. It also proves that ASLR and the relocation mechanism are really interconnected.
从上表可以发现,Windows 8 的映像加载器行为有所改变——如果 PE 文件中存在一个重定位节,那么无论如何,它都会被加载。这也证明了 ASLR 与重定位机制确实是相互关联的。

Generally we can say that implementation of the new ASLR features on Windows 8 doesn't much influence the code logic, that is why
it is difficult to find any profitable vulnerabilities in it. Entropy increase for randomizing various objects is in fact a substitution of a constant expression in a code. The code graphs also show that the code review has been done.

通常我们认为:Windows 8 实现的 ASLR 新功能并没有对代码逻辑构成太大的影响,这就是为什么要在其中找出任何有利可图的漏洞比较困难。为了随机化各种对象而增加的熵实际上是作为旧版 ASLR 代码中的常量表达式的一种替代。


[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 3
支持
分享
最新回复 (9)
雪    币: 341
活跃值: (138)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
2
好厉害 翻译点安卓的吧
2016-3-5 18:16
0
雪    币: 2044
活跃值: (237)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
3
mark...
2016-3-5 19:42
0
雪    币: 60
活跃值: (11)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
整一个PDF ?
2016-3-5 20:14
0
雪    币: 1604
活跃值: (640)
能力值: ( LV13,RANK:460 )
在线值:
发帖
回帖
粉丝
6
怎么弄?
2016-3-24 20:44
0
雪    币: 60
活跃值: (11)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
这是一个技术活,首先你的文档可以先做进WORD,然后使用WORD的导出功能,转换成PDF.
2016-3-25 09:58
0
雪    币: 1604
活跃值: (640)
能力值: ( LV13,RANK:460 )
在线值:
发帖
回帖
粉丝
8
原来 word 就能够转换,还以为多复杂,谢谢了!
2016-3-25 11:27
0
雪    币: 1604
活跃值: (640)
能力值: ( LV13,RANK:460 )
在线值:
发帖
回帖
粉丝
9
机器上没装 office,我用 google 云文档的在线转换功能,直接做成了 PDF ,提供为附件下载,你看排版怎么样
上传的附件:
2016-3-27 09:56
0
雪    币: 60
活跃值: (11)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
Lz, 不怎么用心
2016-3-31 14:13
0
雪    币: 346
活跃值: (25)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
11
感谢楼主!
2016-4-13 16:43
0
游客
登录 | 注册 方可回帖
返回
//