第一课 记事本的WriteFile API HOOK

前面一直在写dll hook技术的学习心得,但是现在又来写API hook的体会,很多人都不理解,为什么要学习API hook,dll hook已经那么强大,为什么还要把API hook单独拿出来学习?在我学习完这些内容之后,我深刻的认识到二者的差别,请听我说。

  1. 使用dll注入技术可以驱使目标进程强制加载用户指定的dll文件,使用该技术时,先在要注入的dll中创建hook代码和设置代码,然后在DllMain()中调用设置好的代码,注入的同时即可完成API的hook(多用于外挂,补丁),但是非常容易被检测到,会留下痕迹和证据
  2. 代码注入技术是DLL注入的一个分支,它比DLL注入技术更复杂,而且更好用,这种技术多用于恶意代码和病毒(使用汇编代码注入可以精准的控制程序的执行),因为代码注入操作很难被检测到(杀毒软件能有效检测dll注入,但是很难检测到代码注入),不过代码注入实现比较复杂,dll注入针对的是完整的PE映象,地址和代码都是我们已知的(可以理解为静态的),而代码注入是在程序执行代码和数据的时候动态获取API地址来使用的,访问程序中其他地址一不小心就会访问到错误地址。
  3. API hook技术可以精准的拦截到我们所需要的API,是代码注入和dll注入的一个应用实例(代码注入和dll注入技术的一个功能就是HOOK API),是逆向工程中最重要的一部分,经常用来破解网络验证和处理反调试。更深层次的逆向(系统内核的逆向都需要Hook API)

下面完整的看一下API hook 的概念和优势

举个例子

下面看一下API HOOK 所用到的所有方法

HOOK API 主要分为调试法和注入法,注入法又包括dll注入和代码注入。

下面讲解 使用调试的方法来HOOK API

RIP_EVENT

关于这种调试事件的文档资料非常少,即使提到也只是用“系统错误”或者“内部错误”一笔带过。既然如此,我们也不需要对其进行什么处理,只要输出一条信息或者干脆忽略它即可。

OUTPUT_DEBUG_STRING_EVENT

当被调试进程调用OutputDebugString时就会引发该类调试事件,OUTPUT_DEBUG_STRING_INFO结构体描述了关于该事件的详细信息。在MSDN中,对该结构体各字段的解释是:lpDebugStringData字段是字符串在被调试进程的进程空间内的地址;nDebugStringLength字段是以字符为单位的字符串的长度;fUnicode指示字符串是否Unicode编码的。根据我个人的实验观察,发现只有第一个字段的解释是对的。实际上,无论调用OutputDebugStringA还是OutputDebugStringW,字符串都会以ANSI编码来表示。如果是调用OutputDebugStringW,那么会先将字符串转换成ANSI编码之后再调用OutputDebugStringA(这个过程在MSDN内有描述)。所以fUnicode的值永远都是0,而nDebugStringLength是以字节为单位的字符串长度,而不是以字符为单位。

LOAD_DLL_DEBUG_EVENT

加载一个DLL模块之后引发该类调试事件,LOAD_DLL_DEBUG_INFO结构体描述了它的详细信息。lpImageName这个字段可能会使你想在调试器中输出DLL的文件名,然而这行不通。MSDN上的解释是,lpImageName的值是文件名字符串在被调试进程的进程空间内的地址,但是这个值可能为NULL,即使不为NULL,通过ReadProcessMemory读取到的内容也可能是NULL。所以,想通过这个字段获取DLL的文件名并不可靠。

那么,通过hFile字段来获取文件名如何?没有Windows API可以直接通过文件句柄获取文件名,想要这么做的话必须绕一个大圈子,实际上hFile是与dwDebugInfoFileOffsetnDebugInfoSize一起使用的,用于获取DLL文件的调试信息。一般情况下我们不需要这么做,所以只要调用CloseHandle关闭这个句柄即可。记住!关闭这个句柄非常重要,如果不这么做的话会引起资源泄漏。 

我的想法是,先通过EnumProcessModules枚举被调试进程的模块,然后通过GetModuleInformation获取模块的基地址,将这个基地址与LOAD_DLL_DEBUG_INFO结构体的lpBaseOfDll字段进行比较,如果相等的话就通过GetModuleFileNameEx获取DLL的文件名。可是我在实验这个方法的时候EnumProcessModules总是返回FALSEGetLastError返回299,这是什么原因呢?

