重新创建一个旧的“肮脏的gameev trick”(2019)

2021-03-28 09:51:17

有一个故事,在我的推特喂食每6个月左右的故事。它的原始版本来自2013年发布的Gamasutra文章,其中包含了在以前的游戏中使用的各种“肮脏”技巧的故事集合。文章中有很多有趣的故事,但是在令人敬畏方面,一个人站在剩下的方面。我已经复制了下面的具体故事,这样即使在原始链接的不太可能发生死亡的情况下也是有道理的。

(S)ELF开采Jonathan Garrett,Insomniac Games棘轮和言辞:上升你的arsenal是一个在没有修补代码或数据的情况下发货的在线标题。这是不幸的。每次发射时,游戏下载并显示最终用户许可协议。这是存储在静态缓冲区中的ASCII字符串。此缓冲区从服务器填充,而无需检查大小在缓冲区内且#39; s容量。我们利用了这一事实来使EULA下载溢出到足够远的静态缓冲区来覆盖已知的全局变量。此变量恰好是特定网络数据包的函数回调处理程序。安装此处理程序后,我们可以发送网络数据包导致跳转到覆盖全局中的地址。该地址是指向Eula数据中早期存储的一些有效载荷代码的指针。 EULA缓冲区的实际结束与覆盖的全局之间存在有价值的数据,因此有效载荷代码的第一个工作是恢复此垃圾数据。一旦完成,事情就会恢复正常,可以完成实际的修补工作。一个并发症是EULA文本被复制与Strcpy。当它找到0字节(通常是字符串的末尾时,Strcpy结束。我们的字符串包含的代码通常包含0字节。因此,我们突变了编译的代码,使得它没有包含零字节,并仔细制作了一块剪发的引导asm,以使其突变。到底,Hack看起来如下所示:1。发送超大的EULA 2.溢出EULA缓冲区,杂项数据,回调处理程序指针3.发送数据包触发处理程序4.游戏跳转到由处理程序指向的引导码。引导程序解码有效载荷数据6.有效载荷下载和恢复脚步的杂项数据7.修补程序执行外卖:包括您发货的游戏中的修补代码,而Don' t使用无限的strcpy。

足以说这个故事不是现代游戏开发的例子,但我认为这就是让它如此吸引人。我在工作中的大部分时间都花在巨大的CodeBases中排除了由分层的抽象组成的巨大代码库,这些抽象层分层分层为第三方库和遗留码。这是与此相反的极地相反,我想给我一些它。所以这是我如何在OS X上重建它的故事。

我想通过这篇文章来说,这篇文章将包含很多可怕的装配。在我开始这个项目之前,我没有写得很多装配,我相信它会显示。所说的,让我们开始吧!

