![post-mortem.png](../images/post_mortem_7efc587c07.png)

## 발견
10월 13일, [Toshi](https://twitter.com/toshiya_ma)가 [Discord](https://discord.gg/jtZn2mQvsh)의 `#sdk-js` 채널에서 예기치 않은 동작을 신고했습니다. 그는 더 이상 사용되지 않는 [TypeScript SDK](https://github.com/symbol/symbol-sdk-typescript-javascript)의 출력과 최신 [JavaScript SDK](https://github.com/symbol/symbol/tree/dev/sdk/javascript)의 출력을 비교하던 중 총 트랜잭션 해시가 다르게 계산되는 것을 발견했습니다. 이는... 예상치 못한 행동이었습니다. 더욱 우려스러운 것은 두 트랜잭션이 모두 네트워크에서 승인(및 확인)되고 있다는 사실입니다.

## 근본 원인
팀은 신속하게 두 가지 중요한 버그를 찾아냈습니다. 하나는 Catapult(Symbol 클라이언트)에서, 다른 하나는 TypeScript 및 Java SDK에서 발견되었습니다.

### 클라이언트
Catapult의 "후시초 2"(2019년 11월 8일) 릴리스에서, 완료 및 본딩된 집계 트랜잭션의 트랜잭션 해시 필드를 검증하기 위해 집계 트랜잭션 플러그인에 새로운 유효성 검사기가 추가되었습니다. 안타깝게도 감독 소홀로 인해 집계 트랜잭션 플러그인에 등록되지 않았으며, 따라서 호출되지 않았습니다.

```
manager.addStatelessValidatorHook([config](auto& builder) {
        // 다음 줄이 있어야 하지만 그렇지 않았습니다.
    builder.add(validators::CreateAggregateTransactionsHashValidator()); 

    builder.add(validators::CreateBasicAggregateCosignaturesValidator(
			config.MaxTransactionsPerAggregate,
			config.MaxCosignaturesPerAggregate)));
	if (config.EnableStrictCosignatureCheck)
		builder.add(validators::CreateStrictAggregateCosignaturesValidator());
});
```

출시를 서두르다 보니 `Failure_Aggregate_Transactions_Hash_Mismatch` 오류 코드를 트리거하는 E2E 테스트를 추가하는 것을 잊어버렸습니다. 결과적으로 심볼은 집계 트랜잭션 해시에 대한 적절한 검증 없이 출시되었습니다.

또한, 더 이상 사용되지 않는 타입스크립트 SDK, 자바 SDK, 파이썬 및 자바스크립트 SDK 간에 집계 트랜잭션의 해시 계산에 불일치가 있었습니다.

Python 및 JavaScript SDK는 집계 트랜잭션 해시를 올바르게 계산하는 반면, 올해 초 더 이상 사용되지 않는 TypeScript SDK와 Java SDK는 그렇지 않았습니다.

### SDK
(더 이상 사용되지 않는) TypeScript SDK에는 계산에 두 가지 버그가 있습니다.

집계 트랜잭션은 임베디드 트랜잭션의 컨테이너라는 점을 기억하세요. 각 임베디드 트랜잭션은 8바이트 바운데이에서 시작하도록 보장됩니다. 이를 위해 필요할 때마다 트랜잭션 사이에 0으로 설정된 패드 바이트가 삽입됩니다. 임베디드 트랜잭션 해시를 계산할 때는 트랜잭션의 의미 있는 데이터만 해시하고 0 패드 바이트는 제외해야 합니다. 안타깝게도 TypeScript SDK는 해시 계산에 이러한 제로 패드 바이트를 포함하고 있었습니다. 실망스럽기는 하지만 보안이 저하되지는 않습니다.

실제 버그는 머클 해시 계산에서 발견되었습니다. 스플라이스를 잘못 사용하여 요소를 **대체**하는 대신 스플라이스를 **삽입**했습니다. 대체하려면 두 번째 매개변수가 1이어야 했습니다.

```
hashes.splice(i / 2, 0, this.hash([hashes[i], hashes[i + 1]]));
```

테스트 커버리지가 부족하고 표준 테스트 벡터를 활용하지 않았기 때문에 이 두 가지 간과 사항은 모두 눈에 띄지 않았습니다.

(더 이상 사용되지 않는) Java SDK에도 TypeScript SDK와 동일한 머클 해시 계산 버그가 있었습니다. 놀랍게도 내장된 트랜잭션 해시는 (패딩 없이) 올바르게 계산되었습니다.

이 두 SDK가 수년 동안 서로 다른 총 트랜잭션 해시를 계산해왔는데도 지금까지 아무도 눈치채지 못했다는 사실이 다소 우습습니다. 최소한 공통된 테스트 벡터를 사용했어야 합니다.

공격의 작동 방식 ###
집계 트랜잭션 해시 확인이 누락되었기 때문에 공동 서명자는 집계 트랜잭션 헤더에만 서명하게 됩니다. 따라서 유일한 제약 조건은 포함된 총 트랜잭션 크기가 헤더의 payload_size와 일치해야 한다는 것입니다.

TypeScript 및 Java SDK에서 사용하는 잘못된 머클 해시 알고리즘의 버그로 인해, 손상된 머클 해시는 트랜잭션의 하위 집합만 수정으로부터 보호합니다. 예를 들어, 3개의 트랜잭션이 있는 경우 처음 두 개의 트랜잭션만 수정으로부터 보호되고 세 번째 트랜잭션은 보호되지 않습니다. 세 번째 트랜잭션의 크기가 변하지 않는 한, 무엇이든 대체할 수 있습니다.

### 공격 예시: 집계 완료
모자이크를 교환하려는 앨리스, 밥, 찰리라는 세 명의 참가자가 있다고 가정해봅시다.

앨리스는 밥과 찰리에게 각각 알파 모자이크 10개를 지불하려고 합니다.
그 대가로 찰리는 100 XYM을 지불하기로 동의했습니다.

앨리스는 세 부분으로 구성된 전체 트랜잭션을 생성합니다:

1. 앨리스가 밥에게 알파카 10개를 보냅니다.
2. 앨리스가 찰리에게 알파카 10개를 보냅니다.
3. 찰리가 앨리스에게 100 XYM을 보냅니다.

트랜잭션을 생성한 후 앨리스는 트랜잭션에 서명합니다. 앨리스는 이를 공동 서명자인 밥에게 전달합니다. 밥은 앨리스를 속이려는 찰리에게 이를 전달합니다.

찰리는 이 사실을 알고 세 번째 트랜잭션을 대체하여 앨리스에게 0 XYM을 대신 보냅니다. 또는 더 나쁜 경우, 앨리스를 속이기 위해 앨리스가

Translated with www.DeepL.com/Translator (free version)