这可能是因为当调试器在处理这类调试事件时,被调试进程还没有启动完毕,所需要的模块还未全部加载完成,所以无法获取它的模块信息。

UNLOAD_DLL_DEBUG_EVENT

卸载一个DLL模块的时候引发该类调试事件。一般情况下只要输出一条信息或者忽略它即可。

CREATE_PROCESS_DEBUG_EVENT

创建进程之后的第一个调试事件,CREATE_PROCESS_DEBUG_INFO结构体描述了该类调试事件的详细信息。该结构体有三个字段是句柄,分别是hFilehProcesshThread,同样要记得使用CloseHandle关闭它们!

EXIT_PROCESS_DEBUG_EVENT

被调试进程结束时引发此类调试事件,EXIT_PROCESS_DEBUG_INFO结构体描述了它的详细信息。或许你能做的只有输出dwExitCode这个字段的值。

CREATE_THREAD_DEBUG_EVENT

创建一个线程之后引发此类调试事件,CREATE_THREAD_DEBUG_INFO结构体描述了它的详细信息。同样要记住用CloseHandle关闭hThread字段!

EXIT_THREAD_DEBUG_EVENT

一个线程结束之后引发此类调试事件,EXIT_THREAD_DEBUG_INFO结构体描述了它的详细信息。对此同样也只能输出dwExitCode的值。

EXCEPTION_DEBUG_EVENT

发生异常时引发此类调试事件,EXCEPTION_DEBUG_INFO结构体描述了它的详细信息。对这种调试事件的处理是最麻烦的,因为异常的种类非常多,对每种异常的处理也不相同。另外,此类调试事件也是实现断点和单步执行的关键。

下面介绍HOOK API 的总体流程(简单的来说就是在API段首下CC断点,然后程序调用函数时会在停在函数段首,然后就可以对API进行各种操作了)

首先查看notepad.exe的PID

运行HOOKdbg.exe

 

这时钩子已经安装完毕,我们输入小写的hello i am shin然后保存

可以发现,保存出来的是大写的HELLO I AM SHIN

接下来开始讲解工作原理

在WriteFile() API段首下CC断点

查看堆栈缓冲区

 

可以看出ESP+8是www.xuenixiang.com的数据缓冲区,只要把ESP+8的内容换成大写再保存就实现了保存后转换为大写

初学者很容易认为是WriteFile()API的起始地址为7C8112FF,但是EIP的值应该是WriteFile()API的起始地址7C8112FF+1=7C811300

像OD这类应用范围很广的调试器,EIP值与设置断点的地址是相同的,并不现实INT3(0XCC)指令,这是OD为了向用户展示更方便的界面而提供的功能,也就是说,复写了INT3指令之后,若执行该命令,则EIP值增1,此时OD会将0XCC恢复为原来的字节,并调整EIP。

 

源代码如下(源码花了一天敲备注,非常详细,请仔细阅读!!!)

//shin-2018.10.11
#include "pch.h"
#include "windows.h"
#include "stdio.h"

LPVOID g_pfWriteFile = NULL;
CREATE_PROCESS_DEBUG_INFO g_cpdi;
BYTE g_chINT3 = 0xCC, g_chOrgByte = 0;

BOOL OnCreateProcessDebugEvent(LPDEBUG_EVENT pde)
{
	// WriteFile() API 地址
	g_pfWriteFile = GetProcAddress(GetModuleHandleA("kernel32.dll"), "WriteFile");

	// API Hook - WriteFile()
	//   更改第一个字节为0xCC (INT 3) 
	//   orginal byte(orginal意思是原始的)是g_chOrgByte
	memcpy(&g_cpdi, &pde->u.CreateProcessInfo, sizeof(CREATE_PROCESS_DEBUG_INFO));//从CreateProcessInfo结构体中拷贝
	                                                                  //出CREATE_PROCESS_DEBUG_INFO大小的字节给g_cpdi
	ReadProcessMemory(g_cpdi.hProcess, g_pfWriteFile,
		&g_chOrgByte, sizeof(BYTE), NULL); 
	    /*hProcess[in]远程进程句柄。 被读取者
		pvAddressRemote[in]远程进程中内存地址。 从具体何处读取
		pvBufferLocal[out]本地进程中内存地址.函数将读取的内容写入此处
		dwSize[in]要传送的字节数。要写入多少
		pdwNumBytesRead[out]实际传送的字节数.函数返回时报告实际写入多少 返回值是布尔类型*/
	WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile,
		&g_chINT3, sizeof(BYTE), NULL);
	//把0xCC覆盖第一个字节
	return TRUE;
}