在这个故事中跳出来的第一件事是关于在游戏中向网络发送机器代码的一部分。尽管在后智之明是显而易见的,但它从来没有发生过这一点。在这篇文章中有一些帮助,我能够证明这也将在OS X上工作。首先,我写了一个快速的装配(在这种情况下,足以拨打出口(42):

将其与“作为”工具内置的OS X组装,并使用Objdump拆卸它以获取十六进制机器代码字节:

1FF5:66 BF 2A 00 MOVW $ 42,%DI1FF9:B8 01 00 00 02 MOVL $ 33554433,%EAX1FFE:0F 05 SYSCALL

以上返回值42,“返回0”语句永远不会被执行,这很酷。但是,这不足以证明任何东西,因为它仅在代码字符串是常量时工作。尝试将该字符串复制到其他(非常量)缓冲区,然后执行立即失败的指令:

int main(void){char * code =" \ x66 \ xbf \ x2a \ x00 \ xb8 \ x01 \ x00 \ x00 \ x02 \ x0f \ x05 \ x0f \ x0f \ x05 \ x0f \ x05 \ x0f \ x05 \ x0f \ x05 \ x0f \ x0f \ x0f \ x0f \ x0f \ x0f \ x0f \ x0f \ x0f \ x0f \ x05" ; char buff [256]; memcpy(buff,code,256); ((void(*)())buff)(); // exc_bad_access返回0将失败; }

事实证明,OS X有内存保护,以帮助防止人们正在进行这些Shenaningans。如果您使用Arguments“-WL,-Allow_stack_execute”在命令行上编译,Clang将愉快地让此代码运行正好。实际上,该参数将允许上述代码在堆栈,BSS部分或数据部分上工作。

请注意,无论我做了什么,我无法获取Xcode 10要识别该编译器标志,它必须是命令行。值得注意的是,如果使用此标志汇编Object-C代码(或Object-C ++),则标志将无法正常运行。我可能会错过一些东西,但我感到无聊,只是倒回命令行/普通C ++,而不是继续与之斗争。

Playstation 2在进入行业之前已经很好,但基于谷歌曲并询问一些有一些经验的同事,似乎PS2似乎有同样的记忆安全性,所以我感觉不太糟糕关于禁用OS X来完成此项目。

我的下一个目标是使用缓冲区溢出将函数指针重定向到我控制的缓冲区。我之前从未故意溢出缓冲区,但是男孩我还有经验跟踪和修复记忆脚部,所以这感觉很自然(理论上)。在实践中,这有点混乱。考虑以下代码:

void你好();静态char buff [32];静态void(* targetfunc)(); int main(int argc,const char ** argv){targetfunc = hello;得到(buff); targetfunc();返回0; void hello(){printf(" hello world \ n"); }

虽然此代码绝对将绝对崩溃,但它不保证编译器在我们可执行文件的BSS部分中的静态变量以相同的顺序定位在代码中。在我的情况下,他们实际上位于我可执行文件中的相反顺序中,正如您在跳跃输出片段中看到的那样。

__zl10targetfunc:// targetfunc0000000100001020 dq 0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000数据XREF = _MAIN + 29,_MAIN + 52000000010000001028 DB 0x00; '' 0000000100001029 db 0x00; '' 000000010000102A DB 0x00; '' 000000010000102b db 0x00; '' 000000010000102c db 0x00; '' 000000010000102d db 0x00; '' 000000010000102E DB 0x00; '' 000000010000102f db 0x00; '' __zl4buff:// buff0000000100001030 db 0x00; '' ;数据XREF = _MAIN + 360000000100001031 DB 0x00; '' 0000000100001032 db 0x00; '' 0000000100001033 db 0x00; '&#39 ;; (Buff下面继续)

unluffly,这意味着尝试,因为我可能会,我无法使用get()调用来更改targetfunc指针的值。经过一系列实验后,我发现(至少对于我的琐碎示例),Clang按照代码中遇到的顺序排列在BSS部分中的变量,因此在Get()调用之前重写代码以分配给Buff事情(下面的例子)。

void hello(){printf(" hello world \ n");静态char buff [32];静态void(* targetfunc)(); int main(int argc,const char ** argv){buff [0] =' c' ; targetfunc = hello;得到(buff); targetfunc();返回0; }

当然,如果两个变量位于可执行文件中的同一部分中,则所有上面只能保持true。例如,如果在声明时初始化TargetFUNC,则如下所示:

它将被放置在可执行文件的数据部分而不是BSS部分(因为它具有初始值)。这并不排除我溢出(我不认为),但这表明我也必须担心编译器将BSS部分和可执行文件中的数据部分置于可执行文件中的顺序。这似乎比这个项目的目的更有价值,所以我只是一直在BSS中保持所有东西。

似乎上面的代码使得一个正确制作的输入字符串可以溢出并将新地址写入函数指针,因此我决定给出镜头。我可执行文件中的Buff的地址为0x0000000100001020。为了能够输入此值来获取(),需要将其转换为ASCII。许多地址为零字节,它没有与它相关的ASCII字符,因此我必须通过按下控制+空格来在终端中输入它们。非零字节为01,10和20,其中两个是非可打印的字符,即我最终从网站上结束并粘贴,以便我不必弄清楚如何键入它们。最后一个,20,是空间字符('')。在终端中,它看起来像这样(注意最后的空间字符):

复制和粘贴上述字符串与实际粘贴为字节01和10的ASCII字符中的实际粘贴不一样,这只是终端如何确定输入这些字符。

除了令人讨厌的进入之外,这不起作用,因为我忘记了关于endianness,并且需要重新排列这个输入,以便将地址指定为一个小endian值。难以花时间比我愿意在博客帖子中承认。正确的字符串看起来像这样:

最后,我可以显现地(使用LLDB打印TargetFunc的地址)使用缓冲区溢出来设置指针。可悲的是,如果我在没有LLDB附加的情况下尝试了相同的伎俩,事情就会失败。事实证明,OS X有一个备份安全功能,它是袖子,以困扰我创造世界上最不安全的应用程序的计划。

ASLR或地址空间布局随机化是一种安全技术,它重新排列可执行文件的关键区域的位置,包括(至少在OS X Mojave)中。这意味着每次在没有LLDB附加的情况下运行测试应用程序,就会随机化了字符缓冲区的地址。

ASLR的概念于2001年发布,首先在2003年的“主流”OS中使用(根据维基百科至少)。鉴于PS2于2000年推出,我相对相信,我们的故事的硬件上没有这样的东西。我还发现了关于游戏机安全性的这个演示文稿,这表明ASLR没有在PS4之前出现索尼控制台。这意味着就像以前一样,我可以很好地禁用我的可执行文件上的这个安全功能。这是由另一个Clang标志,“-wl,-no_pie”完成的,其中饼图是指“位置裸照可执行文件”。与此外不同,可以在Xcode项目中启用此标志,只需转到“构建设置”并启用“生成位置依赖的可执行文件”。

与该国旗进行编译给了我一个可爱的小二进制二进制文件,这一直在相同的内存地址处保持凹痕变量。

既然我已正确将TargetFunc指针重定向到我的缓冲区,那似乎是下一步是实际将一些代码写入该缓冲区来执行。要保持简单的事项,我通过重用了先前称为Exit(42)的代码字符串开始。不幸的是,我的代码字符串中的许多十六进制值都无法以ASCII表示,因此我决定放弃使用gets()并写下一个小python服务器将代码字符串通过套接字传递给我的程序。无论如何,我都需要这样做这一点,所以这觉得这是进步的。

#include< sys / socket.h> #include< netinet / in.h> #include< arpa / inet.h> #include< cstring> #include< stdio.h>静态char buff [32];静态void(* targetfunc)(); void hello(){printf(" hello world \ n"); } int main(void){buff [0] =' c' ; targetfunc = hello; const int server_port = 10002; const char * server_address =" 127.0.0.1" ; const int buff_len = 64; struct sockaddr_in sockaddr = {0}; Sockaddr。 sin_family = af_inet; Sockaddr。 sin_port = htons(server_port); Inet_pton(af_inet,server_address,& sockaddr。sin_addr); int sockethandle =套接字(af_inet,sock_stream,0); Connect(Sockethandle,(Struct Sockaddr *)& sockaddr,sizeof(sockaddr)); recv(佐替兰德,& buff,buff_len,0); targetfunc();返回0; }

只要我达到ASLR,就会完全努力工作。我在这里停下来庆祝这个项目,并将其放弃一个月。

下载和执行汇编代码已经非常棒,但鉴于我的最终目标是能够使用此系统修补游戏,似乎是如果我可以使用该程序集来修复不同部分的错误,那似乎很酷程序。我已经使用mprotect将页面标记为在其他项目中的读/写保护(用于跟踪内存踩踏),因此使用它将页面标记为可执行文件不是一个巨大的延伸。我仍然写了一个小型测试程序,以确保它有效。

在调试中运行时,下面的代码将返回0而不是42,因为它会修改Wontexit42()函数返回false。 Clang将在-O0上方编译,但对我来说并不重要,因为,在真实的项目中,我将成为手写汇编来做到这一点。

#include< sys / mman.h> #include<记忆> #include< unistd.h> #include< stdint.h> bool wontexit42(){返回true; BOOL不应该爆炸42(){返回false; int main(int argc,const char * argv []){int64_t pagesize = getPageSize(); UINT8_T *应该=(UINT8_T *)&应该是42; UINT8_T *不应该=(UINT8_T *)&不应该是42; int64_t应该pageddr = pagesize *(int64_t(应该)/ pagesize); uint8_t * portpage =(uint8_t *)应该是pageaddr; mprotect(不适,页面,prot_read | prot_exec | prot_write); Memcpy(应该,不应该,64);返回wontexit42()? 42:0; }

现在,技术上,上面的代码是依赖于未定义的行为,因为POSIX标准指定MProtect的行为是未定义的,除非它在MMAP指针上运行,但操作系统X Mojave似乎很高兴能够这样做。此外,Memcpy Call中的64个字节大小是我从空中退出的总垃圾,但它足以让测试程序。

一种警告以这种方式修补代码是这种方式,任何改变都需要保持目标函数相同的大小,因为这不会在内存中的其余功能周围移动(并且我甚至不想考虑尝试)。或者,可以添加完全新的功能,假设有可用于存储它的内存。当我在缓冲区中存储汇编代码并在那里执行它时,我已经在上面做到了这一点,所以我不会再去belabour。

最后,我觉得我知道足以尝试实际在一个真正的项目中重建gamasutra的故事,我建立了一个小游戏,用作目标可执行文件。我开始使用一个匹配的游戏,使用了用于图形的金属,但厌倦了在包括ObjectO-C代码的项目中的制作--Allow_stack_execute工作,所以我报废了并建立了一个带有ncurses的快速蛇游戏。游戏很糟糕,但这不是真的,所以在你读书时,你可以试图假装我在谈论一些完全令人敬畏的AAA项目,如果它有所帮助。

(可怕的)代码在这里github上。大多数是无关紧要的,但几个比特与此博客文章有关。首先是我如何设置一些关键的静态vars:

静电焦埃拉[1024];静止(* packethandler)();静态int随机; int main(){memset(Eula,0,Eula_len);随机= 42; packethandler = handlenotificationpacket; //省略其余的代码

Clang将在解析代码(或至少在所有测试中所做的情况)时首次遇到的顺序将这些静态变量定位在BSS部分中,所以任何尝试通过溢出EULA来覆盖Packethandler指针还需要缓冲器,以踩踏随机存储的任何值。我有效载作业的一部分将确保在游戏使用之前将随机值设置为42。

游戏首先从服务器下载数据并将其临时到EULA缓冲区中。在服务器发送EULA之后,它就立即发送将触发到Packethandler()函数的调用的数据包。直到我把Packethandler指向EULA Buffer,我无法做蹲下,所以这是我的第一件事。这比上次使用溢出来设置指针的速度略微棘手,因为现在机器代码正在逐步进行strcpy,这意味着它不包含任何空字节。但是,最初,这并不重要,因为我只是想设置packethandler指针(它是0000000000000092b0),并且是小endian意味着我只实际需要编写0x92b0。

发送另外4个字节的\ x03以填充随机和函数指针之间的填充物

由于它有点长,我不会显示我曾经在这里执行此操作的Python,但如果您有兴趣,您可以在此处查看。

那部分很容易,但一旦工作,游戏就会在收到数据包时立即崩溃触发到Packethandler()的调用,因为Eula缓冲区中没有任何值。这有点被吸了,所以我的下一步是让Eula缓冲区实际做点什么。作为概念证明,我开始重新启动我之前使用的退出(42)代码字符串。原始代码字符串中有几个空字节,所以它需要一些按摩。作为一种进修,这是机器代码的原始位:

幸运的是,原始代码可以重新开始,很简单地解决问题。我刚刚添加了一些不必要的数学操作,以避免在其中需要任何带空字节的指令:

.text.globl _main_main:mov $ 25400,%di sub $ 25358,%di mov $ 0x2,%al shl $ 24,%eax增加0x1,%al syscall

用AS和使用Objdump组装这一点来让我六角形字节给了我以下,Strcpy友好,机器代码:

修改Python Server脚本以发送这只是用此代码字符串替换第一组\ x01字节的问题,并且蛇游戏在我有机会接受EULA之前返回值42。这是很棒的,但它不觉得我在我试图做真正的工作时,我的重写总成是为了避免空字节将是非常可扩展的。原始故事谈到需要编码/解码指令以允许发送空字节,以便是我的下一个项目。

我不知道insomniac的团​​队是否做得更加奇特,而是为了我的目的,我需要做的就是用0xcd替换我的机器代码字符串中的所有空字节,并写一些走向EULA缓冲区的字节的装配( strcpy之后)0xcd的实例为0x00。我可能只是幸运,但我在这个项目的其余部分写的代码都没有遇到有关的有效0xcd字节的问题,而是意外地踩过这个问题。

要获取该组件的机器代码字符串,我实际上刚刚结束将其写成一个单独的程序并使用十六进制恶魔提取十六进制字节

.text.globl _main_main : movabsq $ 0x1111111111111111 , RAX % $ movabsq 0x1111111111107E26 , RCX % % SUBQ RCX, 分 RAX % # 结果是 解码块 MOV % RAX , RDX % $ MOV 为0xFFFF , DX % $ 子 为0xFFFF 后 的 代码 地址 ,%dx#zero dx在机器代码#循环中没有得到空,在这里开始cmpb $ 0xcd,(%rax)jne。+ 6 sub $ 0xcd,(%rax)#跳转到这里,如果不是== 0xcd增加$ 0x1, %rax增加$ 0x1,%dx cmp $ 0x3d0,%dx#1035字节总数,引导程序59个字节,解码接下来976字节jb。-21#int $ 3#underment in debugger在这里red#结束bootstrap 得到这个工作是很多tr ......