BOOL OnExceptionDebugEvent(LPDEBUG_EVENT pde)
{
	CONTEXT ctx;//用context结构体定义一个变量
	PBYTE lpBuffer = NULL;
	DWORD dwNumOfBytesToWrite, dwAddrOfBuffer, i;
	PEXCEPTION_RECORD per = &pde->u.Exception.ExceptionRecord;//把pde结构体中的ExceptionRecord(异常记录)结构体给per

	// 判断异常记录的内容是否是断点异常(断点异常里面包括int3异常)
	if (EXCEPTION_BREAKPOINT == per->ExceptionCode)
	{
		// 判断断点地址是否为WriteFile()API地址
		if (g_pfWriteFile == per->ExceptionAddress)
		{
			// #1. Unhook
			//   将0xCC恢复为 original byte(前面从内存中读出的备份)
			WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile,
				&g_chOrgByte, sizeof(BYTE), NULL);//将函数修改后的首字节0xCC恢复为原首字节(6A)

			// #2. Thread Context 获取线程上下文,在使用该结构之前 要在ContextFlags中指定哪些寄存器组用来读写
			ctx.ContextFlags = CONTEXT_CONTROL;//即将使用如下寄存器
			                                   //DWORD   Ebp;
			                                   //DWORD   Eip;
			                                   //DWORD   SegCs;              // MUST BE SANITIZED
			                                   //DWORD   EFlags;             // MUST BE SANITIZED
			                                   //DWORD   Esp;
			                                   //DWORD   SegSs;
             GetThreadContext(g_cpdi.hThread, &ctx);//获取线程的各种状态

			// #3. WriteFile()的param2、3 值
			//  函数参数存在于相应进程的栈
			//   param 2 : ESP + 0x8
			//   param 3 : ESP + 0xC
			ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0x8),
			 &dwAddrOfBuffer, sizeof(DWORD), NULL);//Buffer 是一个字符串指针,把buffer的地址写到dwAddrOfBuffer
			ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0xC),
		     &dwNumOfBytesToWrite, sizeof(DWORD), NULL);//字符串长度读到dwNumOfBytesToWrite中
			/*hProcess[in]远程进程句柄。 被读取者
             pvAddressRemote[in]远程进程中内存地址。 从具体何处读取
             pvBufferLocal[out]本地进程中内存地址.函数将读取的内容写入此处
             dwSize[in]要传送的字节数。要写入多少
             pdwNumBytesRead[out]实际传送的字节数.函数返回时报告实际写入多少 返回值是布尔类型*/
			// #4.分配临时缓冲区
			lpBuffer = (PBYTE)malloc(dwNumOfBytesToWrite + 1);//加1是因为当前断在WriteFile() + 1位置
			//在调用malloc动态申请内存块时,一定要进行返回值的判断(这里没有判断),
			memset(lpBuffer, 0, dwNumOfBytesToWrite + 1);//把lpBuffer的内容全部赋值为0,赋值的长度为dwNumOfBytesToWrite + 1

			// #5. 恢复WriteFile()缓冲区到临时缓冲区
			ReadProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer,
				lpBuffer, dwNumOfBytesToWrite, NULL);//把buffer的内容存到lpBuffer
			printf("\n### 原字符串的内容 ###\n%s\n", lpBuffer);

			// #6. 将小写字母转换为大写字母
			for (i = 0; i < dwNumOfBytesToWrite; i++)
			{
				if (0x61 <= lpBuffer[i] && lpBuffer[i] <= 0x7A)//如果字符ASCll大于等于97小于等于122
					lpBuffer[i] -= 0x20;//将小写字母转换为大写字母
			}

			printf("\n### 转换成大写后的内容 ###\n%s\n", lpBuffer);//输出到CMD
			printf("------------------------------------\n技术详情访问www.xuenixiang.com\n");

			// #7. 将转换后的缓冲区复制到WriteFile()缓冲区
			WriteProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer,
				lpBuffer, dwNumOfBytesToWrite, NULL);//把lpBuffer的内容写到dwAddrOfBuffer(buffer)中
			/*hProcess
				由OpenProcess返回的进程句柄。
				如参数传数据为 INVALID_HANDLE_VALUE 【即 - 1】目标进程为自身进程
				lpBaseAddress
				要写的内存首地址
				再写入之前,此函数将先检查目标地址是否可用,并能容纳待写入的数据。
				lpBuffer
				指向要写的数据的指针。
				nSize
				要写入的字节数*/

			// #8.释放临时缓冲区
			free(lpBuffer);

			// #9. 将线程上下文的EIP更改为WriteFile()首地址
			//   (当前为WriteFile() + 1位置,int3命令之后)
			ctx.Eip = (DWORD)g_pfWriteFile;//将WriteFile首地址赋值给eip
			SetThreadContext(g_cpdi.hThread, &ctx);//将指定线程g_cpdi.hThread的context存到ctx结构体变量,为运行被调试进程做准备
			                                       //g_cpdi.hThread是被调试者的主线程句柄
			// #10. 运行Debuggee(被调试进程)
			ContinueDebugEvent(pde->dwProcessId, pde->dwThreadId, DBG_CONTINUE);
			Sleep(0);//释放当前线程的剩余时间片,调用sleep(0)之后,cpu会立即执行其他线程,经过一定时间再获得控制权
			        //(避免notepad正在调用writefile()API的过程中,后面的钩子代码在调用成功之前执行完毕,这样会导致内存访问异常)

			// #11. API Hook
			WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile,
				&g_chINT3, sizeof(BYTE), NULL);//将0xCC写到g_pfWriteFile(函数首地址)——继续设置断点

			return TRUE;
		}
	}

	return FALSE;
}

void DebugLoop()
{
	DEBUG_EVENT de;//用DEBUG_EVENT结构体生成一个变量de,de继承结构体的属性和内容
	DWORD dwContinueStatus;//继续状态
	// 等待被调试者发生事件
	while (WaitForDebugEvent(&de,INFINITE))//infinite=0xFFFFFFFF 二者可以替换,都代表无穷等待
		                                  //第一个参数指向event结构,这个结构描述了一个调试事件,第二个参数为等待事件的毫秒数。
		                                  //该函数会将调试事件写入de这个调试事件结构体中,并返回BOOL类型值(0或1)
    {
		dwContinueStatus = DBG_CONTINUE;

		// 被调试进程生成或者附加事件
		if (CREATE_PROCESS_DEBUG_EVENT == de.dwDebugEventCode)//如果DEBUG_EVENT结构体中的调试事件是CREATE_PROCESS_DEBUG_EVENT就继续执行
		                                      //CREATE_PROCESS_DEBUG_EVENT(创建进程之后发送此类调试事件,这是调试器收到的第一个调试事件)
		{
			OnCreateProcessDebugEvent(&de);//进程创建成功之后会把DEBUG_EVENT当参数传给该函数
		}
		// 异常事件
		else if (EXCEPTION_DEBUG_EVENT == de.dwDebugEventCode)
		{
			if (OnExceptionDebugEvent(&de))//出现异常之后会执行该函数
				continue;
		}
		// 被调试进程终止事件
		else if (EXIT_PROCESS_DEBUG_EVENT == de.dwDebugEventCode)//如果事件是进程终止,就跳出调试器循环(while)
		{
			// 被调试者终止 -> 调试器终止
			break;
		}

		// 再次运行被调试者
		ContinueDebugEvent(de.dwProcessId, de.dwThreadId, dwContinueStatus);
	}
}

int main(int argc, char*argv[])//argc是参数个数,argv是参数内容的数组指针
{
	system("title www.xuenixiang.com");
	DWORD dwPID;//声明一个双字空间用来存输入进来的PID

	if (argc != 2)//hookdbg必须有2个参数 
	{
		printf("\nUSAGE : hookdbg.exe <pid>\n");
		return 1;
	}

	// Attach Process(附加进程部分)
	dwPID = atoi(argv[1]);//argv[0]=程序名 argv[1]=PID 把输入的PID从char类型转换为int类型
	if (!DebugActiveProcess(dwPID))//如果附加失败,会返回0,if语句会执行
	{
		printf("DebugActiveProcess(%d) failed!!!\n"
			"Error Code = %d\n", dwPID, GetLastError());//获取错误参数
		return 1;
	}

	// 调试器循环
	DebugLoop();

	return 0;
}

下面是源码分析,会整体性的分析hook过程

:Main()

 

Main函数以程序运行参数的形式接收要钩取API的进程的PID,然后通过DebugActiveProcess()API将调试器附加到该运行的进程上,开始调试。然后进入DebugLoop()函数,处理来自被调试者的调试事件。(也可以通过CreateProcess()API,从一开始就直接以调试模式运行进程)

 

二:DebugLoop()

WaitForDebugEvent(

    __in LPDEBUG_EVENT lpDebugEvent,

    __in DWORD dwMilliseconds

)

 

 

 


 

typedef struct _DEBUG_EVENT {

    DWORD dwDebugEventCode;

    DWORD dwProcessId;

    DWORD dwThreadId;

    union {

        EXCEPTION_DEBUG_INFO Exception;

        CREATE_THREAD_DEBUG_INFO CreateThread;

        CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;

        EXIT_THREAD_DEBUG_INFO ExitThread;

        EXIT_PROCESS_DEBUG_INFO ExitProcess;

        LOAD_DLL_DEBUG_INFO LoadDll;

        UNLOAD_DLL_DEBUG_INFO UnloadDll;

        OUTPUT_DEBUG_STRING_INFO DebugString;

        RIP_INFO RipInfo;

    } u;

} DEBUG_EVENT, *LPDEBUG_EVENT;

ContinueDebugEvent是一个使调试器继续运行的API

定义如下

ContinueDebugEvent(

    __in DWORD dwProcessId,

    __in DWORD dwThreadId,

    __in DWORD dwContinueStatus

    );

 

ContinueDebugEvent() API 的 最 后 一 个 参 数 dwContinueStatus 的 值 为 DBG_CONTINUE 或  DBG_EXCEPTION_NOT_HANDLED。若处理正常,则其值设置为DBG_CONTINUE;若无法处理,或希望在应用程序的SEH中处 理,则其值设置为DBG EXCEPTION NOT HANDLED

 

DebugLoopO函数处理3种调试事件,如下所示。

□ EXIT_PROCESS_DEBUG_EVENT

□ CREATE_PROCESS_DEBUG_EVENT

□ EXCEPTION_DEBUG_EVENT

下面分别看看这3个事件。

 

 

 

 EXIT_PROCESS_DEBUG_EVENT

被调试进程终止时会触发该事件。本章的示例代码中发生该事件时,调试器与被调试者将一 起终止。

CREATE_PROCESS_DEBUG_EVENT-OnCreateProcessDebugEvent()

OnCreateProcessDebugEvent()是 CREATE_PROCESS_DEBUG_EVENT 事件句柄,被调试进程启动(或者附加)时即调用执行该函数。

 

 

 

 

 

typedef struct _CREATE_PROCESS_DEBUG_INFO {

    HANDLE hFile;

    HANDLE hProcess;

    HANDLE hThread;

    LPVOID lpBaseOfImage;

    DWORD dwDebugInfoFileOffset;

    DWORD nDebugInfoSize;

    LPVOID lpThreadLocalBase;

    LPTHREAD_START_ROUTINE lpStartAddress;

    LPVOID lpImageName;

    WORD fUnicode;

} CREATE_PROCESS_DEBUG_INFO, *LPCREATE_PROCESS_DEBUG_INFO;

g_chOrgByte变量中存储的是WriteFile()API的第一个字节,后面进行恢复时会用到,然后使用WriteProcessMemory()API将WriteFile()API第一个字节更改为0xCC

EXCEPTION_DEBUG_EVENT-OnExceptionDebugEvent()

EXCEPTION_DEBUG_EVENT-OnExceptionDebugEvent()是EXCEPTION_DEBUG_EVENT事件句柄,它处理被调试者的Int3指令,下图为代码实现过程

  1. 脱钩(恢复原代码)

2.获取线程上下文(通过对Context结构体的读取来获得当前的线程信息,例如esp,ebp等寄存器的内容)

在写脱壳脚本时,经常用到context结构体中的esp值充当返回值

在使用CONTEXT结构之前 要在ContextFlags中指定哪些寄存器组用来读写

我们要获取WriteFile的缓冲区内容,要用到esp,所以需要获取CONTEXT_CONTROL结构体

 

(如果要用eax等寄存器,则需要让ContextFlags =CONTEXT_INTEGER)

WINBASEAPI

BOOL

WINAPI

GetThreadContext(

    __in    HANDLE hThread,

    __inout LPCONTEXT lpContext

);

 

获取WriteFile()param

 

 

 

 

 

存储在dwAddrOfBuffer中的数据缓冲区地址是被调试者(notepad.exe)虚拟内存空间中的地址

 

把小写字母转换为大写字母后覆写在WriteFile()缓冲区

把线程上下文的EIP修改为WriteFile()起始地址

将线程上下文的EIP更改为WriteFile()首地址(当前为WriteFile() + 1位置,int3命令之后)

修改好CONTEXT.Eip成员后,调用SetThreadContext()API来修改


 

SetThreadContext(

    __in HANDLE hThread,

    __in CONST CONTEXT *lpContext

);

运行调试进程

 

全部准备完成后,接下来就正常调用WriteFile()API了,调用ContinueDebugEvent()API就可以重启被调试进程,使之继续运行。

 

继续HOOK(达到循环获取的目的)

 

OnExceptionDebugEvent() ContinueDebugEvent() ,为   Sleep(0)

Sleep(0) 线 线 CPU 片。也 Sleep(0) CPU 线 程(Notepad.exe) 线 WriteFiIe()API。然 HookDbg.exe, Sleep(0) 钩子 码(WriteProcessMemory()API) 行。若 Sleep(0) Notepad.exe WriteFile()API HookDbg.exe会尝试将WriteFile()API的首字节修改为OxCC。若运气不佳,这可能会导致内存访问异常。(避免notepad正在调用writefile()API的过程中,后面的钩子代码在调用成功之前执行完毕)

 

最近比较忙,一个月才搞定~~~

                                                                                                                                                             2018.11.1-shin

  • 9
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
好的,以下是一个简单的记事本应用程序的实现步骤: 1. 创建一个新的Android项目并添加必要的依赖项。 2. 在布局文件中创建一个EditText元素,以便用户可以输入文本。 3. 在MainActivity中获取EditText元素的引用,并设置一个监听器以响应用户输入。 4. 在监听器中,您可以使用SharedPreferences API将用户输入保存在设备上。 5. 添加一个菜单选项,以便用户可以打开和保存文本文件。您可以使用FileChooser API来让用户选择要打开和保存的文件。 6. 在菜单选项的响应函数中,您可以使用File API来读取和写入文件。 7. 最后,您可以在应用程序中添加其他功能,例如搜索、替换和撤销/重做等。 下面是一个简单的代码示例: ``` public class MainActivity extends AppCompatActivity { private EditText mEditText; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mEditText = findViewById(R.id.editText); // 设置EditText的监听器 mEditText.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { // 在文本改变之前执行的代码 } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { // 在文本改变时执行的代码 } @Override public void afterTextChanged(Editable s) { // 在文本改变之后执行的代码 saveText(s.toString()); } }); } // 保存用户输入 private void saveText(String text) { SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); editor.putString("text", text); editor.apply(); } // 读取保存的用户输入 private String loadText() { SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE); return prefs.getString("text", ""); } // 创建菜单 @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_main, menu); return true; } // 处理菜单选项的点击 @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.action_open: // 打开文件 openFile(); return true; case R.id.action_save: // 保存文件 saveFile(); return true; default: return super.onOptionsItemSelected(item); } } // 打开文件 private void openFile() { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("*/*"); startActivityForResult(intent, 1); } // 保存文件 private void saveFile() { Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("*/*"); intent.putExtra(Intent.EXTRA_TITLE, "note.txt"); startActivityForResult(intent, 2); } // 处理文件选择器的结果 @Override public void onActivityResult(int requestCode, int resultCode, Intent resultData) { if (resultCode == Activity.RESULT_OK) { Uri uri = null; if (resultData != null) { uri = resultData.getData(); } if (requestCode == 1) { // 读取文件 readFile(uri); } else if (requestCode == 2) { // 写入文件 writeFile(uri); } } } // 读取文件 private void readFile(Uri uri) { try { InputStream inputStream = getContentResolver().openInputStream(uri); BufferedReader reader = new BufferedReader(new InputStreamReader( inputStream)); String line; StringBuilder text = new StringBuilder(); while ((line = reader.readLine()) != null) { text.append(line); text.append('\n'); } mEditText.setText(text.toString()); inputStream.close(); } catch (Exception e) { e.printStackTrace(); } } // 写入文件 private void writeFile(Uri uri) { try { OutputStream outputStream = getContentResolver().openOutputStream(uri); outputStream.write(mEditText.getText().toString().getBytes()); outputStream.close(); } catch (Exception e) { e.printStackTrace(); } } } ``` 这只是一个简单的示例,您可以根据您的需求进行更改和扩展。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xuenixiang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值